-
Notifications
You must be signed in to change notification settings - Fork 0
Node.js native C modules
Overview:
- make project folder as a git repo
- as a Node.js module, it is defined for
npm
in apackage.json
file - write C++ code against the Node.js API
- build it with
node-gyp
via definitions in abinding.gyp
file - test it / wrap it with a
index.js
file
Assuming a module called "addon":
mkdir addon
cd addon
git init
npm init
npm install --save bindings
npm install --save-dev node-gyp
Edit the package.json
to add "gypfile": true
. You might also want to set a postinstall script to run a test after each build:
{
"name": "<module name>",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"postinstall": "node test.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"bindings": "^1.5.0"
},
"devDependencies": {
"node-gyp": "^6.1.0"
},
"gypfile": true
}
Create a binding.gyp
file (the code below assumes module name is "addon"):
{
"targets": [{
"target_name": "addon",
"sources": [ "addon.cpp" ],
"defines": [],
"cflags": ["-std=c++11", "-Wall", "-pedantic"],
"include_dirs": [],
"libraries": [],
"dependencies": [],
"conditions": [
['OS=="win"', {}],
['OS=="mac"', {}],
['OS=="linux"', {}],
],
}]
}
A minimal addon.cpp
:
#include <node_api.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
napi_value Hello(napi_env env, const napi_callback_info info) {
printf("hello\n");
return nullptr;
}
napi_value Goodbye(napi_env env, const napi_callback_info info) {
napi_value msg;
napi_create_string_utf8(env, "ciao", NAPI_AUTO_LENGTH, &msg);
return msg;
}
napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor export_properties[] = {
{
"Hello", nullptr, Hello,
nullptr, nullptr, nullptr, napi_default, nullptr
},
{
"Goodbye", nullptr, Goodbye,
nullptr, nullptr, nullptr, napi_default, nullptr
},
};
assert(napi_define_properties(env, exports,
sizeof(export_properties) / sizeof(export_properties[0]), export_properties) == napi_ok);
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
A minimal index.js
as the entry-point for the module:
const addon = require('bindings')('addon');
// module.exports defines what the module actually exposes to code that loads it:
module.exports = addon;
Why module.exports = addon
? In fact most native modules use a javascript wrapper, setting index.js
as the entry point for the module. This lets you wrap the native code with extra features that are much easier to write in javascript than in C++.
A minimal test.js
:
const assert = require("assert")
const addon = require("./index.js");
console.log(addon);
npm install
node test.js
Node-gyp is a command-line utility for compiling C/C++ code into binaries. It was designed for building node.js modules, but it can also compile general libraries and executables.
The main methods are node-gyp rebuild
to build the module and node-gyp clean
to delete all build files again
This file is a JSON configuration to tell node-gyp how to build files. Similar to a makefile
or CMakeLists.txt
file, this specifies the details of how to compile the code, including things like additional include paths, library paths, defines, linker options, etc., but it does so as a JSON structure rather than a sequence of commands. It has very many options.
Relative paths are OK in a binding.gyp file, and in most cases are relative to the binding.gyp file location. However for some reason, relative library search paths need an extra ../
in front of them.
Node-gyp disables exceptions by default, but if you need them:
"cflags!": [ "-fno-exceptions" ],
"cflags_cc!": [ "-fno-exceptions" ],
To specify things according to the OS platform (important for different libraries, for example):
"conditions": [
['OS=="win"', {}],
['OS=="mac"', {}],
['OS=="linux"', {}],
],
Does your module depend on a dll? If so, you might get weird 'module could not be found' errors, because node can't find the dll in question. To fix, add a 'copies' section to the binding.gyp. Probably this should be inside the "conditions" section for 'OS=="win"'.
"copies": [{
'destination': './build/Release',
'files': [
"<path to dll>"
]
}],
The API reference docs are here -- but they are really a reference, and not a good place to learn from initially.
There is an official repository of examples here
Here's a quick tutorial on the napi
API
Note that there is a confusingly similar named node-addon-api
, which is just a C++ wrapper around the N-API.
Getting argument values in a call:
napi_value methodname(napi_env, const napi_callback_info info) {
size_t argc = 1; // expect 1 arg
napi_value argv[1]; // this array holds the args
assert(napi_ok == napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr));
// or
napi_value this; // this holds the `this` value of the function
void * data; // this is the implicit void * data attached to the function (if any)
assert(napi_ok == napi_get_cb_info(env, info, &argc, argv, &this, &data));
There are several different APIs for developing native (i.e. C/C++) Node.js modules. Until recently, the recommended API was called nan
, however this C++ API does not guarantee stability. But nowadays it is recommended instead to use the napi
API, which does guarantee ABI stability. There is also a C++ wrapper of this API, with the confusingly-similar name of node-addon-api
.
If you want to use the C++ wrapper of napi
:
-
install it:
npm install --save-dev node-addon-api
-
add to binding.gyp:
'include_dirs': [ "<!@(node -p \"require('node-addon-api').include\")" ],
- additional requirements per https://github.com/nodejs/node-addon-api/blob/HEAD/doc/setup.md -- in particular choose whether to support C++ exceptions
-
The C++ file looks like this instead:
#include <napi.h>
Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
return exports;
}
Two options, defined per the gyp configuration: C++ exceptions, or error state flag.
https://github.com/nodejs/node-addon-api/blob/HEAD/doc/error_handling.md
throw Napi::Error::New(env, "Example exception");
The exception will bubble up as a C++ exception of type Napi::Error
, until it is either caught while still in C++, or else automatically propagated as a JavaScript exception when returning to JavaScript. Catch in C++ like this:
try {
result = jsFunctionThatThrows({ arg1, arg2 });
} catch (const Error& e) {
cerr << "Caught JavaScript exception: " + e.what();
}
Napi::Error::New(env, "Example exception").ThrowAsJavaScriptException();
Napi::Value result = jsFunctionThatThrows({ arg1, arg2 });
if (env.IsExceptionPending()) {
Error e = env.GetAndClearPendingException();
return e.Value();
}
Napi::Env env;
env.Null(), env.Undefined(), env.Global()
env.RunScript(std::string||char *||Napi::String)
Do not store data in C++ global (static) space, since a module may be loaded into more than one running environment/thread.
Option 1: Associate a C++ data item with the current instance of the addon:
template <typename T> using Finalizer = void (*)(Env, T*);
template <typename T, Finalizer<T> fini = Env::DefaultFini<T>>
env.SetInstanceData(T* data);
T* data = env.GetInstanceData();
Option 2: Use the Napi::Addon<T>
method: https://github.com/nodejs/node-addon-api/blob/main/doc/addon.md:
class Module : public Napi::Addon<Module> {
public:
Napi::Value example(const Napi::CallbackInfo& info) {
return Napi::Number::New(info.Env(), 1);
}
Module(Napi::Env env, Napi::Object exports) {
// See https://github.com/nodejs/node-addon-api/blob/main/doc/class_property_descriptor.md
DefineAddon(exports, {
InstanceMethod("index", &Module::example),
// InstanceValue, InstanceAccessor etc.
});
}
};
NODE_API_ADDON(Module)
Napi::Value mymethod(const Napi::CallbackInfo& info) { ... }
info.Env(), info.Length(), info[0] etc.,
info.This() // whatever the JS `this` points to
info.Data() / info.SetData(void*)
if (info.Length() >= 1 && info[0].IsObject()) {
Napi::Object obj = info[0].As<Napi::Object>();
}
// or, coerced:
if (info.Length() >= 1) {
Napi::Object obj = info[0].ToObject>();
}
Represents a JS value, base class of Napi::Number, Boolean, String, etc. https://github.com/nodejs/node-addon-api/blob/main/doc/value.md
Defines a class whose instances behave like a JS object, but which has a C++ object backing with members and methods. https://github.com/nodejs/node-addon-api/blob/main/doc/object_wrap.md
Normally, Napi values live for as long as the native method call To limit to a shorter scope (e.g. efficient for loop):
for (int i = 0; i < LOOP_MAX; i++) {
Napi::HandleScope scope(info.Env());
Napi::Value newValue = // whatever. This newValue dies at the end of this loop iteration.
};
https://github.com/nodejs/node-addon-api/blob/main/doc/object_lifetime_management.md
Create a reference-counted wrapper to prevent a value from garbage collection https://github.com/nodejs/node-addon-api/blob/main/doc/reference.md
ref = Napi::Reference::Reference(env, value)
ref.Env(), ref.IsEmpty(), ref.Value(),
ref.Ref() / ref.Unref() // incr/decr