Skip to content

The Future of Native Modules in Node.js

N-api and prebuilt native modules are ready for prime time.

Node.js 10 to Update the Native Module Library n-api

Node.js 10 is just around the corner and with it comes a number of improvements. One that is exciting us is the update to the native modules library n-api. It comes out of experimental status in the upcoming release.

JavaScript has always had a minimal standard library compared to other languages. In the beginning we only used JavaScript in the browser. As browsers evolved and matured into application virtual machines, so did the need to add more capability through browser libraries. This brought new applications like Web Bluetooth, Web USB and so on; ever expanding the things we can use JavaScript for.

A bit of history

At one point we realised that JavaScript would be a great language for writing highly scalable server applications due to its event driven nature. Node.js was born. A new minimal standard library, composed with some essential features needed for writing non-browser applications, such as filesystem bindings, a TCP stack, a module loader, and much more. It is hard to estimate exactly what the use cases of the future are, so to try and make the platform more flexible, the ability to write modules in C/C++ was added. This allows developers to take full advantage of any APIs available on their platform, but still expose it as a JavaScript API for users to consume.

Lots of great modules were written this way. LevelDB , an embedded and fast database was written as a native module that glued the LevelDB C++ code with an easy-to-use JavaScript API. LevelDB sparked an ecosystem where a lot of interesting modules and applications were developed on top. Few LevelDB users know how the C++ in the module works, but luckily we don't have to - the native module abstracts all that away.

As more people started using native modules, we also learned some of the drawback. It turns out they can be hard to maintain, as the V8 APIs you use to implement the modules change a lot. For users to use a module they needed a full compiler stack installed on their machine (on Windows this used to involve users having to install Visual Studio!).

Along came NAN

Trying to solve the problem of the ever-changing V8 APIs, NAN was born. NAN stands for "Native Abstractions for Node.js" and is a series of macros that abstract away the differences between the changing V8 APIs. In fact, NAN was originally created by Rod Vagg to help with LevelDB development. This meant you could write a native module using the latest version of Node.js and have it work on most of the previous versions without too much complexity. It also meant that on most newer Node.js versions your old modules would continue to compile. This was a massive improvement in terms of being able to maintain native modules.

text
// silly NAN backed module that prints a string from c++</p><p>#include 
#include </p><p>using namespace v8;</p><p>NAN_METHOD(Print) {
  if (!info[0]->IsString()) return Nan::ThrowError("Must pass a string");
  Nan::Utf8String path(info[0]);
  printf("Printed from C++: %s\n", *path);
}</p><p>NAN_MODULE_INIT(InitAll) {
  Nan::Set(target,
    Nan::New("print").ToLocalChecked(),
    Nan::GetFunction(Nan::New(Print)).ToLocalChecked()
  );
}</p><p>NODE_MODULE(a_native_module, InitAll)

Prebuilds

To avoid having to install a compile toolchain, a series of experiments were performed around prebuilding modules before publishing them. The prebuilt binary would be hosted online on a place like Github releases and downloaded by an npm install script when the module is installed. If no available prebuild is available the npm install script would fall back to compiling the module as usual.

You can see an example of this in the leveldown repository .

Still a lot of issues continued to pop up. Once in a while NAN would have to make a backwards incompatible change. This meant old modules wouldn't compile on newer versions of Node.js without upgrading them. The introduction of new Node.js compatible runtimes such as Electron, made the situation even more complex. Modules compiled using Node.js would not run on Electron, leaving the users with obscure errors.

Downloading prebuilds turned out to be difficult with network proxies and security concerns about downloading binaries from 3rd party sources. Ironically, prebuilds made Electron tricky to use as well. The prebuild downloaded targeted the Node.js version that npm install was running on instead of the Electron one. In addition, the hard requirement for an npm install script can be a security issue also, as users disable these to avoid running an npm worm .

The Present

NAN and prebuilds make things better; but still not quite as good as we want. Native modules are still considered "expensive" dependencies, and are often used as an argument to include something in Node.js core VS an NPM module.

Luckily things are rapidly improving and we are already at the stage now where we have the tools to write and publish native modules that users can install with little or no technical overhead.

N-api

To make native modules easier to write and maintain, Node.js core contributors have been developing a new core API called n-api (or node-api).

The idea behind n-api is to build a stable interface on top of the V8 APIs that you write your native modules against. This approach introduces a series of benefits:

  1. Remove the need to recompile modules, as the interface never breaks (think of it as syscalls but for Node.js).
  2. Allows JavaScript engines other than V8 to implement n-api.
  3. Uses native modules across Electron and other runtimes as long as they implement n-api.

N-api has a tiny performance impact as you have to go through a slim abstraction layer instead of raw V8 code. However, it has great documentation .

Having been an experimental feature for a while, n-api recently landed as a non-experimental API. It is due to be released as such in Node.js 10 with backports coming to Node.js 8 and 6 (although as experimental in 6 for now).

Here is our NAN example from above ported to n-api:

text
// silly n-api backed module that prints a string from c
#include 
#include </p><p>napi_value print (napi_env env, napi_callback_info info) {
  napi_value argv[1];
  size_t argc = 1;</p><p>  napi_get_cb_info(env, info, &argc, argv, NULL, NULL);</p><p>  if (argc < 1) {
    napi_throw_error(env, "EINVAL", "Too few arguments");
    return NULL;
  }</p><p>  char str[1024];
  size_t str_len;</p><p>  if (napi_get_value_string_utf8(env, argv[0], (char *) &str, 1024, &str_len) != napi_ok) {
    napi_throw_error(env, "EINVAL", "Expected string");
    return NULL;
  }</p><p>  printf("Printed from C: %s\n", str);</p><p>  return NULL;
}</p><p>napi_value init_all (napi_env env, napi_value exports) {
  napi_value print_fn;
  napi_create_function(env, NULL, 0, print, NULL, &print_fn);
  napi_set_named_property(env, exports, "print", print_fn);
  return exports;
}</p><p>NAPI_MODULE(NODE_GYP_MODULE_NAME, init_all)

( See the full n-api example repo here and checkout the examples in Node.js core )

In addition to the C API, a higher level C++ wrapper is available as well called node-addon-api . The C++ wrapper is an npm module maintained by the n-api collaborators. Using the C++ wrapper you get support back until Node.js 4 as it has a compatibility layer for older versions.

In general I use the C API when wrapping C interfaces and the C++ one when wrapping a C++ API.

Bundled prebuilds

With n-api supporting prebuilds become a lot easier. Since n-api has a stable API we can prebuild a module for Node.js 10 and it will work on Node.js 11, 12, and newer.

To work around the issues of having to download prebuilds at installation, myself and some other contributors recently published a set of modules called prebuildify , node-gyp-build and prebuildify-ci .

  1. prebuildify will prebuild your module
  2. node-gyp-build can test a prebuild at install time and supports loading a prebuild from disk using a JavaScript api.
  3. prebuildify-ci helps you setup prebuildify on ci to automatically build for Linux 32/64 bit, MacOS, and Windows 32/64 bit

Other prebuild modules exist on npm. So how do they differ from prebuildify? With prebuildify instead of downloading a prebuild for your platform at installation time, we simply bundle all prebuilds for all platforms inside a ./prebuilds folder in the node_modules before it is published to npm. At installation then we use node-gyp-build to simply test if any of the prebuilds bundled in the module can load on the platform. If not we call out the compiler toolchain as npm normally does.

If you disable the installation script for security reasons the prebuilds still load on runtime. The installation script is just to test if it works.

When we first tried out this approach, we, the prebuildify collaborators, were worried that the footprint of adding multiple prebuilds inside the node_modules folder would make it slower to install due to the bigger package size. Ironically, it has actually made all the modules we ported to prebuildify faster to install. It usually takes longer to download all the dependencies needed to download a specific prebuild than it does to simply download them all in one go, gzipped, along with the rest of the module.

Combining prebuildify with n-api is close to being a perfect fit. N-api means you need to do very few prebuilds - one for each platform you need to support VS one for each platform x Node.js versions. You don't need to publish a new module when a new Node.js release is issued.

You can see a full example of how to use prebuildify with n-api and ci in the n-api example repo Building the prebuilds on platforms other than the one you are developing on can be a bit tedious. That's why we created prebuildify-ci; it sets up travis and appveyor to build your module when you tag a new release.

  1. First setup your module. Running prebuildify-ci init will setup a appveyor.yml and travis.yml file that prebuilds your module when it is tagged. After a succesful build, it will upload the prebuilds temporarily to Github releases so we can download from there before releasing the module. prebuildify-ci init
  2. git push a tagged release (and do not release it on npm yet). git commit -am "new cool stuff" npm version minor git push && git push --tags
  3. Wait for ci to finish.
  4. Download the releases from Github. prebuildify-ci download This should download and extract the prebuilds to ./prebuilds.
  5. Simply publish your module to npm with the prebuilds. npm publish

It is as easy as that.

The Future for Native Modules

The future is bright for native modules. In a years time n-api will be supported in all active Node.js release lines.

Native modules can be incredibly powerful and enable us to modularize Node.js core even more as it allows things like an alternative tcp stack , efficient bloomfilters and a modern crypto library to be built outside core without forcing users to compile them on installation. This will help us continue to bring great things like LevelDB and another non JavaScript projects into the Node.js ecosystem.

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.

Contact