NAN to Node-API Migration: A Short Story - NodeSource

The NodeSource Blog

You have reached the beginning of time!

NAN to Node-API Migration: A Short Story

Throughout the years I’ve created my fair share of native addons. The first ones were created by using the native C++ Node.js and v8 API’s. When NAN was created which made life much easier, especially in terms of maintenance, there was no question that I should move all my addons to use it.

The years passed and Node-API was created and though it was on my radar and saw the benefits of using it, I never had the time to try it on my own modules. So when thinking on a topic for a blog, it struck me that writing about the migration of a couple of my addons from using NAN to Node-API could be interesting.

Background

Back in the old Node.js days the only way to implement a Node.js native addon was by using v8, Node.js and libuv libraries. For example, looking at part of the code from on of my oldest native addons which worked for the v0.8.x Node.js versions:

#include "node.h"
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>

v8::Persistent<v8::String> errno_symbol;

v8::Handle<v8::Value> Bind(const v8::Arguments& args) {
   HandleScope scope;
   sockaddr_un sun;
   int fd;
   int ret;

   assert(args.Length() == 2);

   fd = args[0]->Int32Value();
   v8::String::Utf8Value path(args[1]);

   strncpy(sun.sun_path, *path, sizeof(sun.sun_path) - 1);
   sun.sun_path[sizeof(sun.sun_path) - 1] = '\0';
   sun.sun_family = AF_UNIX;

   if ((ret = bind(fd, reinterpret_cast<sockaddr*>(&sun), sizeof(sun))) == -1) {
       SetErrno(errno);
   }

   return scope.Close(v8::Integer::New(ret));
}

void Initialize(v8::Handle<v8::Object> target) {
   errno_symbol = v8::Persistent<v8::String>::New(v8::String::NewSymbol("errno"));
   target->Set(v8::String::NewSymbol("AF_UNIX"), v8::Integer::New(AF_UNIX));
   target->Set(v8::String::NewSymbol("SOCK_STREAM"), v8::Integer::New(SOCK_STREAM));
   target->Set(v8::String::NewSymbol("bind"), v8::FunctionTemplate::New(Bind)->GetFunction());
}

NODE_MODULE(unix_stream, Initialize)

In this code snippet, a bind() method is exposed to JS as well as a couple of constants AF_UNIX and SOCK_STREAM. As it can be seen, only v8 and Node.js libraries are used.

The main issue that this approach had was that the v8 and Node.js exposed API’s might (and most of the time they did) change across versions. This caused that, in order for the native addon to be usable in different Node.js versions we had to do things like this:

#if NODE_VERSION_AT_LEAST(0, 12, 0)
Handle<Value> Bind(const v8::FunctionCallbackInfo<v8::Value>& args) {
   HandleScope scope(v8::Isolate::GetCurrent());
#else
Handle<Value> Bind(const v8::Arguments& args) {
   HandleScope scope;
#endif
   sockaddr_un sun;
   int fd;
   int ret;

   assert(args.Length() == 2);

   fd = args[0]->Int32Value();
   v8::String::Utf8Value path(args[1]);

   strncpy(sun.sun_path, *path, sizeof(sun.sun_path) - 1);
   sun.sun_path[sizeof(sun.sun_path) - 1] = '\0';
   sun.sun_family = AF_UNIX;

   if ((ret = bind(fd, reinterpret_cast<sockaddr*>(&sun), sizeof(sun))) == -1) {
       SetErrno(errno);
   }
#if NODE_VERSION_AT_LEAST(0, 12, 0)
   args.GetReturnValue().Set(ret);
#else
   return scope.Close(v8::Integer::New(ret));
#endif
}

This way the code would work on any version from 0.8.x to 0.12.x but it’s kind of ugly and what’s more important, it can quickly become a huge burden to maintain the more versions you want your addon to support.

In order to solve this specific issue, Native Abstractions for Node.js (NAN) was created. From their documentation:

Thanks to the crazy changes in V8 (and some in Node core), keeping native addons compiling happily across versions, particularly 0.10 to 0.12 to 4.0, is a minor nightmare. The goal of this project is to store all logic necessary to develop native Node.js addons without having to inspect NODE_MODULE_VERSION and get yourself into a macro-tangle.

In other words, NAN provides a common interface to access the v8 and Node.js functionality that their API’s provide across the different Node.js versions.

Next I’m showing the exact same Bind() function implemented using NAN@2

NAN_METHOD(Bind) {
   Nan::HandleScope scope;

   sockaddr_un sun;
   int fd;
   int ret;

   assert(info.Length() == 2);

   fd = info[0]->Int32Value();
   String::Utf8Value path(info[1]);

   memset(&sun, 0, sizeof(sun));
   strncpy(sun.sun_path, *path, sizeof(sun.sun_path) - 1);
   sun.sun_family = AF_UNIX;

   if ((ret = bind(fd, reinterpret_cast<sockaddr*>(&sun), sizeof(sun))) == -1) {
       ret = -errno;
   }

   info.GetReturnValue().Set(ret);
}

Which is so much nicer and makes it compatible with every nodejs version starting from 0.10.x.

So far so good. NAN helps A LOT to the burden of native addons creation and maintenance but it also comes with its own set of drawbacks:

  • The add-on needs to be rebuilt for every NODE_MODULE_VERSION, so the binary distribution becomes cumbersome.
  • It’s dependent on the V8 engine, so in the event a different JS engine were to be used it wouldn’t work.

Hello Node-API

Node-API was added to Node.js 8.0.0 as experimental with its main objective being to provide an API that allowed to develop native addons that are independent of the underlying JS engine used by Node.js (At the time it made a lot of sense as there was active development efforts to add support for the Chakracore JS engine). Also, this API is ABI across Node.js versions, meaning that a native addon built on a specific major version would run correctly in subsequent major versions without recompilation.

It is a C API that’s maintained in the nodejs source tree though in order to make it easier to use, node-addon-api, a C++ API built on top of it, is also provided.

So as stated before, we’re going to proceed with the migration of two of my native addons from using NAN and to use node-addon-api.

I’m going to describe the process highlighting what seemed more interesting. Also, I’d like to point out that there’s a very handy conversion.js script that will help a lot by automating the conversion for you, though I haven’t used it for the addons I’m presenting here.

node-ioctl

This is a simple wrapper over the ioctl() syscall so it looked like a great candidate for a first NAN to Node-Addon-API migration.

The first step would be setting up the node-addon-api to be used by our addon:

  1. Install node-addon-api as a dependency, replacing NAN.

NAN → node-addon-api Screen Shot 2021-08-19 at 8.43.18 AM

  1. Then modify the binding.gyp file to be able to actually use node-addon-api. The needed changes being:

    • Configure the location of the napi.h-
    • As our c++ addon code doesn’t throw exceptions, disable it by defining NAPI_DISABLE_CPP_EXCEPTIONS
    • Finally, as this addon is supported on OS X, define the corresponding conditions.

NAN

{
   'targets': [
       {
           'target_name': 'ioctl',
           'sources': [ 'src/ioctl.cpp' ],
           'include_dirs': [
               '<!(node -e "require(\'nan\')")'
           ]
       }
   ]
}

node-addon-api

{
   'targets': [
       {
           'target_name': 'ioctl',
           'sources': [ 'src/ioctl.cpp' ],
           'include_dirs': [
               '<!(node -p "require(\'node-addon-api\').include_dir")'
           ],
           'defines': [ 'NAPI_DISABLE_CPP_EXCEPTIONS',
                        'NODE_ADDON_API_ENABLE_MAYBE' ],
           'conditions': [
               ['OS=="mac"', {
                   'cflags+': ['-fvisibility=hidden'],
                   'xcode_settings': {
                       'GCC_SYMBOLS_PRIVATE_EXTERN': 'YES', # -fvisibility=hidden
                   }
               }]
           ]
       }
   ]
}

And now for the actual code of the addon. The addon is actually quite simple as it just exports one ioctl() method.

We will focus first on the headers to be included. As already said before, Node-API is independent of the underlying v8 so we can’t directly use any of the v8 functions. The same goes for the Node.js public api’s which shouldn’t be used directly in order to keep the binary compatibility. All of this means not to include either v8.h nor node.h but just napi.h.

NAN → node-addon-api Screen Shot 2021-08-23 at 9.08.41 AM

Looking now at the addon initialization, the modifications are quite straightforward and hopefully self-explanatory: it just exports an ioctl method implemented in the Ioctl function.

NAN

void InitAll(Local<Object> exports) {
   Nan::Set(exports,
            Nan::New("ioctl").ToLocalChecked(),
            Nan::GetFunction(Nan::New<FunctionTemplate>(Ioctl)).ToLocalChecked());
}

NODE_MODULE(ioctl, InitAll)

node-addon-api

Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
   exports.Set(Napi::String::New(env, "ioctl"),
               Napi::Function::New(env, Ioctl));
   return exports;
}

NODE_API_MODULE(ioctl, InitAll)

This code, though, serves us well to introduce some of the basic node-addon-api classes.

  • Napi::Env which is an opaque structure that contains the environment in which the current code is run (the actual Node.js runtime). This structure is passed to native functions when they're invoked, and it must be passed back when making Node-API calls.
  • Napi::Object Napi::String Napi::Function are the C++ representation of the underlying JS primitives (Napi::Function inherits from Napi::Object) and they all inherit from Napi::Value base class.

And finally the actual Ioctl method implementation. The summary of what it actual does is

  1. Validating and extracting the arguments (2 or 3 arguments are supported)
  2. Perform the ioctl() syscall with those arguments.
  3. Return the integer returned by the syscall.

NAN

NAN_METHOD(Ioctl) {
   Nan::HandleScope scope;

   Local<Object> buf;
   int length = info.Length();

   assert((length == 2) || (length == 3));

   void* argp = NULL;

   if (!info[0]->IsUint32()) {
       Nan::ThrowTypeError("Argument 0 Must be an Integer");
   }

   if (!info[1]->IsUint32()) {
       Nan::ThrowTypeError("Argument 1 Must be an Integer");
   }

   if ((length == 3) && !info[2]->IsUndefined()) {
       if (info[2]->IsInt32()) {
           argp = reinterpret_cast<void*>(Nan::To<int32_t>(info[2]).ToChecked());
       } else if (info[2]->IsObject()) {
           buf = Nan::To<Object>(info[2]).ToLocalChecked();
           if (!Buffer::HasInstance(buf)) {
               Nan::ThrowTypeError("Argument 2 Must be an Integer or a Buffer");
           }

           argp = Buffer::Data(buf);
       }
   }

   int fd = Nan::To<int32_t>(info[0]).ToChecked();
   unsigned long request = Nan::To<uint32_t>(info[1]).ToChecked();

   int res = ioctl(fd, request, argp);
   if (res < 0) {
       return Nan::ThrowError(Nan::ErrnoException(errno, "ioctl", nullptr, nullptr));
   }

   info.GetReturnValue().Set(res);
}

node-addon-api

Napi::Number Ioctl(const Napi::CallbackInfo& info) {
 void* argp = NULL;
 Napi::Env env = info.Env();

 size_t length = info.Length();
 if ((length != 2) && (length != 3)) {
   Napi::Error::New(env, "ioctl only accepts 2 or 3 arguments").
       ThrowAsJavaScriptException();
   return Number();
 }

 if (!isInteger(info[0])) {
   Napi::Error::New(env, "Argument 0 Must be an Integer").
       ThrowAsJavaScriptException();
   return Number();
 }

 if (!isInteger(info[1])) {
   Napi::Error::New(env, "Argument 1 Must be an Integer").
       ThrowAsJavaScriptException();
   return Number();
 }

 if ((length == 3) && !info[2].IsUndefined()) {
   if (isInteger(info[2])) {
     argp = reinterpret_cast<void*>(info[2].ToNumber().Int32Value());
   } else if (info[2].IsBuffer()) {
     argp = info[2].As<Napi::Buffer<unsigned char>>().Data();
   } else {
     Napi::Error::New(env, "Argument 2 Must be an Integer or a Buffer").
       ThrowAsJavaScriptException();
     return Number();
   }
 }

 int fd = info[0].ToNumber().Int32Value();
 unsigned long request =
     static_cast<unsigned long>(info[1].ToNumber().DoubleValue());

 int res = ioctl(fd, request, argp);
 if (res < 0) {
   Napi::Error e = Napi::Error::New(env, "ioctl");
   e.Set("code", Napi::Number::New(env, errno));
   e.ThrowAsJavaScriptException();
   return Number();
 }

 return Napi::Number::New(env, res);
}

Some important things to highlight here:

  • Napi::Number Ioctl(const Napi::CallbackInfo& info) defines the callback method that is called when calling the ioctl() method from JS. It returns a JS number Napi::Number while the Napi::CallbackInfo contains the arguments passed to the method which can be accessed via the [] operator.
  • When accessing the arguments, which are Napi::Value, we can use specific methods to check their JS type and convert them into that specific JS type. Once that conversion is done we can extract the value it represents. As an example for an Napi::Value that represents an int32_t Napi::Number, we would do:
 Napi::Value val;
 if (val.isNumber()) {
   Napi::Number numb = val.As<Napi::Number>();
   int32_t integer = numb.Int32Value();
 }

Notice also the use of the Napi::Env for every call that creates a new Javascript value such when creating a Napi::Error or a Napi::Number

Napi::Error::New(env, "ioctl");
Napi::Number::New(env, res);

node-pcsclite

It's a wrapper over the libpcsclite library which allows operating on SmartCards.

This one is a bit of a more complex add-on, and for this very same reason I won’t go into as much detail as I did with the previous pme and just focus on a specific case that doesn't appear in node-ioctl.

Just establish that the addon defines two main C++ classes PCSCLite and CardReader. They are initialized in the following way for the NAN version and the new node-addon-api version

NAN

void init_all(v8::Local<v8::Object> target) {
   PCSCLite::init(target);
   CardReader::init(target);
}

NODE_MODULE(pcsclite, init_all)

node-addon-api

Napi::Object init_all(Napi::Env env, Napi::Object target) {
 PCSCLite::init(env, target);
 CardReader::init(env, target);
 return target;
}

These classes are bound to the lifetime of a JS object by wrapping them into an ObjectWrap. For the NAN version, this means that these classes need to inherit from Nan::ObjectWrap whereas for node-addon-api they’ll need to inherit from Napi::ObjectWrap

NAN

class PCSCLite: public Nan::ObjectWrap {
    public:
        static void init(v8::Local<v8::Object> target);
    private:
       PCSCLite();
       ~PCSCLite();
       static Nan::Persistent<v8::Function> constructor;
       static NAN_METHOD(New);
};

node-addon-api

class PCSCLite : public Napi::ObjectWrap<PCSCLite> {
    public:
        static void init(Napi::Env env, Napi::Object target);
        PCSCLite(const Napi::CallbackInfo& info);
        ~PCSCLite();
}

And here’s the actual implementation of how the ObjectWrap are setup for both the NAN and the new node-addon-api versions

NAN

Nan::Persistent<v8::Function> PCSCLite::constructor;

void PCSCLite::init(Local<Object> target) {
   // Prepare constructor template
   Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(New);
   tpl->SetClassName(Nan::New("PCSCLite").ToLocalChecked());
   tpl->InstanceTemplate()->SetInternalFieldCount(1);
   // Define Prototype Methods
   Nan::SetPrototypeTemplate(tpl, "start", Nan::New<FunctionTemplate>(Start));
   Nan::SetPrototypeTemplate(tpl, "close", Nan::New<FunctionTemplate>(Close));

   Local<Function> newfunc = Nan::GetFunction(tpl).ToLocalChecked();
   constructor.Reset(newfunc);
   Nan::Set(target, Nan::New("PCSCLite").ToLocalChecked(), newfunc);
}

NAN_METHOD(PCSCLite::New) {
   Nan::HandleScope scope;
   PCSCLite* obj = new PCSCLite();
   obj->Wrap(info.Holder());
   info.GetReturnValue().Set(info.Holder());
}

node-addon-api

void PCSCLite::init(Napi::Env env, Napi::Object exports) {
    Napi::Function func =
        DefineClass(env,
                    "PCSCLite",
                    {
                       InstanceMethod("start", &PCSCLite::Start),
                       InstanceMethod("close", &PCSCLite::Close)
                    });

    Napi::FunctionReference* constructor = new          Napi::FunctionReference();
    *constructor = Napi::Persistent(func);
    env.SetInstanceData(constructor);

    exports.Set("PCSCLite", func);
}

Comparing both we can see that the NAN version is very similar as you’d do it using the v8 and Node.js libraries directly whereas on the node-addon-api case the code is much more concise and simpler thanks to the Napi::ObjectWrap<T> base class and the DefineClass static method, which allow to define a Javascript class with its methods and properties in only one call. Also it’s important to call attention to the fact that there’s no need to define a specific PCSCLite::New method to be called when the new PCSCLite() JS code is executed, but the Napi::ObjectWrap<T> base class handles all this for you.

The whole set of code changes that were necessary to perform the migration of both addons can be found here and here.

Conclusions

Some final thoughts after spending a couple of days on the migration of the code.

  • It was much easier than I had expected thanks to the API documentation, the extensive list of examples available and the Node-API Resource webpage whose content is top-notch.
  • The API is generally quite easy to use and understand and usually leads to cleaner and more concise code.
  • Having binary compatibility across Node.js version is amazing.
  • If I were to create new addons, Node-API would be my choice over NAN, unless I were to use some specific v8 methods not covered by it.

Need a helping hand?

If you have any questions, please feel free to contact us at info@nodesource.com or in this form.

To get the best out of Node.js, start a free trial of N|Solid, an augmented version of the Node.js runtime, enhanced to deliver low-impact performance insights and greater security for mission-critical Node.js applications. #KnowyourNode

The NodeSource platform offers a high-definition view of the performance, security and behavior of Node.js applications and functions.

Start for Free