How to call C/C++ code from Node.js
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.
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 runningyarn 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!