Native modules for Node.js with N-API

Native modules for Node.js with N-API

Do you like our work......we hire!

Never miss our publications about Open Source, big data and distributed systems, low frequency of one email every two months.

How to create native modules for Node.js? How to use N-API, the future of native addons development? 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 JavaScript

The first thing to know is how JavaScript code is run. To do that, we need a JavaScript engine. For instance, each browser embeds a Javascript engine: Spidermonkey for Firefox, JavaScriptCore for Safari, and V8 for Chrome and Chromium. Node.js is also able to run JavaScript code, as it includes the V8 engine. It then has a set of functionalities outside the JavaScript scope. This is why Node.js is not a JavaScript framework, but a JavaScript runtime. It interprets JavaScript thanks to V8 which then call its own features. V8 is written in C++. Node.js, written in C++ as well, includes V8 libraries in its code.

Node.js modules

Node.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++.

Bindings

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.

Why use native modules?

  • Heavy computation

    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.

  • Asynchronous code

    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.

  • C/C++ library binding

    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.

Development guide

Libraries

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.

  • Node

    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.

  • NAN

    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.

  • N-API

    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.

Build

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"
}

Share your addon

  • Run the GYP build

    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.

  • Use Prebuild

    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.

Some useful tips

Asynchronous code

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 API

The overall application using a native addon should look like this:

  • the application requires a module available on NPM:

    // Application - index.js
    const myModule = require("myModule");
    
    myModule.myFunction();
  • the module myModule requires the native addon:

    // myModule - lib/index.js
    const 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).

Buffers and externals

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 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 JavaScripe-addon-apit. 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.

Conclusion

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!

Share this article

Canada - Morocco - France

We are a team of Open Source enthusiasts doing consulting in Big Data, Cloud, DevOps, Data Engineering, Data Science…

We provide our customers with accurate insights on how to leverage technologies to convert their use cases to projects in production, how to reduce their costs and increase the time to market.

If you enjoy reading our publications and have an interest in what we do, contact us and we will be thrilled to cooperate with you.

Support Ukrain