A Guide for JavaScript Developers to build C++ Add-ons with node-addon-api

Bianca Cao
AI2 Labs
Published in
10 min readJan 8, 2021

Do you expect to reuse a third party C++ library in your Node.js project?
Are you jealous about the outstanding C++ performance while coding something computationally heavy in JavaScript?

It is not something impossible to access C++ from JavaScript. Here you will see how I make it.

Background Introduction

JavaScript, as the programming language for the Web, is quite popular and easy to learn, while C++ is static typed and compiled language that quite hard to pick up by anyone who only stick with dynamic and interpreted languages. To be specific, JavaScript allows you to change variables dynamically into any data type but in C++ you need to declare it first. In addition, C++ need a compiler to transfer your codes into executable ones, yet JavaScript interpreter will just read and run them line by line. That is why developers usually would not feel pleasantly transforming from JavaScript to C++.

Nowadays, JavaScript can not only be used for websites, but also be strong enough to support some server-side applications. More and more people are using web frameworks with great performance and lower develop cost including Express, Koa, fastify, socket.io and so on.

However, some underlying libraries have not been extended into Node.js yet. So, what are the options if you want to call C++ functions or libraries from JavaScript without an available Node package?

This blog will explain several alternatives and dig into node-addon-api, talking about how I build my first JavaScript bindings to OpenMesh C++ library. You can also find my source code for my at https://github.com/BiancaZYCao/TriMesh4Js to have a quick read.

Option 1 FFI: node-ffi OR node-ffi-napi

A foreign function interface (FFI) is a mechanism by which a program written in one programming language can call routines or make use of services written in another.

You can call C/C++ codes by loading DLL (Dynamic Link Library) to use those functions without writing any C/C++ code. In Foreign Function Interface for Node.js, you can find this as a summary for its mechanism:

synchronous translation pipeline

To give you an idea about what it looks like, here is a simple example to bind C++ ceil() function:

var ffi = require('ffi-napi');
var libm = ffi.Library('libm', {
'ceil': [ 'double', [ 'double' ] ]
});
libm.ceil(1.5); // 2

👍 Advantages:

  • You can do it without source code file
  • There is no need to write any C/C++ code
  • There is no need to recompile after any changes

✋ Limitations:

  • acts like a black box sometimes: It will be hard to understand what is going on inside, unless you have strong debugging skills to make sure that all the arguments passed into a certain function correctly for C++ execution and whether the results returned back is right.
  • a certain performance loss: As you can see in the pipeline image It basically convert the interface defined in JavaScript into an interface in C, and share NodeJS’s Buffer memory with the loaded DLL. That is why it works with any DLL yet may sacrifice some performance.
  • accepts DLL only: As DLL is the only thing it calls, you have to pack your own written codes into DLL before using it.

In short, this will be a good choice when you would like to call the shared library directly just for some functions that you are familiar with by passing the parameters in and get value back. Unfortunately, I gave it up after getting lost in the functions for some reasons which I will explain later.

looking for an easy way to use it? You might want to try node-ffi-generate.

Option 2 Node.js Extensions

Actually, Node.js offers three official ways for implementing C/C++ addons: N-API, nan, or direct use of internal V8, libuv and Node.js libraries. As you do not need to understand all those underlying components/APIs, N-API is recommended among these choices unless you need those functionalities have not exposed.

A Native Tool : NAN (Native Abstractions for Node.js)

Before N-API released, NAN provided a V8 abstraction layer to support add-on development easier working with various version. As the older way to implement C++ bindings, the main disadvantage is that all the codes needs to be updated along with every upgrade of V8 Engine. Thus, you are not likely to choose it especially building the new Add-ons.
Here is an example about NAN binding functions (You can find this in opencv4nodejs. This function is to get raw pixel values from a image mat in Node.js: const [b, g, r] = matBGR.atRaw(200, 100); )

NAN extension example

The Modern Choice: N-API

N-API provides an ABI-stable API that can be used to develop native add-ons for Node.js, simplifying the task of building and supporting such add-ons across Node.js versions (stable since Node.js v10).

Application Binary Interface (ABI) plays a key role on the interaction between software programs and system binaries. ABI stability guarantees different version compilers will generate compatible binary code following some certain rules.

Similar to NAN, N-API allows you to pass data between C/C++ and JavaScript and to do any operations you want to on both sides. Besides, it also abstracts out its API from the underlying JavaScript engine to make your addons separate from the underlying V8 engine. Below illustrate how N-API works and the “helloworld” example code will be shown afterwards in my personal journey.

How N-API works

A Simpler Choice for C++ Add-ons: node-addon-api

It contains header-only C++ wrapper classes which simplify the use of the C based N-API provided by Node.js when using C++.
The purpose of this module is to raise the N-API API from the level of “C” up to “C++“.

Even though it is not counted as a Node.js official module, it at least provides an easier option for beginners who expect to

  • make bindings on an existing C++ library
  • migrate computationally intensive tasks in C++ for better performance

without much effort and fully understanding underlying Node Libraries.

The journey to create customized modules with node-addon-api is less painful provided with the C++ object model in napi.h. The example codes were also more readable and clearer than N-API so it finally became my choice. After building your Add-ons, you are able to called it via require(). I will compare the codes with N-API in details in next chapter. Here is my understanding about the overall architecture that maybe helpful.

Big Picture about C++ Add-ons

My Experience with these options

The node-ffi caught my eyes firstly but it was no longer campatible, so I quickly moved on node-ffi-napi instead. It was confusing because the dll files were just black boxes to me and it was not easy to use with sepcific data structure. For example, to create a 5-number array and fill it with 9 as [9,9,9,9,9] ( This should be included in basic libc++ dll), I felt hard to bind with FFI even I know how to code it in C++ like
std::array<int,5> myarray;
myarray.fill(9);
It kept telling me symbol/image not found if not correct. And yes, I did export symbols from dll, but it was also not much helpful to match those functions. I had no idea about how to diagnose and debug such an simple function, not to mention, using it for a C++ library holding complicated data structures.

After a while, I started to looking at the extension option. NAN was kind of outdated and N-API was still hard to pick up for me especially because I never learned or wrote any code in C/C++ . The “helloworld” example is as follows:

helloworld example N-API

Compared with that, The “helloworld” example in node-addon-api looks much more friendly and clear.

helloworld example using node-addon-api

Therefore, I started my journey with node-addon-api and finally came up with something can be called in my Node.js project indeed.

How to start the first real binding?

Environment Prerequisites

-C++ ( I used CMake/make to compile C++ code)
- Python (for node-gyp)
- Node and npm
- Command line tools & editor
- C++ library (OpenMesh download or simply brew install open-mesh)

Dependencies need to be installed in your Node.js project

node-gyp — Node.js native addon build tool (installed globally)
other build tool alternatives are CMake.js node-pre-gyp and prebuild
node-addon-api : follow setup
bindings : npm install --save bindings

Reminder before we start

For people like me who never touched C/C++ before, it would be very helpful to prepare yourself with basic concepts including compiler, linker, pointer, reference, handle, etc.

You can find sample codes in node-addon-examples and node-cpp-skel to play around fundamental bindings. However, one or more practical repositories are highly recommended when it comes to build something more complicated than simple functions, classes and objects. MAVJS brought me inspirations personally, but you can pick any other projects to refer to (not only about how to code add-ons, but also how to write binding configuration, test in .js and even deploy it as a package)

I will focus mainly on my experience about writing binding C++ codes for OpenMesh.

Write the binding codes in C++

Since OpenMesh is used as a data structure which is a mesh object wrapper, I edit on node-addon-examples/6_object_warp templates. I am going to highlight only a few points. Please check my code myRealMesh.cc for details.

How to write an Object Wrapper Class

a typical Object Wrap

Things should pay attention to or may be confusing

  1. Check object_lifetime_management if Napi::HandleScope or Napi::EscapableHandleScope is necessary (usually not)
  2. Build Class Constructor properly by creating a peristent reference to allow functions to be called on a specific instance. Do not forget to add the SuppressDestruct method if you need to prevent its destructor from attempting to reset the reference when the environment is no longer valid.
  3. Check if your object is supporting multiple instances (can be created and distinguished)

How to implement a function

A typical Napi function will include:
- receiving arguments passing in as Napi::CallbackInfo&
- transferring into C++ type like double & doing some operations in C++
- returning result in any Napi data type

A typical function

Basic Napi Data types

Basic Napi Data types
Inheritance relationship

Something I did creatively

Here I wanna share something creative (though may not be normal): how I defined a const pointer to pass the handle reference around.
In OpenMesh, the VertexHandle represents every vertex on the mesh and is required when adding a new face (a triangle with 3 vertices). When I looked up in OpenMesh Binding for Python, the mechanism is just exposing everything directly to Python output like <openmesh.VertexHandle object at 0x126471378>. Unlike that in pybind11, I tried to return the reference back in JavaScript with Napi::External but it became invalid when passing back to add a face with error Segmentation fault: 11. After printing everything out, I realized it is dynamically changes every time manipulating the mesh.
The easiest solution is to use vertexID tracking each point instead, which means you are only sending and receiving integer value for all operations. Or, if sticking on the reference, I have to manually claim a constant pointer in my object wrapper (myMesh) to make it into a static value that can be matched on both C++ and Js sides.

std::vector<TriMesh::VertexHandle>  vHStorage_; 
std::vector<TriMesh::VertexHandle> * const ptrVHS = &vHStorage_; //vHStorage_ is dynamic while ptrVHS remains as a constant

Following Steps

prepare your inding.gyp file
Before building your add-on code into .node, you need to declare all the information in binding.gyp, including target_name, source code files, directories, libraries and compile conditions. For details, you can take a look at the gyp user documentation.

build executable .node files by node-gyp or other tools
Basically, just run node-gyp configure then node-gyp build. If you meet any errors or warnings at this stage, you need to fix them before continue. Sometimes, testing in pure C++ or by debuggers like gdb will be useful to diagnose your bugs. In addition, every time you changes your C++ code, you need to rebuild it again as update.

try the add-ons in your .js file & write test.js
To call it, just one more line on top of your .js file:
let addon = require("bindings")("<target_name>")
and call it just like any other libraries.

call bindings to build a tri-Mesh

For test files, I used mocha and assert module to check all the features in my extension code work well.

test sample with mocha for the get mesh points function

Congratulations! You have just crossed the finish line. 🎉 🎉🎉

I cannot believe I made this happen within 2 weeks, starting from all the concepts and learning C/C++. After exploring all the documentations, code samples and blog articles, I feel like it is worthwhile to record this as a supplementation to limited resources especially for JavaScript developers. Hope my journey be useful to choose a suitable tool for your case and to create C++ addons for NodeJS.

--

--