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.