本质上,Node.js 扩展就是 C++动态链接库:
Addons are dynamically-linked shared objects written in C++.
相当于JS 通往 C/C++世界的一扇门:
Addons provide an interface between JavaScript and C/C++ libraries.
这些 C++扩展(xxx.node
文件)也能像 JS 模块一样直接require
使用,因为Node 模块加载机制提供了原生支持
P.S.所谓动态链接库,就是能在运行时动态加载的库(.so
文件,或者 Windows 下的.dll
文件):
A shared library(.so) is a library that is linked but not embedded in the final executable, so will be loaded when the executable is launched and need to be present in the system where the executable is deployed.
与之相对的是静态库(.a
文件),编译时链接到可执行文件中,无需从外部加载:
A static library(.a) is a library that can be linked directly into the final executable produced by the linker,it is contained in it and there is no need to have the library into the system where the executable will be deployed.
在 Node.js 中,编写一个 C++扩展有 3 种方式:
P.S.实际上,有了 N-API 这层独立抽象之后,C++扩展还能跨 JavaScript 引擎、跨 Electron 等运行时,具体见The Future of Native Modules in Node.js
其中,N-API 是首选方式,除非用 N-API 搞不定才考虑其它方式:
Unless there is a need for direct access to functionality which is not exposed by N-API, use N-API.
跨 Node 版本(无需重编)直接运行无疑是决定性的优势,但只有专门提供的 N-API 才保证 ABI 稳定。也就是说,只用 N-API(不同时混用下层的 Node、V8、libuv API)才能保证 C++扩展在不同的 Node 版本下可以直接运行,具体见Implications of ABI Stability
不用 N-API 的话,手搓一个有些复杂,涉及好几层的知识:
node::ObjectWrap
类P.S.关于 Node.js 源码依赖、运行机制的更多信息,见Node.js 架构剖析
清晰起见,这里采用最原始的方式,手搓一个最简单的 C++扩展:
// hoho.cc
// 见 https://github.com/nodejs/node/blob/master/src/node.h
#include <node.h>
// 见 https://github.com/nodejs/node/blob/master/deps/v8/include/v8.h
using namespace v8;
namespace demo {
void Method(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(
isolate, "hoho, there.", NewStringType::kNormal).ToLocalChecked());
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "hoho", Method);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
}
注意到其中关键的两行:
// 实现初始化方法
void Initialize(Local<Object> exports) { /* ... */ }
// 注册模块名对应的初始化逻辑
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
C++扩展通过 Node.js 提供的NODE_MODULE
宏将初始化方法(Initialize
)暴露出来,其中NODE_GYP_MODULE_NAME
是个宏(macro),在编译前的预处理阶段会被展开成node-gyp
命令传入的模块名
P.S.宏展开可以理解为字符串替换,具体见Macros
在对 C++源码进行编译之前,先要有一份编译配置:
{
"targets": [
{
"target_name": "hoho",
"sources": [ "hoho.cc" ]
}
]
}
配置文件名为binding.gyp
,放在项目根目录下(类似于package.json
),供node-gyp
编译使用
P.S.binding.gyp
具体格式及各字段含义见Input Format Reference
先要安装node-gyp
命令:
npm install -g node-gyp
P.S.当然,也可以npm install node-gyp
将其安装到当前项目,并通过npx node-gyp
调用
接着通过node-gyp configure
命令,生成当前平台构建过程所需的配置文件(Unix 系统下生成 Makefile,Windows 下是 vcxproj 文件),例如(Mac OSX):
$ node-gyp configure
gyp info it worked if it ends with ok
...
gyp info ok
# 生成的文件位于 build 目录下
$ tree build/
build/
├── Makefile
├── binding.Makefile
├── config.gypi
├── gyp-mac-tool
└── hoho.target.mk
编译得到.node
二进制文件:
$ node-gyp build
gyp info it worked if it ends with ok
gyp info using node-gyp@6.1.0
gyp info using node@10.18.0 | darwin | x64
gyp info spawn make
gyp info spawn args [ 'BUILDTYPE=Release', '-C', 'build' ]
CXX(target) Release/obj.target/hoho/hoho.o
SOLINK_MODULE(target) Release/hoho.node
gyp info ok
编译产物位于Release/hoho.node
,试玩一下:
// index.js
// 省略后缀名,自动找到hoho.node并加载、初始化
const hoho = require('./build/Release/hoho');
console.log(hoho.hoho());
运行结果:
$ node index.js
hoho, there.
上例直接使用了 Node、V8 提供的 C++ API,可能存在跨版本兼容性问题(过几个版本可能就编译报错了),并且在不同版本的 Node 环境下都需要重新编译,否则会产生运行时报错:
$ node -v
v10.18.0
# 切换到8.17.0
$ n 8.17.0
# 不重编直接执行
$ node index.js
module.js:682
return process.dlopen(module, path._makeLong(filename));
^
Error: The module '/path/to/hoho/build/Release/hoho.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 64. This version of Node.js requires
NODE_MODULE_VERSION 57. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).
必须重新编译、执行:
$ node-gyp rebuild
gyp info it worked if it ends with ok
...
gyp info ok
$ node index.js
hoho, there.
那么,有没有一劳永逸的方式?
有。N-API
不直接用 Node、V8 等下层 C/C++模块暴露出来的 API,全都换用 N-API:
// hoho-anywhere.cc
#include <node_api.h>
namespace demo {
napi_value Method(napi_env env, napi_callback_info args) {
napi_value greeting;
napi_status status;
status = napi_create_string_utf8(env, "hoho, anywhere.", NAPI_AUTO_LENGTH, &greeting);
if (status != napi_ok) return nullptr;
return greeting;
}
napi_value init(napi_env env, napi_value exports) {
napi_status status;
napi_value fn;
status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn);
if (status != napi_ok) return nullptr;
status = napi_set_named_property(env, exports, "hoho", fn);
if (status != napi_ok) return nullptr;
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
}
只引一个头文件node_api.h
,值类型等也不再直接使用v8::String
修改编译配置binding.gyp
:
{
"targets": [
{
"target_name": "hoho",
"sources": [ "hoho-anywhere.cc" ]
}
]
}
编译运行:
$ node-gyp rebuild
$ node index.js
hoho, anywhere.
# 切换Node版本
$ n 8.17.0
# 无需编译,可直接运行!
$ node index.js
hoho, anywhere.
P.S.更复杂的用法,以及关于 N-API 的更多信息,见N-API
P.S.另外,N-API 提供的都是 C 接口,对于 C++环境,可采用node-addon-api
有些场景下,用 C++扩展来实现尤为合适:
P.S.注意,运行时初始化 C++模板本就存在一些开销,在苛求性能的场景要把这个因素考虑进来,并且 C++并不总是比 JS 快(比如正则匹配的某些场景)