Primer into NodeJS Native Modules

June 29, 2018 0 Comments

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 (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.


Tag cloud