Codementor Events

Primer into NodeJS Native Modules

Published Jun 23, 2018Last updated Jun 25, 2018

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 (here and here). A
lot of
abstractions were built (here and here). 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.

Discover and read more posts from Andrei Glingeanu
get started
post commentsBe the first to share your opinion
Show more replies