How to call C/C++ code from Node.js

Konstantin Tarkus
5 min readSep 3, 2017

It might be NOT that hard as you think! Let’s see you can you can call C/C++ code or use a native library from a Node.js app.

Node.js ❤ C/C++

Q: Why would I need to call C/C++ code? There is half a million NPM modules, why would I need native stuff at all?

A: It you don’t need any C/C++ interop right now, it doesn’t mean you won’t need it in the future. In large apps there is usually a mix of different programming languages and technologies is used, you don’t want to limit yourself to working with a single stack, but instead, choose the right tool for the job.

Consider, for example, a password hashing function that is used during sign in process. It’s takes at least 200ms to calculate a hash string, by design! You don’t want to make it run on the same thread where your Node.js app is running, at least, not in a production environment.

The process looks as follows. You create a file with some C++ code (it may call some 3rd party library), place binding.gyp configuration file in the root of your project, telling what exactly needs to be compiled natively (more on that later), add gypfile: true flag to package.json, run yarn install (or, npm install). It will compile your C++ code into a /build/Release/<name>.node file, that later on can be referenced from inside JavaScript code and used as if it was a regular JavaScript module. You don’t even need the node-gyp module installed globally (or, locally) for that, because npm (or, yarn) can use the built-in node-gyp library to compile your code.

This <name>.node thingy is called an “addon” in Node.js terminology.

Node.js Addons are dynamically-linked shared objects, written in C++, that can be loaded into Node.js using the require() function, and used just as if they were an ordinary Node.js module. They are used primarily to provide an interface between JavaScript running in Node.js and C/C++ libraries.

Hello World

Let’s start by rewriting the following piece of JavaScript code into C++:

module.exports.hello = () => 'world';

Assuming that you already have a sandox project with a Node.js app, create two files in the root of your project — binding.cpp and binding.gyp with the following content:

binding.cpp

#include <napi.h>using namespace Napi;String Hello(const CallbackInfo& info) {
return String::New(info.Env(), "world");
}
void Init(Env env, Object exports, Object module) {
exports.Set("hello", Function::New(env, Hello));
}
NODE_API_MODULE(addon, Init)

binding.gyp

{
"targets": [
{
"target_name": "native",
"sources": [
"binding.cpp"
],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"cflags!": ["-fno-exceptions"],
"cflags_cc!": ["-fno-exceptions"],
"defines": ["NAPI_CPP_EXCEPTIONS"]
}
]
}

Also, update package.json file in the root of your project to include gypfile: true flag and node-addon-api dependency:

{
"name": "app",
"version": "0.0.0",
"private": true,
"gypfile": true,
"dependencies": {
"node-addon-api": "^0.6.3"
}
}

The target_name field in binding.gyp file is used to tell npm/yarn under what name to save the compiled output file. In our case it will be saved into build/Release/native.node.

The binding.gyp > targets > sources field contains the list of C/C++ source files that need to be compiled. If you add another .cpp or .h file, don’t forget to append it to that list.

The Env, Object, String, CallbackInfo in thebinding.cpp file are C++ wrapper classes over Node’s C API (aka N-API) imported from the Napi namespace (notice using namespace Napi; at the top of the file). If you’re curious, you can find their implementations inside of the nodejs/node-addon-api project on GitHub (see napi.h for example). The actual source files from “node-addon-api” helper module are referenced in the include_dirs and dependencies fields in the binding.gyp file.

Now, after building the project via yarn install (or, npm install) you must be able to call your native module from JavaScript. The only gotcha is that “node-addon-api” is still an experimental feature and works only under --napi-modules flag. So you can execute it as follows:

$ node --napi-modules -e \
"console.log(require('./build/Release/native.node').hello())"
world

There is one more useful library called bindings that allows importing Node.js addons without providing the full path-name to the .node file. Install it by running yarn add bindings, then you’ll be able to reference our native module as follows: const native = require('bindings')('native');

Using a native C/C++ library from Node.js

Now, let’s call some 3rd party library from under our native module (addon). I’d like to believe that you develop your Node.js apps inside Docker containers. As an example, let’s install Sodium crypto library and try to call its methods from our binding.cpp file. In node:8.4.0-alpine Docker container you can install it as follows:

apk add --no-cache make g++ python2 libsodium-dev

The make , g++ and python2 modules are used in order to be able to compile C/C++ code inside node:alpine container, and libsodium-dev is the library itself that we will use. In development mode you would use libsodium-dev as it comes with the source files and in production mode you would install libsodium.

Now let’s update binding.gyp file to reference libsodium as follows:

{
"targets": [
{
...
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")",
"/usr/include/sodium"
],
...
"libraries": ["/usr/lib/libsodium.so.18"],
...
}
]
}

Now you can reference sodium.h at the top of the binding.cpp file and try to call some of the libsodium’s methods. We’re going to use crypto_pwhash_str function in our example that converts a password string into a hash using Argon2 password hashing algorithm:

#include <napi.h>
#include <sodium.h>
using namespace Napi;String Hash(const CallbackInfo& info) {
Env env = info.Env();
std::string password = info[0].As<String>().Utf8Value();
char hash[crypto_pwhash_STRBYTES];
crypto_pwhash_str(
hash,
password.c_str(),
password.size(),
crypto_pwhash_OPSLIMIT_INTERACTIVE,
crypto_pwhash_MEMLIMIT_INTERACTIVE);
return String::New(env, hash.c_str(), crypto_pwhash_STRBYTES);
}
void Init(Env env, Object exports, Object module) {
exports.Set("hash", Function::New(env, Hash));
}
NODE_API_MODULE(addon, Init)

Now, after running yarn install you can run this code in Node:

$ node --napi-modules -e \
"console.log(require('bindings')('native').hash('Passw0rd'))"
$argon2i$v=19$m=32768,t=4,p=1$/N3vumg47o4EfbdB5FZ5xQ$utzaQCjEKmBTW1g1+50KUOgsRdUmRhNI1TfuxA8X9qU

There is a little bit more work to make this code work asynchronously, but it’s still pretty easy to do. You can find a working example here:

src/utils/password_hash.cpp in the Node.js API Starter project on GitHub.

Summary

Calling C/C++ code using modern N-API interface in combination with C++ wrappers (node-addon-api) is almost too easy to be true. I hope the code samples above demonstrate the point. Just try to avoid using old APIs and approaches, e.g. Nan, using C instead of C++, and you’ll be good :)

Happy coding!

--

--

Konstantin Tarkus

Empowering startups with cutting-edge expertise: software architecture, optimal practices, database design, web infrastructure, and DevOps mastery.