Calling C++ from Node.js (Part 1)
Node has bindings to call C code directly, but you can instead make it nice object-oriented C++ by using node-addon-api
. Here’s how you do it.
But first, a digression. Probably one of the most useful things you can do in software is use a higher level language to call a lower one. Write processing for the performance intensive workload in the lower language, then rely on the higher language for easier scripting.
An unfortunate reality is that there is no C++ web framework in wide use. Drogon exists, but hasn’t caught on yet compared to Express/Ruby/Spring/Django. So let’s say you want to use the high performance functions in the Boost library on the backend of your new web project. Rather than write the whole thing in C++, or ship an executable, or ship a script wrapping an executable, or deploy yet another microservice, why not instead take advantage of Node’s ability to call C++ directly?
In this tutorial I use Node v20, the most recent long-term support. Also, the code for this project is available here.
cbrown@Chriss-MacBook-Air Documents % mkdir performant-node
cbrown@Chriss-MacBook-Air Documents % cd performant-node
cbrown@Chriss-MacBook-Air performant-node % npm init -y
cbrown@Chriss-MacBook-Air performant-node % npm install node-addon-api
cbrown@Chriss-MacBook-Air performant-node % npm install --save-dev node-gyp
cbrown@Chriss-MacBook-Air performant-node % mkdir src
cbrown@Chriss-MacBook-Air performant-node % touch src/main.cpp
At this point, build tools are set up, and it’s time to start adding source code.
cbrown@Chriss-MacBook-Air performant-node % mkdir src
cbrown@Chriss-MacBook-Air performant-node % touch src/main.cpp
Let’s start with something simple to learn the scaffolding, into main.cpp
copy the following.
#define NAPI_DISABLE_CPP_EXCEPTIONS
#include <napi.h>
using Napi::CallbackInfo;
using Napi::Env;
using Napi::Function;
using Napi::Number;
using Napi::Object;
using Napi::String;
using Napi::TypeError;
Number addTwo(const CallbackInfo &info)
{
Env env = info.Env();
// check if arguments are numbers
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber())
{
TypeError::New(env, "Expected two numbers as arguments").ThrowAsJavaScriptException();
return Number::New(env, 0); // return default value
}
double arg0 = info[0].As<Number>().DoubleValue();
double arg1 = info[1].As<Number>().DoubleValue();
double sum = arg0 + arg1;
return Number::New(env, sum);
}
Object Init(Env env, Object exports)
{
exports.Set(String::New(env, "addTwo"),
Function::New(env, addTwo));
return exports;
}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)
Code explanation
#define NAPI_DISABLE_CPP_EXCEPTIONS
is necessary for the node-gyp
to know whether or not to expect C++ to be able to throw exceptions. We disable it here to avoid making more changes to get this to compile.
#include <napi.h>
is the header for what you installed when you added node-addon-api
to your project, for example
cbrown@Chriss-MacBook-Air performant-node % find . -name "napi.h"
./node_modules/node-addon-api/napi.h
Defining function Object Init
and feeding it into the NODE_API_MODULE
macro is standard boilerplate for providing compatibility with Node.
The crux of this is defining addTwo
which returns an Napi::Number
, and to do so we access const CallbackInfo &info
. This parameter will contain all the information the function is going to get from Node, but we first have to validate that it’s actually a number. Below we convert it to a double and return the result, but we didn’t have to convert it to a double—we used DoubleValue()
, but could have used FloatValue()
or Int64Value()
instead, among other options.
By the way, if you’re using VS code and it’s struggling to find <napi.h>
, make sure to add /usr/local/include/node
to your include path.
{
"configurations": [
{
...
"includePath": [
"${workspaceFolder}/**",
"/usr/local/include/node"
],
...
}
],
...
}
The rest
Now create everything surrounding our C++ function.
cbrown@Chriss-MacBook-Air performant-node % touch binding.gyp
cbrown@Chriss-MacBook-Air performant-node % touch index.js
{
"targets": [
{
"target_name": "addon",
"sources": [
"./src/main.cpp"
],
"include_dirs": [
"<!(node -p \"require('node-addon-api').include_dir\")"
]
}
]
}
const addon = require("./build/Release/addon");
console.log(addon.addTwo(200, 300));
Now build then run index.js to add the two numbers. You should see 500 printed out to the terminal.
cbrown@Chriss-MacBook-Air performant-node % npx node-gyp clean && npx node-gyp configure build
cbrown@Chriss-MacBook-Air performant-node % node index.js
500
Just what we hoped for.
Coming up in part two, adding in external libraries like Boost. Or running C++ from Javascript on the client side.