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.