Understanding Node.js C++ Addons

Understanding Node.js C++ Addons

·

4 min read

We know that node.js is run by V8 engine just like in Chrome. V8 is a JavaScript engine written in C++. So built-in functions in node.js is actually written in C++ and provided by some binding logic. This techinque is open in node.js. So we could write our own business logic in C++ and use the same binding logic and call the code in JavaScript. This is called C++ addons.

Below code I copy from node.js C++ addons document, which shows a simple C++ addon.

// hello.cc
#include <node.h>

namespace demo {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

void Method(const FunctionCallbackInfo<Value>& args) {
  Isolate* isolate = args.GetIsolate();
  args.GetReturnValue().Set(String::NewFromUtf8(
      isolate, "world").ToLocalChecked());
}

void Initialize(Local<Object> exports) {
  NODE_SET_METHOD(exports, "hello", Method);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)

}  // namespace demo

As you can see, inside the code, the v8 functions are used. Becuase the v8 apis are changed constantly, there is another package called Node-API, which provides a wrapper to make this addons development more easily. Below code shows what node-api for addons looks like.

#include <assert.h>
#include <node_api.h>

static napi_value Method(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value world;
  status = napi_create_string_utf8(env, "world", 5, &world);
  assert(status == napi_ok);
  return world;
}

#define DECLARE_NAPI_METHOD(name, func) { name, 0, func, 0, 0, 0, napi_default, 0 }

static napi_value Init(napi_env env, napi_value exports) {
  napi_status status;
  napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method);
  status = napi_define_properties(env, exports, 1, &desc);
  assert(status == napi_ok);
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

As you can see, this code use the header file <node_api.h>, instead of <node.h>.

Now we know the basic concept of adddons, let walk through a simple hello world example.

First, prepare some basic tools. In mac or linux, check if these tools are installed properly.

node --version
npm --version
python --version
git --version
cc --version
make --version

Then create a simple package first.

npm init -y

Inside package.json files, change the content as below.

{
  "name": "hello",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "gypfile": true
}

As you can see, the most important thing is to set gypfile: true. With this key enabled, later npm install command will try to find the binding.gyp file and use node-gyp tool to compile the project.

Now let's write the binding.gyp file, which is a config file for compiling C++ code into C++ addons.

{
  "targets": [
    {
      "target_name": "hello",
      "sources": [ "hello.c" ]
    }
  ]
}

The target_name key is the name of the package. Later we can use this name to require the addon. The sources key contains the C++ code files needed to be compiled.

Next, let write the C++ code using the node api.

#include <assert.h>
#include <node_api.h>

// this is the real method to be called
static napi_value Method(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value world;
  status = napi_create_string_utf8(env, "world", 5, &world);
  assert(status == napi_ok);
  return world;
}

#define DECLARE_NAPI_METHOD(name, func) { name, 0, func, 0, 0, 0, napi_default, 0 }

static napi_value Init(napi_env env, napi_value exports) {
  napi_status status;
  // this defines the method binding
  napi_property_descriptor desc = DECLARE_NAPI_METHOD("hello", Method);
  status = napi_define_properties(env, exports, 1, &desc);
  assert(status == napi_ok);
  return exports;
}

// NODE_GYP_MODULE_NAME is the target_name in the binding.gpy file
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

OK, now we have all basic stuff for this addon, let compile the code.

% npm install             

> hello@1.0.0 install
> node-gyp rebuild

gyp info it worked if it ends with ok
gyp info using node-gyp@9.0.0
gyp info using node@16.17.0 | darwin | arm64
gyp info find Python using Python version 3.8.9 found at "/Library/Developer/CommandLineTools/usr/bin/python3"
gyp info spawn /Library/Developer/CommandLineTools/usr/bin/python3
gyp info spawn args [
gyp info spawn args   '/Users/yao/.nvm/versions/node/v16.17.0/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py',
gyp info spawn args   'binding.gyp',
gyp info spawn args   '-f',
gyp info spawn args   'make',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/yao/Documents/GitHub/demos/todo/c++-addon/hello/build/config.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/yao/.nvm/versions/node/v16.17.0/lib/node_modules/npm/node_modules/node-gyp/addon.gypi',
gyp info spawn args   '-I',
gyp info spawn args   '/Users/yao/Library/Caches/node-gyp/16.17.0/include/node/common.gypi',
gyp info spawn args   '-Dlibrary=shared_library',
gyp info spawn args   '-Dvisibility=default',
gyp info spawn args   '-Dnode_root_dir=/Users/yao/Library/Caches/node-gyp/16.17.0',
gyp info spawn args   '-Dnode_gyp_dir=/Users/yao/.nvm/versions/node/v16.17.0/lib/node_modules/npm/node_modules/node-gyp',
gyp info spawn args   '-Dnode_lib_file=/Users/yao/Library/Caches/node-gyp/16.17.0/<(target_arch)/node.lib',
gyp info spawn args   '-Dmodule_root_dir=/Users/yao/Documents/GitHub/demos/todo/c++-addon/hello',
gyp info spawn args   '-Dnode_engine=v8',
gyp info spawn args   '--depth=.',
gyp info spawn args   '--no-parallel',
gyp info spawn args   '--generator-output',
gyp info spawn args   'build',
gyp info spawn args   '-Goutput_dir=.'
gyp info spawn args ]
gyp info spawn make
gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
  CC(target) Release/obj.target/hello/hello.o
  SOLINK_MODULE(target) Release/hello.node
gyp info ok 

up to date, audited 1 package in 350ms

found 0 vulnerabilities

If everything runs ok, a build folder should be generated and the ./build/Release/hello.node file is the real addon.

Now let's try to require this addon and call the method we defined.

var addon = require('./build/Release/hello.node');

console.log(addon.hello()); // 'world'

Run the test.

% npm run test

> hello@1.0.0 test
> node index.js

world

OK, this is the basic process about writing an C++ addon. If you want to know more, check this repository node-addon-examples.