Native modules for Node.js with N-API

Writing C/C++ addon is a useful and powerful feature of the Node.

js runtime.

Let’s explore them from their internals, to their development and publication.

Run JavaScriptNode.

js libraries take the form of modules that you require in your Node.

js application.

const foo = require(‘foo’);These modules are usually written in JavaScript, and export a set of functions and classes.

There is an other way of writing modules, which is the topic of this article: native addons, in C++.

When writing a native addon for Node.

js, the code you want to export to JavaScript needs to be understood by V8.

We call that a binding, i.

e.

linking your native code to JavaScript constructs.

std::string text = "Hello World !";v8::Local<v8::String> jsText = v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), text.

c_str());As you can see in this code sample, we use V8 types to specify a JavaScript string.

The syntax is bit heavy and we won’t actually do things this way.

But keep in mind that what we are doing is using V8 to tell what is the equivalent JavaScript types of our C++ constructs.

Because native code performs better than JavaScript, it makes sense to move heavy computation load towards a native addon.

Beware of the slight overhead when switching from JavaScript context to C++: it is usually negligible, but it will reduce performance if abused.

Node.

js runs on a single thread, the event loop.

It delegates blocking tasks to some other threads, workers of the worker pool.

When writing a native addon, you can and should also create workers for your heavy tasks or I/O blocking tasks.

If you want to use a C or C++ library in your Node.

js code, you can bind the library functionalities so that it can be used in Node.

js.

This is typically what is done is the module krb5.

We bind the necessary functions of MIT Kerberos library in order to build a kinit and kdestroy function.

The way to develop native modules has evolved.

We will see a brief description of each method, as you may encounter useful resources in your research that use the previous methods, especially NAN.

Using the node library is the most basic way to develop a native addon.

The node library provides an easy way to export your module, as well as a user-friendly class, node::ObjectWrap , to bind C++ objects.

Apart from that, you will basically have to do everything using the V8 library directly.

Furthermore, you will have to get your hands on libuv for asynchronism, which is quite complex.

To make things easier, the NAN library has been created: Native Abstraction for Node.

js.

It wraps the node library and libuv.

You don’t have to mess with libuv directly, you can simply use the AsyncWorker class for asynchronous tasks.

However, you still need to use the V8 library.

Last but not least, N-API, the future of native addons development, officially supported by Node.

js core developers.

It basically abstracts everything.

It abstracts node library and libuv (like NAN did) but it also abstracts V8.

Why is the V8 abstraction a nice feature?.First, it makes the code more simple and readable, but there are other reasons.

There is currently some efforts to implement Node.

js using other JavaScript engines (for instance, node-jsc, on top of JavaScriptCore).

With the abstraction offered by N-API, your native addon should be able (in the future) to work for both engines.

This abstraction also guarantees that you won’t have to change your code when the V8 API changes (which actually regularly happens).

An important feature of N-API is its portability between Node.

js versions.

If you compiled your addon against Node.

js 8, you can safely upgrade to Node.

js 10 without having to recompile the addon.

N-API is a C library.

However, a C++ wrapper is available: node-addon-api.

I recommend it as it makes the code easier to write, and looks very much like NAN.

Building a native addon is a trivial task.

We use the node-gyp tool.

We only need to fill the binding.

gyp file containing the path to the source files, the include directories, the dependencies and the compiler options.

You can also set specific parameters for specific OS or architectures.

When using N-API with the node-addon-api, here is what your binding.

gyp file should look like:{ "targets":[{ "target_name":"krb5", "sources":[ ".

/src/module.

cc", ".

/src/foo.

cc" ], "cflags!":['-fno-exceptions'], "cflags_cc!":['-fno-exceptions'], "include_dirs":["<!@(node -p "require('node-addon-api').

include")"], "dependencies":["<!(node -p "require('node-addon-api').

gyp")"], }]}You will also need to add the node-addon-api dependency to your package.

json file:"dependencies": { "node-addon-api":"latest"}This is a simple approach: just ship your code and run the node-gyp rebuild command when installing the module.

Just add it to your package.

json:"scripts": { "install": "node-gyp rebuild"}Drawback: you need to have all building utilities on the target machine, i.

e.

gcc (c++), make, Python 2 and Bison.

The idea here is to build your module and share the compiled files.

This is a fairly new approach especially for N-API support, but I would argue that it is the cleanest way of sharing your module.

You won’t need building dependencies on the target machine, and won’t have to run the module compilation.

First, you need to compile your native addon with the Prebuild tool.

Now there are two possibilities.

The first one is to push the compiled addon, located in the prebuilds folder, used in your Node.

js module.

They will be downloaded with NPM directly and you just install them using the prebuild-install module.

Here is a typical set of scripts to get you going:"scripts": { "install": "prebuild-install || node-gyp rebuild", "prebuild": "prebuild –verbose –strip"}The prebuild-install is automatically called on npm install.

If the prebuild-install didn’t work, it will launch the node-gyp compilation.

npm run prebuild runs the prebuild and store the binaries in the prebuilds folder.

The second way, DevOps approach, is to have a CI chain to test and release your module.

A simple chain would be to test your module in Travis and publish the newly compiled addon on GitHub releases when tests are passing.

Refer to this page explaining how to upload to GitHub releases.

Once uploaded, use prebuild-install — download [URL] to download and install the compiled addon.

Note: You may need to include some external libraries for your native addon.

This is typically the case when binding a native library for Node.

js.

You have different ways to do so.

If the library is well supported by system packet managers, you can use them.

It requires to launch the packet manager on the target machine.

If you want to avoid that, you can ship the library with your module.

This is only a good solution for small libraries, as you might get limited by NPM package size.

If you ship the library source code, you can compile it using a secondary GYP file that you reference in your module GYP file.

As an example, you can take a look at the leveldown module.

Data from the event loop cannot be accessed in a worker thread.

You need to make a copy.

Example, use class attributes to share data:class Worker_double_it: public Napi::AsyncWorker { public: Worker_double_it(int number, Napi::Function& callback) : Napi::AsyncWorker(callback), number_copy(number) { } private: void Execute() { number_copy *= 2; } void OnOK() { Napi::HandleScope scope(Env()); Callback().

Call({ Napi::Number::New(Env(), number_copy), }); } int number_copy;}; Napi::Value double_it(const Napi::CallbackInfo& info) { if (info.

Length() < 2) { throw Napi::TypeError::New(info.

Env(), "2 arguments expected"); } int number = info[0].

As<Napi::Number>().

Int32Value(); Napi::Function callback = info[1].

As<Napi::Function>(); Worker_double_it* worker = new Worker_double_it(number, callback); worker->Queue(); return info.

Env().

Undefined();}This example shows the implementation of a function that takes an integer as input, runs a worker thread to double it (clearly overkill, just for the sake of demonstration), and calls the callback with the doubled input as parameter.

The full sample project is available here.

As we said, you should not try to access the number variable in the Execute() method.

First, copy it, as we did with number_copy .

The JavaScript code would look like:const promaths = require(".

/build/Release/promaths")promaths.

double_it(3, function (res) { console.

log(res); //outputs 6});Note: Design your asynchronism mechanisms correctly.

You really should read the following article if you haven’t already: Don’t Block the Event Loop (or the Worker Pool).

Among other things, you should not flood the worker pool, neither should you block it with long-running threads.

Create your JavaScript APIThe overall application using a native addon should look like this:the application requires a module available on NPM:// Application – index.

jsconst myModule = require("myModule"); myModule.

myFunction();the module “myModule” requires the native addon:// myModule – lib/index.

jsconst native_myModule = require(".

/build/Release/native_myModule") module.

exports = { myFunction: function() { // do something with your native addon here native_myModule.

myNativeFunction(); }}Your NPM module should expose features brought by your native addon.

This allows you to write your API regardless of how it is implemented in the native addon.

For example, you cannot return promises from your native addon, only callbacks.

If you want your API to work with promise, you would need to wrap the addon somehow.

You could even want to have your API work for both callbacks and promises, depending on whether a callback has been passed or not (see an example here).

I want to mention these two types, buffers and externals, as they are not very common, or at least not intuitive.

Yet, they are powerful and will solve many difficult bindings.

Buffers are raw data, bytes stored in V8.

You can take any C++ variable from any type, copy it into a buffer that is accessible in JavaScript (although it’s just raw data), and convert it back to a C++ type.

It basically is a cast to unsigned char* to create the buffer, and a reinterpret_cast<T> to cast it back to the original type T.

You can use this type when you want to store data outside the scope of your C++ code and be able to move it around from JavaScript.

You can, for example, use this for a complex structure that has many layers of other structures etc.

And you don’t really care to be able to read this data in JavaScript as it is used only by your C++ code.

You just store it in bytes, move it around in JavaScript, and pass it back to C++ whenever you want to use it again.

Externals serve a similar purpose, except that you don’t store the data in V8’s memory.

You let the C++ code handle the variable.

The V8 external variable just stores the pointer, i.

e.

the memory address, of the externally managed variable.

This is extremely useful when you have some heavy variables that you use all the time, in many of your functions (typically a library context structure).

You could use buffers instead, but buffers are a lot more heavy as you copy the whole data each time.

With the information provided in this article, you should be able to have a clear view of how to start developing a native addon, and what tools you should use in the process.

We have seen that the preferred way nowadays is to use the N-API, and even though it wraps everything, you should understand what is going on behind the scene.

The tips and example should be a nice addition to the documentation as I felt it was missing a bit of guidance.

Happy development!Copyright 2004–2016 Adaltas | All Rights ReservedOriginally published at www.

adaltas.

com on December 12, 2018.

.

. More details

Leave a Reply