N-API and getting started with writing C addons for Node.js
The latest release of Node.js introduced some significant changes and API additions to the Node world but perhaps the least talked about addition which is incidentally is released in v8.x of Node is getting further away from V8 (the JS engine) as the heart of Node.
N-API allows developers to write C/C++ addons without having a deep understanding of working of V8 and provides abstractions more true to Node and Javascript in general.
Update: This article is being constantly updated against breaking changes in N-API since the api itself is still experimental. The current version of the article is based on Node 8.6.x up to 8.8.x, for a earlier versions refer to this repo.
A bit of History
C/C++ addons have long been a part of Node.js’ ecosystem yet it was always a challenge to develop these addons even for developers coming from the C world. It is even stated in the docs “At the moment, the method for implementing addons is rather complicated, involving knowledge of several components and APIs”. Perhaps the biggest hurdle has been the V8 internals, dealing with V8 involves a good understanding of both C++, JS and then some about the internal workings of V8.
Note that NaN (Node Abstractions for Node) was a great layer of abstraction but specific to V8.
The issue is that V8 APIs weren’t widely popular before Node.js came around, the consumer facing V8 had a single purpose, serve JS and serve it fast to Chrome, the people who integrated Webkit and other parts of Chrome with V8 were the only ones who needed in depth knowledge of V8, everyone else rode along. Although V8 changed the landscape of the web and touched the back-end world, it wasn’t designed with everyday Node modules in mind and just a while ago Microsoft decided that their JS Engine Chakra must be as good or better than V8 to be widely adopted, so they decided to dethrone V8 as the only king of VMs in Node.
The brainchild of Node-ChakraCore and simplifications in the addons API lead to the introduction of N-API and somehow it managed to be impressive and simple. It allows developers to write C/C++ addons without having a deep understanding of working of V8 and provides abstractions more true to Node and Javascript in general.
Getting started with a Node module in N-API
In this article I will be covering some of the great additions and benefits of this “experimental” API for JS developers wanting to develop addons and expand the reaches of Node. The native addon world should be a lot less intimidating to the average JS/C/C++ developer with N-API as you would not be dealing with V8 directly. Most of these abstractions are implemented in C and have syntactic sugar wrappings for C++.
Setting up with node-gyp
To build your native modules you can utilize node-gyp, the GYP project is another abstraction layer for build systems utilized by the Chromium project that was brought-in to Node, it’s a great tool for building native modules and will generate a makefile under the hood where applicable. You’ll need to have python installed as with most Googly projects. You can then install node-gyp from NPM:
npm install -g node-gyp
Now you can create a simple module by creating a new folder and running npm init
then add binding.gyp in your root directory and setup a target source for it.
Leave the module file empty for now, it won’t build yet but we will get to that shortly.
NAPI_MODULE and exported properties
First thing that we would need in our C/C++ module file module.c
is including the NAPI header. This is different from the node.h
that was previously available and is instead named node_api.h :
#include <node_api.h>
Likewise a new function has been implemented for initializing your Node native module, it is known as NAPI_MODULE
and behaves in a similar way as its predecessor. We’ll point our module to an Init method returning out exports:
napi_env
is an abstracted concept for a V8 Isolate or instances in other VMs, an isolates in essence is an instance of V8 with its own heap and garbage collector, we’ll be passing the env around to run within the same VM instance.
napi_value
is a beautiful abstraction over JS’s variables, it represents a JS value, you can easily create and populate these values and return them to JS-land.
By either populating exports or settings module.exports within NAPI you can configure your addon to return a particular object when required by JS. We’ll create our function in JS’ context and assign it to a napi_value
using napi_create_function
and assign the function to our exports object using napi_set_named_property
:
napi_set_named_property
takes the env, an object (exports in our case), the name of our property and the value it points to which in our case is a napi_value
of our function.
You can use the define properties call to assign properties to any JS object in your code but here we are using to to replicate something like:
Note that the return type of napi_set_named_property
is a napi_status
, N-API returns a status for almost all its calls and it is a really great way to throw meaningful errors for JS to handle. I’ll cover error handling more in the Error Handling with N-API section below but for now all you need to know is that if status == napi_ok
then you are ok.
Creating our C function
Now that we have defined our MyFunction
function in module.exports as part of descs the N-API way, let’s put our function to a reasonable use. It will accept a single JS Number as an argument, convert it to a 32 bit Int, multiply it by two and return it as another JS Number which in our case will be represented as a napi_value
. Pretty simple functionality for all the trouble but it is a better start than “Hello World”:
Our function accepts the env/instance and a napi_callback_info
, you can extract arguments and other information regarding the context from it. We have also defined status on top of our function and will reuse it for multiple calls as a best practice.
Next, we will utilize napi_get_cb_info
to fetch our arguments as an array of N-API values. We’ll need to define the total number of arguments expected or the argument count (argc) and will leave the last two arguments which are this
and the data pointer as null:
Now that we have our JS Number passed as the first member of the arguments vector, we’ll need to convert it from a JS value napi_value
to a normal int so we can multiply it in C. Since we are passing values from JS to C world our data requires some massaging. We’ll call napi_get_value_int32
which also returns a napi_status
that we can then use to verify the success of the operation. We’ll pass the function the env/instance, the N-API value and a pointer to our defined int to be populated with the results.
We can validate our statuses against the enum napi_ok
and throw a JS error using the napi_throw_error
call which accepts env, an optional error code which we will leave as null and a C string char* as the message. This way if anything goes wrong we will throw a meaningful error to JS-land.
Note that you can use the napi_throw_type_error
in this case to provide a more valid type error.
At last we will multiply our number by two and convert it back to a JS value using napi_create_int32
, by now you are probably catching on to the pattern of env/instance as the first argument and return of status in N-API.
Our function will end up looking something like this:
Once your addon is built you can require and call it from JS in the following way albeit I would recommend using the bindings module:
Here is the full source: https://github.com/schahriar/n-api-article/tree/master/Getting_Started
Building and running your addon
Now you have all the code that you need to run a program that will output the following fact:
8 times 2 equals 16
You can trigger the build using:
node-gyp configure build
and run your JS file using node with the --napi-modules
flag set (only applies to 8.5.x and below):
node --napi-modules module.js
All this work for a function that multiplies by two?
You might say that you could’ve done that in JS with a single line of code:
But the implications of C/C++ bindings are much bigger than a multiplication function that should have the same performance as its JIT-ed alternative. By writing native addons you are effectively extending Node to areas where it wasn’t suitable for before, great examples of this includes node-opencl or leveldown.
My very first experiment with native bindings was to implement a proprietary set of GLSL bindings that rendered a shader on the GPU and back to an ArrayBuffer, it felt as though I had pushed the boundaries of what was possible with Node and I would encourage you to take Node’s reach where it has never been before and be part of the wildness of the Node community.