Felix Lemke

NodeJS C++ datatransfer

Because of a lack of documentation, I decided to write a short tutorial for exchanging data between NodeJS JavaScript and C++ for implementing native extensions. The extensions enable the usage of low level C++ apis and (in some cases) rewards the developer or user with performance benefits, when using correctly. The article requires JavaScript and C++ knowledge. If you don't want to know how to setup a nodejs c++ addon, directly jump to the JavaScript to C++ or C++ to JavaScript sections.

Preparations

In order to transfer data between JavaScript and C++ there has to exist a working NodeJS addon. In this section there is an explanation of the addon setup. Create the following folder structure.

At first install required dependencies with

yarn add node-gyp nan --dev

And add the following content to your binding.gyp file

{
  "targets": [
    {
      "target_name": "addon",
      "sources": [
        "src/addon-class.cc",
        "src/addon.cc"
      ],
      "include_dirs" : [
        "<!(node -e \"require('nan')\")"
      ]
    }
  ]
}

Afterwards initialize the node module with the following content in your addon.cc file.

#include <nan.h>

#include "addon-class.h"

NAN_MODULE_INIT(InitAll) {
  Numscript::Init(target);
}

NODE_MODULE(numscript, InitAll)

The addon class header addon-class.h looks like follows

#ifndef ADDONCLASS_H
#define ADDONCLASS_H

#include <nan.h>

class AddonClass : public Nan::ObjectWrap {
  public:
    static NAN_MODULE_INIT(Init);

  private:
    AddonClass();
    ~AddonClass();

    static NAN_METHOD(New);
    static NAN_METHOD(setup);
    static NAN_METHOD(simulate);

    Nan::Persistent<v8::Function> constructor;
};

#endif

And in the classes source file addon-class.cc you can implement the functions in the following manner.

NAN_MODULE_INIT(Numscript::Init) {
  v8::Local<v8::FunctionTemplate> ctor = Nan::New<v8::FunctionTemplate>(New);
  auto ctorInst = ctor->InstanceTemplate();
  ctorInst->SetInternalFieldCount(1);

  // This is where the fun begins.

  target->Set(Nan::New("AddonClass").ToLocalChecked(), ctor->GetFunction());
}

And this is how we create the class instance.

NAN_METHOD(Numscript::New) {
  if (info.IsConstructCall()) {
    Numscript *obj = new Numscript();
    obj->Wrap(info.This());
    info.GetReturnValue().Set(info.This());
  }
  
  return;
}

In the package.json file add the building script, to configure node-gyp and build node-gyp.

"scripts": {
  "build": "node-gyp configure && node-gyp build"
}

And finally you should be able to run yarn build to run the build script.

JavaScript to C++

Working with function arguments is essential for working with C++ addons, e.g. for passing configurations or arrays of data to work with. When declaring a function as NAN_METHOD(FunctionName) (which is a helper macro for static void FunctionName(const Nan::FunctionCallbackInfo& info)) there is the info parameter which contains the JS object instance itself and all Javascript function parameters as an array.

For an examplary javascript function myFunction(callback: () => void, runs: number, title: string) => void the regarding C++ could be

Nan::Callback *callback = new Nan::Callback(Nan::To<v8::Function>(info[0]).ToLocalChecked());
int runs = info[1]->Uint32Value();
std::string name = *v8::String::Utf8Value(info[2]->ToString());

In addition to parsing there are several helper functions to check the argument type. They all return c++ boolean values.

info[0]->IsFunction()
info[0]->IsString()
info[0]->IsNumber()

This is very handy with the nan error handling

return Nan::ThrowTypeError(
  "Argument must be a string");
}

C++ to JavaScript

If one calls a native c++ function from within JavaScript, it (in many cases) expects a return value. One can set a return value of type v8::Local<T> with the following example

NAN_METHOD(Numscript::setup) {
  info.GetReturnValue().Set(Nan::New<v8::String>("Hello world").ToLocalChecked());
}

New objects or primitives can be created with the

Nan::New()

method. It has several overwrites. If the parameter is kind of string (e.g. char[], std::string) it returns a MaybeLocal object of generic type T which can be checked by .ToLocalChecked() and returns a v8::Local<T>. On the other hand there is no need to local check numbers and other primitives.

Primitives

To create primitives of type string, number, boolean, undefined or null as v8::Local<T>, use the following code snippets.

Nan::New("string").ToLocalChecked()
Nan::New<v8::Number>(42)
Nan::True()
Nan::False()
Nan::Undefined()
Nan::Null()

Objects

It is a little more complex to create objects. Here you have got several options.

v8::Local<v8::ObjectTemplate> objTemplate = Nan::New<v8::ObjectTemplate>();
objTemplate->Set(Nan::New("prop").ToLocalChecked(), Nan::New<v8::Number>(42));

Imagine you want to access the setup function as a property of an object, you can do the following

v8::Local<v8::FunctionTemplate> fn = Nan::New<v8::FunctionTemplate>(setup);
objTemplate->Set(Nan::New("setup").ToLocalChecked(), fn);

And finally bind the object template to the addon instance by

Nan::SetPrototypeTemplate(ctor, "myObj", objTemplate);

References

Felix Lemke
Make. Learn. Share.