Playing with a native C extension for Node.js

I’ve always felt like a good way to fully understand a language is to play with the native interface it exposes. I’ve already done a post playing with a native C Ruby extension, and now that I’m using Node.js more I felt like I should dig into it. Here is how I ended up playing with a native C extension for Node.js. I did not have a real goal while playing around, so I did the exact same thing I did for Ruby. As always, the code is hosted on my GitHub repository!

Why a native extension

Node.js is a great tool for event-based concurrency. Having a garbage collector and being single-threaded makes for a language that is easy to get into and fast. But like any language, it is not perfect.

For example, Node.js is not the best if you need to do some CPU-bound computations. Even using threads won’t help since only one Node.js thread will run at a time. There is also the case of wanting to interface with existing native libraries or with the OS.

For all these reasons, and many others, a native extension could be useful.

Needed setup

There are multiple tooling frameworks that can be used to do a native library for Node.js. In the end, most end up wrapping around N-API, the Node.js native API. I decided to use node-gyp.

The initial repository setup was quite trivial:

  1. Create (and fill) the binding.gyp file
  2. Run npm init

The binding.gyp file is used to configure how node-gyp will build the native part of the extension. In my case, I went with something simple:

Once you have the file in your directory, npm init will recognize it and do the rest of the setup.

Loading the native interface can be tricky, luckily there is a package for that! The library is called node-bindings. Using this library, you can load your library with one simple line: require('bindings')('nci.node').

This will find your native bundle (in my case nci.node) and just load it. No need to think about release vs debug, about platforms, or anything else.

Module management

Exposing your module is done through the  NAPI_MODULE macro. The macro requires a function as a second argument, this function will be called to initialize the module. The initialization method needs to return everything that is exported from the module. For example, the following would register a new module with the method Init as initializer: NAPI_MODULE(NODE_GYP_MODULE_NAME, Init).

In my case, I add the newly created class definition to the existing exports and return that. I also take this time to store a few global variables that I’ll need to reuse in various calls.

Creating the class definition is simple enough: register the class by giving it a name and a constructor callback and then export it. For example, I went with the following:

Class/instance management

There are two Node.js syntaxes that can get your constructor called:

I decided to support only the first version simply because it is a bit easier to do so. In order to distinguish the two versions, you’ll need to check the target of the call. Basically, if the target is set, you are in the  new MyClass()  version. Doing so is pretty easy:

Once in the constructor, I get and validate the first argument, expecting an int. Once this is done, I simply create the native instance and wrap it like so:

When wrapping the instance is the moment where you define a cleanup callback. That callback will be called when the object is garbage collected.

Once the object is wrapped, you’ll need to register all functions on the instance. In order to do so, simply repeatedly call napi_create_function and napi_set_named_property to create and add the function to the object.

Accessing the native object

In the function callbacks, you’ll need to access your native object. In order to do so, you’ll need to get the call information and then unwrap the target object (this) like so:

Loading the module

If you are using node-bindings like me, loading the library is done simply through:  require('bindings')('nci.node'). It is a good idea to wrap that into your own module and not require your customer to do this for you.

Related links