Skip to main content
Home

Primer into NodeJS Native Modules

Note: This article does not cover anything concerning WASM standard. Here is discussed only the old-fashioned C++ API for building Node.js modules.

A lot was said on the internets about the subject of writing modules in C++ for Node.js. A lot of abstractions were built. The most of the them won’t be able to beat the robustness and conciseness of the official docs but I’m going to take it a bit slower and just document the way I arrived at a very basic level of understanding of the subject. This exploration is, of course, just a very basic starting point with a lot of details left out to be figured as we move forward.

Lifecycle of a node module #

First of all, we need to understand the fact that in order to be able to use C++ code from JavaScript, we need to get the C++ code compiled into a special binary file. These files end with .node extension and they contain a low-level representation of a Node.js module. Node’s require() function knows how to treat them properly and a properly compiled C++ module just works out of the box.

That’s how a manual require looks:

const nativeModule = require('./build/Release/native');

In this case, the module is named native.node and very often they’re located in the build/Release folder relative to the project root folder. More on that folder structure later.

Running the Hello world #

We’ll start with the obligatory hello world.

You’ll need a C++ toolkit already installed on your system (g++ on Unix-like systems and Visual Studio on Windows). More details can be read on node-gyp’s README file).

mkdir native-modules
cd native-modules
touch binding.gyp
touch package.json
touch main.js
# Here we're going to put C++ source code
touch main.cpp

Fill package.json:

{
	"name": "node-native-modules-hello-world",
	"version": "0.0.1",
	"main": "index.js",
	"license": "MIT",
	"gypfile": true,
	"scripts": {
		"install": "node-gyp rebuild",
		"start": "node index.js"
	}
}

Putting node-gyp rebuild command in the install script will make sure your native modules will get compiled every time you run npm install, this is actually called a hook and here’s more of them. Don’t worry about node-gyp binary, it is pre-installed nowadays alongside Node on every system.

This node-gyp binary is actually where all the convenience lives. It is a very smart utility that knows how to generate build systems on a cross-platform basis, depending on where it is being run. That’s actually where its name comes from: GYP for Generate Your Projects and it has its roots from the GYP project of the Chromium team. It knows how to generate a Visual Studio project on Windows and a make-based process on Unix, but we’re getting into details here and I really want to keep everything simple.

The next important bit is the gypfile: true flag in our package.json file. It indicates that node-gyp should take the binding.gyp file, which we already created, into consideration. Here’s what we are going to fill this file with:

{
	"targets": [
		{
			"target_name": "native",
			"sources": [ "main.cpp" ]
		}
	]
}

Here we indicate that we intend to generate a native.node module and it should be the result of compiling main.cpp.

Here’s what will suffice for our example on the C++ side of things (put that in main.cpp):

#include <node.h>

void HelloWorld(const v8::FunctionCallbackInfo<v8::Value>& args)
{
	v8::Isolate* isolate = args.GetIsolate();
	auto message =
		v8::String::NewFromUtf8(isolate, "Hello from the native side!");
	args.GetReturnValue().Set(message);
}

void Initialize(v8::Local<v8::Object> exports)
{
	NODE_SET_METHOD(exports, "helloWorld", HelloWorld);
}

NODE_MODULE(module_name, Initialize)

This will look very familiar if you have any level of proficiency with C++. Here we define a function named HelloWorld that just returns a string. Next, we declare the helloWorld property onto the exports object to have the value HelloWorld. This effectively results in a module that exports a function, which returns a basic string. That’s the equivalent JS code:

function HelloWorld() {
  return 'Hello from the native side!';
}

module.exports.helloWorld = HelloWorld;

Now we have the job of compiling this bit of code into a native.node file.

npm install
ls build/Release

You can see that it generated a ./build/Release/native.node file, which is a module waiting us to require and use it!

Now, we’ll go ahead and use this module (put that in main.js):

let native = require('./build/Release/native.node');

console.log(native.helloWorld());

Because the native.node module is already compile, we can safely run main.js file and watch it run:

node main.js
Hello from the native side!

The require(...) part looks a bit ugly but we can very easily fix it with the help of a very small npm module called bindings.

npm install bindings

And use the module right away. Here’s the resulting main.js file:

let native = require('bindings')('native');
console.log(native.helloWorld());

A lot simpler and no need to manually trace the path to the native.node file! bindings will do the heavy lifting for us.

An little more complex example #

Next, we’re going to perform a computation that’s a bit more involved on the C++ side of things, just to prove we’re heading into the right direction.

We are going to create a function in C++ which takes a variable amount of arguments and print them using the famous printf() function. The trick is to pass only numbers from JavaScript and output each number in as much base systems as possible. We’re going to handle as 2, 6, 7, 8 and 16 bases. That will be enough for us to get dangerous enough.

The folder structure we are going to use:

├── binding.gyp
├── build
│   ├── Makefile
│   ├── Release
│   ├── binding.Makefile
│   ├── config.gypi
│   ├── gyp-mac-tool
│   └── native.target.mk
├── main.cpp # C++ code for actually outputting formatted strings
├── main.js # the JS source code for running the program
└── package.json

4 directories, 11 files

Here's the actual C++ code that implements the logic for converting numbers:

void NativePrintf(const v8::FunctionCallbackInfo<v8::Value>& args)
{
    int number = (int) args[0]->NumberValue();
    std::cout << "Base 10: ";
    convertDecimalToOtherBase(number, 10);
    std::cout << std::endl;
    std::cout << "Base 2: ";
    convertDecimalToOtherBase(number, 2);
    std::cout << std::endl;
    std::cout << "Base 6: ";
    convertDecimalToOtherBase(number, 6);
    std::cout << std::endl;
    std::cout << "Base 7: ";
    convertDecimalToOtherBase(number, 7);
    std::cout << std::endl;
    std::cout << "Base 8: ";
    convertDecimalToOtherBase(number, 8);
    std::cout << std::endl;
    std::cout << "Base 16: ";
    convertDecimalToOtherBase(number, 16);
    std::cout << std::endl;
    std::cout << "-------------";
    std::cout << std::endl;
}

The function convertDecimalToOtherBase() is ommited for brevity.

The full source code for the example can be found on the GitHub repository.

As you can see, with a little bit of help from C++, you can achieve pretty complex stuff very easily. You can implement complex apps that launch pipes or FIFOs and embed them in its entirety into your existing Node app, or you can use popular networking libraries for C++ into your small Node program. The imagination is the limit.

Profile picture

Andrei Glingeanu's notes and thoughts. You should follow him on Twitter, Instagram or contact via email. The stuff he loves to read can be found here on this site or on goodreads. Wanna vent or buy me a coffee?