How to create a native add-on using C++
In this tutorial we’re going to go through the basics on how to write a native add-on to NodeJS using C++, one of the platform’s most powerful capabilities of which most web/JS developers now a days are not even familiar with.
For somewhat there is a vacuum which got created around C++ in the recent years, people are so scared from a low-level back-end programming, why should they even deal with memory allocation dilemmas when there are all these interpretation languages like Python, Ruby, and JavaScript who can do that for us? Well folks, once you will realize that all these single web-page application third-party libraries and frameworks are all recycled and repetitive, and you will start doing some real shit by optimizing some heavy-ass machine algorithms, you will realize that performance is critic, a title which doesn’t really suit NodeJS, with all the love and respect. So why do I even bother making this tutorial? Because people are unaware of many features in the programming world, and the beauty of algorithmics. And why am I even approaching NodeJS developers? Because they have the biggest, most-popular community of all, they have a lot to learn, but they are very open-minded, and that’s the most important thing. You don’t have to be a C++ developer not at all, but if I can at least catch a small part of your attention I will be overwhelmed.
We will create a very small and simple module which calculates the distance between two dots, it can
do it either synchronously or asynchronously. The algorithm for calculating the distance between a
given pointA
and pointB
in a 2D
Cartesian coordinate system looks like
this:
√[(pointAX - pointBX)^2 + (pointAY - pointBY)^2]
Probably not too complicated, kicks me back to the glamorous days in high-school. If put in JavaScript it should take around 10 seconds right? Well in a native NodeJS add-on it should be a bit longer. Of-course when it comes to small logics you shouldn’t make too much effort, because it will consume more power only converting all JS objects into C++ native structures. But imagine you would like to run a blob-detection algorithm and you would like to calculate the center of mass of multiple blobs, in C++ it would be much faster, especially when using shaders. Anyway that’s not the point of this tutorial, the point is that you will be provided with the necessary tools so next time if you would like to write some heavy logic, you will know how to do it.
We will start with a brief introduction for Google’s v8 engine and some practical examples on how to use it, this will help us to get start, and then we will write our add-on. Let’s begin then shall we?
Introduction to V8
The v8 JavaScript Engine is an open source JavaScript engine developed by The Chromium Project for the Google Chrome web browser. It is intended to be used both in a browser and as a standalone high-performance engine that can be integrated into independent projects like Couchbase, MongoDB and Node.js. There lots of benchmarks out there but I will not bore you with diagrams, we all know that this engine has proven itself to be worthy, and it is being used world-widely and probably for a good reason.
“Ambition is a dream with a v8 engine” -Elvis Presley.
If you’re a JavaScript developer you must be familiar with the event-loop and the scoping system of v8, so it’s a good thing that you understand the concept, but you never got to actually look at its source code and explicitly use it. A detailed documentation for v8’s different API versions is available here. I assume that you don’t bother reading any of my references that I provide along this tutorial (As a lazy blog-reader myself), I will post here that first thing you’re going to see once you enter v8’s documentation web-site:
Throughout history v8 have changed a lot, and it wore many forms. As a result, add-ons are not
usable across different versions of the platform since each one supports a different API, which will
break our process during run-time. In NodeJS team they came up with a very convenient solution
called Nan. Nan stands for “Native Abstractions for NodeJS” and is
basically a header file filled with macro and utility goodness for making add-on development for
NodeJS easier across all versions of v8, without inspecting NODE_MODULE_VERSION
macro all the
time. In this tutorial I’m going to refer both of them as if they are bundled in a single framework.
Eventually JavaScript is just a rehash of v8, everything you know so far is still valid, but it uses a different idiom. To prevent some misconceptions, here are some important points regards JavaScript’s equivalents which I think you should follow:
Scopes and Variables
In v8 a scope is referred as Isolate
(v8::Isolate
) and a variable is referred as Local
(v8::Local
). A local is a pointer to an object. All v8 objects are accessed using locals, they are
necessary because of the way the v8 garbage collector works. An isolate can be thought of as a
container for any number of locals. When you’ve finished with your locals, instead of deleting each
one individually you can simply delete their scope.
JavaScript
let obj = {
foo: 'foo',
bar: 'bar',
baz: 'baz'
}
console.log(obj.foo)
console.log(obj.bar)
console.log(obj.baz)
C++
using Nan::New;
using std::cendl;
using std::cout;
using v8::Local;
using v8::Object;
using v8::String;
Local<Object> obj = New<Object>();
obj->Set(New<String>("foo").ToLocalChecked(), New<String>("foo").ToLocalChecked());
obj->Set(New<String>("bar").ToLocalChecked(), New<String>("bar").ToLocalChecked());
obj->Set(New<String>("baz").ToLocalChecked(), New<String>("baz").ToLocalChecked());
cout << obj->Get(New<String>("foo").ToLocalChecked()) << cendl;
cout << obj->Get(New<String>("bar").ToLocalChecked()) << cendl;
cout << obj->Get(New<String>("baz").ToLocalChecked()) << cendl;
Asynchronous Callbacks
Asynchronous logic can be implemented using the AsyncWorker
(Nan::AsyncWorker
) and invoked by
AsyncQueueWorker
(Nan::AsyncQueueWorker
). Thanks to these two you can have much of the annoying
asynchronous queuing and handling taken care of for you. It can even store arbitrary V8 objects for
you and have them persist while the asynchronous work is in progress.
JavaScript
setImmediate(() => {
callback(null, 'result')
})
C++
using Nan::AsyncQueueWorker;
using Nan::AsyncWorker;
using Nan::HandleScope;
using Nan::New;
using Nan::Null;
using std::string;
using v8::Local;
using v8::String;
class ResultWorker : AsyncWorker {
private:
string* result
public:
ResultWorker(Callback* callback) : AsyncWorker(callback) {}
~ResultDistance() {
delete result;
}
// Executed inside the worker-thread.
// It is not safe to access V8, or V8 data structures
// here, so everything we need for input and output
// should go on 'this'.
void Execute () {
result = new string("result");
}
// Executed when the async work is complete.
// This function will be run inside the main event loop
// so it is safe to use V8 again.
void HandleOKCallback () {
HandleScope scope;
Local<Value> argv[] = {
Null(),
New<String>(result).ToLocalChecked()
};
callback->Call(2, argv);
}
};
AsyncQueueWorker(new ResultWorker(callback));
Modules Registration and Methods Definition
v8 and Nan provide us with some handy macros (NODE_MODULE
, NAN_MODULE_INIT
, NAN_METHOD
and
NODE_SET_METHOD
) which will help us register a new NodeJS module and define its methods. This
might be confusing for some, since we can’t see the function’s signature it would appear as if
variables are just being magically created in the stack, but once the macros are being pre-processed
they will just turn into ordinary functions. In the example below I commented the original signature
so you can have more clew on what’s going on.
JavaScript
exports.fn = (a, b) => a + b
C++
using Nan::To;
using v8::Local;
using v8::Object;
// void Fn(FunctionCallbackInfo<Value>& info)
NAN_METHOD(Fn) {
double a = To<double>(info[0]).FromJust();
double b = To<double>(info[1]).FromJust();
info.GetReturnValue().Set(a + b);
}
// void Init(Local<Object> target)
NAN_MODULE_INIT(Init) {
NODE_SET_METHOD(target, Fn);
}
// First argument would be the entry file's name
NODE_MODULE(addon, Init);
As you can see when dealing with v8 explicitly is a time-consuming process which requires you to do lots of extra-work. With that said, keep in mind that only a small portion of your code is going to interact with the engine since the core logic should be written using native C++ and other third-party libraries. You always need to find the right balance. Always make sure that your add-on doesn’t require too much data to be passed otherwise the conversion process is gonna be hard and inefficient, and think twice before you choose this approach for the sake of simplicity. Overall the estimated optimization should be around 150% and up, depends on the task, first check your JavaScript code snippet, check for unnecessary logic and optimize it, and if you’re really sure that it is fully optimized, and you’re still striving for more performance, only then move to C++.
So far I went through the very basics which will help you create this bridge between the two platforms. The v8 lacks of detailed documentation, tutorials and examples. Nan however is a bit more documented IMHO, so when I approach the API documentation I would start from there, and if I didn’t find anything useful I would look at v8’s latest API docs. It’s not a hard material to learn but it’s different, so it might be a bit challenging for some, but remember, practice practice practice.
Speaking of practice, let’s move on to the next step where we going to implement our first add-on for distance calculation between two points.
Creating the Add-On
In this step we will base our development process on the TDD methodology, so you will have a chance to look at the final target API that we desire. We will start by writing a test file:
Add Test File
Added .npmignore
+┊ ┊1┊test.js
Added test.js
+┊ ┊ 1┊const Distance = require('.');
+┊ ┊ 2┊
+┊ ┊ 3┊let result;
+┊ ┊ 4┊let pointA = { x: 0, y: 0 };
+┊ ┊ 5┊let pointB = { x: 3, y: 4 };
+┊ ┊ 6┊
+┊ ┊ 7┊result = Distance.calculate.sync(pointA, pointB);
+┊ ┊ 8┊
+┊ ┊ 9┊if (result !== 5) throw Error(
+┊ ┊10┊ '#Sync: Result expected to equal 5 but instead got ' + result
+┊ ┊11┊);
+┊ ┊12┊
+┊ ┊13┊console.log('sync calculation passed');
+┊ ┊14┊
+┊ ┊15┊result = Distance.calculate.async(pointA, pointB, (err, result) => {
+┊ ┊16┊ if (err) throw err;
+┊ ┊17┊
+┊ ┊18┊ if (result !== 5) throw Error(
+┊ ┊19┊ '#Async: Result expected to equal 5 but instead got ' + result
+┊ ┊20┊ );
+┊ ┊21┊
+┊ ┊22┊ console.log('async calculation passed');
+┊ ┊23┊});🚫↵
And the following NPM script should execute it:
Add npm Test Script
Changed package.json
┊ 6┊ 6┊ "repository": {
┊ 7┊ 7┊ "type": "git",
┊ 8┊ 8┊ "url": "https://github.com/DAB0mB/node-distance-addon.git"
+┊ ┊ 9┊ },
+┊ ┊10┊ "scripts": {
+┊ ┊11┊ "test": "node test"
┊ 9┊12┊ }
┊10┊13┊}
Like I said in the introduction, it’s a simple module which can calculate the distance between 2
given points. calculate.sync
can do it synchronously and calculate.async
can do it
asynchronously. Now that you got the idea we will start configuring our add-on.
The first thing you’ll need to do is to make sure that you have node-gyp
installed:
sudo npm install -g node-gyp
node-gyp
is also dependent on many other packages, so before you go any further please take a look
at the official installation instructions in their
README.md file.
Assuming that you have installed everything properly, we will now need to create a binding.gyp
file:
Create binding.gyp
File
Added binding.gyp
+┊ ┊ 1┊{
+┊ ┊ 2┊ "targets": [
+┊ ┊ 3┊ {
+┊ ┊ 4┊ "target_name": "distance",
+┊ ┊ 5┊ "sources": [
+┊ ┊ 6┊ "src/distance.cc"
+┊ ┊ 7┊ ],
+┊ ┊ 8┊ "include_dirs": ["<!(node -e \"require('nan')\")"]
+┊ ┊ 9┊ }
+┊ ┊10┊ ]
+┊ ┊11┊}🚫↵
GYP stands for ‘Generate Your Project’ and was created by the Chromium team as a configuration file for building native projects. The configuration show above should be a good template for any future add-on you’re looking to develop. Let’s take a deeper look at it:
target_name
- Specifies the output dir of our add-on, in which case it should bebuild/Release/distance
sources
- Should include all the cpp files that are associated with you add-on.include_dirs
- Additional dirs that should be included when building the add-on. If you’ll run the given script in the terminal you’ll get the node-module path for Nan, a library which we’re interested in during the build process.
More information about GYP configuration can be found here.
Be sure to also add the specified flag to the package.json
which basically says ‘Hey, I have a GYP
file which should be taken into consideration as well’:
Create binding.gyp
File
Changed package.json
┊ 7┊ 7┊ "type": "git",
┊ 8┊ 8┊ "url": "https://github.com/DAB0mB/node-distance-addon.git"
┊ 9┊ 9┊ },
+┊ ┊10┊ "gypfile": true,
┊10┊11┊ "scripts": {
┊11┊12┊ "test": "node test"
┊12┊13┊ }
Now we will add the following NPM scripts so whenever we run npm run build
our project will be
built:
Add npm Build Scripts
Changed .gitignore
+┊ ┊1┊build
┊1┊2┊node_modules🚫↵
Changed package.json
┊ 9┊ 9┊ },
┊10┊10┊ "gypfile": true,
┊11┊11┊ "scripts": {
+┊ ┊12┊ "pre-publish": "npm run build",
+┊ ┊13┊ "build": "node-gyp rebuild",
+┊ ┊14┊ "test": "npm run build && node test"
┊13┊15┊ }
┊14┊16┊}
The only thing left to do before jumping into implementation would be installing Nan:
npm install nan --save
The basis for build process is set. We will create the entry file for our add-on:
Create Add-On Entry File
Added src/distance.cc
+┊ ┊1┊#include <nan.h>
+┊ ┊2┊#include <v8.h>
+┊ ┊3┊
+┊ ┊4┊NAN_MODULE_INIT(Init) {
+┊ ┊5┊
+┊ ┊6┊}
+┊ ┊7┊
+┊ ┊8┊NODE_MODULE(distance, Init)🚫↵
Every add-on should start with these two macro calls. They are both compiled into a piece of code
which will register our module with ease. The NODE_MODULE
macro template accepts the name of the
target as the first argument (That one we set as target_name
in the GYP file, remember?) and the
initialization method for our module. The NAN_MODULE_INIT
generates a function with the given name.
It accepts target
as the first argument which is equivalent to Node.js’ exports
. Now we will
create our first method stub for a synchronous distance calculation:
Create CalculateSync
Method Stub
Changed src/distance.cc
┊ 1┊ 1┊#include <nan.h>
┊ 2┊ 2┊#include <v8.h>
┊ 3┊ 3┊
+┊ ┊ 4┊NAN_METHOD(CalculateSync) {
+┊ ┊ 5┊
+┊ ┊ 6┊}
┊ 5┊ 7┊
+┊ ┊ 8┊NAN_MODULE_INIT(Init) {
+┊ ┊ 9┊ NAN_EXPORT(target, CalculateSync);
┊ 6┊10┊}
┊ 7┊11┊
┊ 8┊12┊NODE_MODULE(distance, Init)🚫↵
We exported the CalculateSync
by using the NAN_EXPORT
macro, and we used NAN_METHOD
to define
a new node-valid function. It accepts info
as the first argument, and it is the same as
JavaScript’s arguments
vector. We already know which arguments this function should accept, that’s
why I followed TDD methodology from the first place:
Destructure Arguments Vector
Changed src/distance.cc
┊ 1┊ 1┊#include <nan.h>
┊ 2┊ 2┊#include <v8.h>
┊ 3┊ 3┊
+┊ ┊ 4┊using Nan::To;
+┊ ┊ 5┊using v8::Local;
+┊ ┊ 6┊using v8::Object;
┊ 5┊ 7┊
+┊ ┊ 8┊NAN_METHOD(CalculateSync) {
+┊ ┊ 9┊ Local<Object> js_pointA = To<Object>(info[0]).ToLocalChecked();
+┊ ┊10┊ Local<Object> js_pointB = To<Object>(info[1]).ToLocalChecked();
┊ 6┊11┊}
┊ 7┊12┊
┊ 8┊13┊NAN_MODULE_INIT(Init) {
We use the To()
function to convert the first argument into the desired type, and then we call the
method ToLocalChecked()
. This method is simply going to convert the result into v8’s Local, unless
the argument is undefined, in which case an error is going to be thrown. I like to prefix JS object
with a js_
so I know with what kind variable I’m dealing with. We should have two points
containing x
and y
fields. Let’s try to extract them out of the arguments vector and convert
them into native C++ structures:
Convert JS Objects to Native C++ Structures
Changed src/distance.cc
┊ 1┊ 1┊#include <nan.h>
┊ 2┊ 2┊#include <v8.h>
┊ 3┊ 3┊
+┊ ┊ 4┊using Nan::New;
┊ 4┊ 5┊using Nan::To;
┊ 5┊ 6┊using v8::Local;
┊ 6┊ 7┊using v8::Object;
+┊ ┊ 8┊using v8::String;
+┊ ┊ 9┊
+┊ ┊10┊struct Point {
+┊ ┊11┊ double x;
+┊ ┊12┊ double y;
+┊ ┊13┊};
┊ 7┊14┊
┊ 8┊15┊NAN_METHOD(CalculateSync) {
┊ 9┊16┊ Local<Object> js_pointA = To<Object>(info[0]).ToLocalChecked();
┊10┊17┊ Local<Object> js_pointB = To<Object>(info[1]).ToLocalChecked();
+┊ ┊18┊
+┊ ┊19┊ Point* pointA = new Point();
+┊ ┊20┊ pointA->x = To<double>(js_pointA->Get(New<String>("x").ToLocalChecked())).FromJust();
+┊ ┊21┊ pointA->y = To<double>(js_pointA->Get(New<String>("y").ToLocalChecked())).FromJust();
+┊ ┊22┊
+┊ ┊23┊ Point* pointB = new Point();
+┊ ┊24┊ pointB->x = To<double>(js_pointB->Get(New<String>("x").ToLocalChecked())).FromJust();
+┊ ┊25┊ pointB->y = To<double>(js_pointB->Get(New<String>("y").ToLocalChecked())).FromJust();
┊11┊26┊}
┊12┊27┊
┊13┊28┊NAN_MODULE_INIT(Init) {
Then again we convert the To()
function to convert the result into the desired data-type, only
this time it’s a primitive, so we use FromJust()
instead of ToLocalChecked()
. Note that v8 only
uses double precision rather than a floating point. We can fetch properties from a given JS object
with ease using the Get()
method. Pay attention to use the ->
rather than a period because
remember, a Local is actually a pointer! It is not the actual object.
Now all is left to do is defining the return value. Keep in mind that the value should be returned
through v8’s current scope, not natively, so using the return
keyword would be useless. The return
value can actually be defined through the provided info
argument, like this:
Add Return Value to CalculateSync
Method
Changed src/distance.cc
┊23┊23┊ Point* pointB = new Point();
┊24┊24┊ pointB->x = To<double>(js_pointB->Get(New<String>("x").ToLocalChecked())).FromJust();
┊25┊25┊ pointB->y = To<double>(js_pointB->Get(New<String>("y").ToLocalChecked())).FromJust();
+┊ ┊26┊
+┊ ┊27┊ info.GetReturnValue().Set(CalculateDistance(pointA, pointB));
┊26┊28┊}
┊27┊29┊
┊28┊30┊NAN_MODULE_INIT(Init) {
And of-course it requires us to add the core distance calculation method:
Add Core Distance Calculation Method
Changed src/distance.cc
+┊ ┊ 1┊#include <cstdlib>
+┊ ┊ 2┊#include <cmath>
┊ 1┊ 3┊#include <nan.h>
┊ 2┊ 4┊#include <v8.h>
┊ 3┊ 5┊
┊ 4┊ 6┊using Nan::New;
┊ 5┊ 7┊using Nan::To;
+┊ ┊ 8┊using std::pow;
+┊ ┊ 9┊using std::sqrt;
┊ 6┊10┊using v8::Local;
┊ 7┊11┊using v8::Object;
┊ 8┊12┊using v8::String;
┊12┊16┊ double y;
┊13┊17┊};
┊14┊18┊
+┊ ┊19┊double CalculateDistance(Point* pointA, Point* pointB) {
+┊ ┊20┊ return sqrt(pow(pointA->x - pointB->x, 2) + pow(pointA->y - pointB->y, 2));
+┊ ┊21┊}
+┊ ┊22┊
┊15┊23┊NAN_METHOD(CalculateSync) {
┊16┊24┊ Local<Object> js_pointA = To<Object>(info[0]).ToLocalChecked();
┊17┊25┊ Local<Object> js_pointB = To<Object>(info[1]).ToLocalChecked();
This is it for the synchronous calculation. Now we will add an async version of it. We will start by creating a method with everything we learned so far until the point where we have to return the result:
Create CalculateAsync
Method with Basic Deconstructuring
Changed src/distance.cc
┊35┊35┊ info.GetReturnValue().Set(CalculateDistance(pointA, pointB));
┊36┊36┊}
┊37┊37┊
+┊ ┊38┊NAN_METHOD(CalculateAsync) {
+┊ ┊39┊ Local<Object> js_pointA = To<Object>(info[0]).ToLocalChecked();
+┊ ┊40┊ Local<Object> js_pointB = To<Object>(info[1]).ToLocalChecked();
+┊ ┊41┊
+┊ ┊42┊ Point* pointA = new Point();
+┊ ┊43┊ pointA->x = To<double>(js_pointA->Get(New<String>("x").ToLocalChecked())).FromJust();
+┊ ┊44┊ pointA->y = To<double>(js_pointA->Get(New<String>("y").ToLocalChecked())).FromJust();
+┊ ┊45┊
+┊ ┊46┊ Point* pointB = new Point();
+┊ ┊47┊ pointB->x = To<double>(js_pointB->Get(New<String>("x").ToLocalChecked())).FromJust();
+┊ ┊48┊ pointB->y = To<double>(js_pointB->Get(New<String>("y").ToLocalChecked())).FromJust();
+┊ ┊49┊}
+┊ ┊50┊
┊38┊51┊NAN_MODULE_INIT(Init) {
┊39┊52┊ NAN_EXPORT(target, CalculateSync);
+┊ ┊53┊ NAN_EXPORT(target, CalculateAsync);
┊40┊54┊}
┊41┊55┊
┊42┊56┊NODE_MODULE(distance, Init)🚫↵
Here’s the different part. We don’t wanna simply return the value, we want to make the calculations in parallel with the event loop, and once we’re finished we will interact with it once again. In our model there are two threads. The first thread is the event loop thread, and the second thread will be a worker thread managed by Nan, the library supports asynchronous I/O in NodeJS. Let’s start implementing and I will give some more explanations as we go further:
Queue Distance Worker
Changed src/distance.cc
┊ 3┊ 3┊#include <nan.h>
┊ 4┊ 4┊#include <v8.h>
┊ 5┊ 5┊
+┊ ┊ 6┊using Nan::AsyncQueueWorker;
+┊ ┊ 7┊using Nan::AsyncWorker;
+┊ ┊ 8┊using Nan::Callback;
┊ 6┊ 9┊using Nan::New;
┊ 7┊10┊using Nan::To;
┊ 8┊11┊using std::pow;
┊ 9┊12┊using std::sqrt;
+┊ ┊13┊using v8::Function;
┊10┊14┊using v8::Local;
┊11┊15┊using v8::Object;
┊12┊16┊using v8::String;
┊38┊42┊NAN_METHOD(CalculateAsync) {
┊39┊43┊ Local<Object> js_pointA = To<Object>(info[0]).ToLocalChecked();
┊40┊44┊ Local<Object> js_pointB = To<Object>(info[1]).ToLocalChecked();
+┊ ┊45┊ Callback* callback = new Callback(info[2].As<Function>());
┊41┊46┊
┊42┊47┊ Point* pointA = new Point();
┊43┊48┊ pointA->x = To<double>(js_pointA->Get(New<String>("x").ToLocalChecked())).FromJust();
┊46┊51┊ Point* pointB = new Point();
┊47┊52┊ pointB->x = To<double>(js_pointB->Get(New<String>("x").ToLocalChecked())).FromJust();
┊48┊53┊ pointB->y = To<double>(js_pointB->Get(New<String>("y").ToLocalChecked())).FromJust();
+┊ ┊54┊
+┊ ┊55┊ AsyncQueueWorker(new DistanceWorker(callback, pointA, pointB));
┊49┊56┊}
┊50┊57┊
┊51┊58┊NAN_MODULE_INIT(Init) {
Here we fetch the third argument which is the callback. We wrap it with Nan’s Callback, which will
make sure it is not garbage collected once the scope is being deleted, we want it to keep living
until it’s not relevant. At the bottom of the method, instead of returning a value explicitly, we
queue our DistanceWorker
into the workers queue. On that note, let’s implement the DistanceWorker:
Create DistanceWorker
with a Constructor and a Deconstructor
Changed src/distance.cc
┊24┊24┊ return sqrt(pow(pointA->x - pointB->x, 2) + pow(pointA->y - pointB->y, 2));
┊25┊25┊}
┊26┊26┊
+┊ ┊27┊class DistanceWorker : public AsyncWorker {
+┊ ┊28┊ private:
+┊ ┊29┊ Point* pointA;
+┊ ┊30┊ Point* pointB;
+┊ ┊31┊
+┊ ┊32┊ public:
+┊ ┊33┊ DistanceWorker(Callback* callback, Point* pointA, Point* pointB) :
+┊ ┊34┊ AsyncWorker(callback), pointA(pointA), pointB(pointB) {}
+┊ ┊35┊
+┊ ┊36┊ ~DistanceWorker() {
+┊ ┊37┊ delete pointA;
+┊ ┊38┊ delete pointB;
+┊ ┊39┊ }
+┊ ┊40┊
+┊ ┊41┊ void Execute () {
+┊ ┊42┊
+┊ ┊43┊ }
+┊ ┊44┊
+┊ ┊45┊ void HandleOKCallback () {
+┊ ┊46┊
+┊ ┊47┊ }
+┊ ┊48┊};
+┊ ┊49┊
┊27┊50┊NAN_METHOD(CalculateSync) {
┊28┊51┊ Local<Object> js_pointA = To<Object>(info[0]).ToLocalChecked();
┊29┊52┊ Local<Object> js_pointB = To<Object>(info[1]).ToLocalChecked();
AsyncWorker
is an abstract class that you can subclass to have much of the annoying asynchronous
queuing and handling taken care of for you. It can even store arbitrary v8 objects for you and have
them persist while the asynchronous work is in progress. The execute()
method is being executed
inside the worker-thread. It is not safe to access V8, or V8 data structures there, so everything we
need for input and output should go on ‘this’. The HandleOKCallback()
method is executed when the
async work is complete. This function will be run inside the main event loop, so it is safe to use
v8 again. Let’s implement the core distance calculation on the worker thread:
Execute Distance Calculation
Changed src/distance.cc
┊26┊26┊
┊27┊27┊class DistanceWorker : public AsyncWorker {
┊28┊28┊ private:
+┊ ┊29┊ double distance;
┊29┊30┊ Point* pointA;
┊30┊31┊ Point* pointB;
┊31┊32┊
┊39┊40┊ }
┊40┊41┊
┊41┊42┊ void Execute () {
+┊ ┊43┊ distance = CalculateDistance(pointA, pointB);
┊43┊44┊ }
┊44┊45┊
┊45┊46┊ void HandleOKCallback () {
And handle a successful invocation once the calculation is finished:
Handle Successful Invokation
Changed src/distance.cc
┊ 6┊ 6┊using Nan::AsyncQueueWorker;
┊ 7┊ 7┊using Nan::AsyncWorker;
┊ 8┊ 8┊using Nan::Callback;
+┊ ┊ 9┊using Nan::HandleScope;
┊ 9┊10┊using Nan::New;
+┊ ┊11┊using Nan::Null;
┊10┊12┊using Nan::To;
┊11┊13┊using std::pow;
┊12┊14┊using std::sqrt;
┊13┊15┊using v8::Function;
┊14┊16┊using v8::Local;
+┊ ┊17┊using v8::Number;
┊15┊18┊using v8::Object;
┊16┊19┊using v8::String;
+┊ ┊20┊using v8::Value;
┊17┊21┊
┊18┊22┊struct Point {
┊19┊23┊ double x;
┊44┊48┊ }
┊45┊49┊
┊46┊50┊ void HandleOKCallback () {
+┊ ┊51┊ HandleScope scope;
┊47┊52┊
+┊ ┊53┊ Local<Value> argv[] = {
+┊ ┊54┊ Null(),
+┊ ┊55┊ New<Number>(distance)
+┊ ┊56┊ };
+┊ ┊57┊
+┊ ┊58┊ callback->Call(2, argv);
┊48┊59┊ }
┊49┊60┊};
Normally, when defining a NodeJS method (NAN_METHOD
macro) a scope will be created for us
automatically. In this function’s context there is no scope exist, so we will have to create it
using the HandleScope
deceleration (The current scope is stored globally so even though we don’t
use it explicitly, v8 and Nan know what to do). We also created an arguments vector as the return
value, following Node.js’ conventions, the first argument would be the error and the second argument
would be the result.
This is it! Finally, we will transform the add-on into a nicer looking node-module:
Transform Add-On into a Nicer Looking Node Module
Added index.js
+┊ ┊1┊const Distance = require('./build/Release/distance');
+┊ ┊2┊
+┊ ┊3┊exports.calculate = {
+┊ ┊4┊ sync: Distance.CalculateSync,
+┊ ┊5┊ async: Distance.CalculateAsync
+┊ ┊6┊};🚫↵
And now, let’s run our small test to see that it works, using the following command:
npm run test
If everything went well, you should have the following messages printed to the terminal:
sync calculation passed
async calculation passed
What’s Next?
You’ve just learned the very basics of how to use C++ within NodeJS. There’s a lot more to learn when it comes to building an add-on, and I’m not just talking about learning v8 and Nan’s API. Think about the possibilities, the C++ community have been developed for years and there are so much great libraries out there that are not necessarily relevant to NodeJS due to its efficiency, like Boost, OpenCV, CGAL and many more.
- Check out the full project of this tutorial: https://github.com/DAB0mB/node-distance-addon
- Check out this awesome framework for creating tutorials by Urigo, which helped me to make this nicely structured tutorial: https://github.com/Urigo/tortilla
Join our newsletter
Want to hear from us when there's something new?
Sign up and stay up to date!
*By subscribing, you agree with Beehiiv’s Terms of Service and Privacy Policy.