在使用 Electron 开发客户端时,如果现有 Node 模块所提供的功能无法满足需要,我们可以使用 C++ 开发自定义的 Node 模块,也称插件(addon)。
Node.js 插件的扩展名为 .node,是二进制文件,其本质上是通过动态链接库(.dll 或 .so)重命名而来。
选择 Node-API
开发 Node.js 扩展的方式有三种:
- Node-API(以前叫 N-API)
- nan
- 直接使用 v8、libuv 等库进行开发
除非是为了使用 Node-API 未公开的接口,否则建议使用 Node-API 进行开发。
因为 Node-API 是二进制(ABI)兼容的,它将底层 JavaScript 引擎与上层插件隔离开了,JavaScripty 引擎的修改不会影响我们开发的上层插件,我们基于某个版本编译的插件在不需要重新编译的情况下,就可以运行在其他版本的 Node.js 中。
安装编译环境
Node 插件使用 C++开发,因此在不同的系统上采用不同的编译环境。
在 Linux 环境通常使用 GCC 和 LLVM;
Mac 环境通常使用 Xcode;
Windows 环境通常使用 Visual Studio,如果不想安装完整的 Visual Studio,可以使用如下命令仅安装必要的工具链:
1
| npm install --global windows-build-tools
|
Node 插件通常使用 node-gyp 进行编译,node-gyp 基于 Google 的 gyp-next 构建系统,node-gyp 已经与 npm 捆绑在一起,但我们在使用 node-gyp 之前还需要先安装 Python。
至此 Node 插件开发的环境已经搭建完成。
搭建工程
本文以在 Windows 下开发 Node 插件为例,其他系统环境在编译选项方面略有不同
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| { "name": "node-addson-sample", "version": "1.0.0", "private": true, "description": "A sample node addson sample", "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.1.0" }, "scripts": { "build-debug": "node-gyp --debug --arch=x64 configure rebuild", "build-release": "node-gyp --release --arch=x64 configure rebuild", "test": "node test.js" } }
|
使用npm install安装依赖项。
各个依赖项的作用如下:
build-debug和build-release脚本分别用于编译 Debug 和 Release 版本的插件;
test脚本用于执行测试用例;
32 位插件
指定 arch 为 ia32(--arch=ia32)就可以编译 32 位版本的 Node 插件。
需要注意:64 位版本 Node.js 只能加载 64 位的 Node 插件,32 位版本的 Node.js 也只能加载 32 位的 Node 插件,否则会报错:
1
| Error: \\?\D:\node-addon-sample\build\Debug\node-addon-sample.node is not a valid Win32 application.
|
编译脚本
Node-API 支持 GYP 和 CMake.js 两种编译方式,这里选择使用 GYP 方式。
新建binding.gyp文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| { "targets": [ { "target_name": "node-addson-sample", # ***.node "cflags!": [ "-fno-exceptions" ], "cflags_cc!": [ "-fno-exceptions" ], # 指定需要编译的源文件 "sources": [ "main.cpp" ], "include_dirs": [ "<!@(node -p \"require('node-addon-api').include\")" ], # 预编译宏 "defines": [ "NAPI_CPP_EXCEPTIONS", # 在Node-API中启用C++异常 ], "conditions": [ [ # Windows平台编译选项 "OS == 'win'", { "configurations": { # Debug编译选项 "Debug": { # 预编译宏 "defines": [ "DEBUG", "_DEBUG" ], "cflags": [ "-g", "-O0" ], "conditions": [ [ "target_arch=='x64'", { "msvs_configuration_platform": "x64", } ], ], "msvs_settings": { "VCCLCompilerTool": { # 0 - MultiThreaded (/MT) # 1 - MultiThreadedDebug (/MTd) # 2 - MultiThreadedDLL (/MD) # 3 - MultiThreadedDebugDLL (/MDd) "RuntimeLibrary": 1, # /MTd "Optimization": 0, # /Od, no optimization "MinimalRebuild": "false", "OmitFramePointers": "false", "BasicRuntimeChecks": 3, # /RTC1 "AdditionalOptions": [ "/EHsc" ], }, "VCLinkerTool": { "LinkIncremental": 2, # Enable incremental linking # 附加依赖库 "AdditionalDependencies": [ ], }, }, # 附加包含目录 "include_dirs": [ ], }, # Debug编译选项 "Release": { # 预编译宏 "defines": [ "NDEBUG" ], "msvs_settings": { "VCCLCompilerTool": { "RuntimeLibrary": 0, # /MT "Optimization": 3, # /Ox, full optimization "FavorSizeOrSpeed": 1, # /Ot, favour speed over size "InlineFunctionExpansion": 2, # /Ob2, inline anything eligible "WholeProgramOptimization": "false", # Dsiable /GL, whole program optimization, needed for LTCG "OmitFramePointers": "true", "EnableFunctionLevelLinking": "true", "EnableIntrinsicFunctions": "true", "RuntimeTypeInfo": "false", "ExceptionHandling": "2", # /EHsc "AdditionalOptions": [ "/MP", # compile across multiple CPUs ], "DebugInformationFormat": 3, "AdditionalOptions": [ ], }, "VCLibrarianTool": { "AdditionalOptions": [ "/LTCG", # link time code generation ], }, "VCLinkerTool": { "LinkTimeCodeGeneration": 1, # link-time code generation "OptimizeReferences": 2, # /OPT:REF "EnableCOMDATFolding": 2, # /OPT:ICF "LinkIncremental": 1, # disable incremental linking # 附加依赖库 "AdditionalDependencies": [ ], }, }, # 附加包含目录 "include_dirs": [ ], } } }, ] ] } ] }
|
binding.gyp 中的编译选项大多与特定平台的编译器有关,具体可以查阅相关编译器文档,如 Windows 平台可以查询MSVC 文档。
可以使用如下命令指定需要使用的 Visual Stuido 版本:
1
| npm config set msvs_version 20xx
|
node-gyp 官方提供了一些示例,我们可以从这些示例中获取不少灵感:
binding.gyp-files-in-the-wild
GYP 官方文档:
https://gyp.gsrc.io/docs/UserDocumentation.md
第一个 API
现在新建main.cpp,在该文件中定义我们的第一个 API,API 名为Add,支持传入 2 个整数参数,返回整数相加的和。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <napi.h>
Napi::Number Add(const Napi::CallbackInfo& info) { Napi::Env env = info.Env();
if (info.Length() != 2) throw Napi::TypeError::New(env, "Wrong number of arguments");
if (!info[0].IsNumber() || !info[1].IsNumber()) throw Napi::TypeError::New(env, "Wrong arguments");
const int ret = info[0].ToNumber().Int32Value() + info[1].ToNumber().Int32Value();
return Napi::Number::New(env, ret); }
|
在定义完 API 之后,还需要将 API 导出,在文件末尾添加如下代码:
1 2 3 4 5 6 7 8
| Napi::Object Init(Napi::Env env, Napi::Object exports) { exports.Set(Napi::String::New(env, "Add"), Napi::Function::New(env, Add));
return exports; }
NODE_API_MODULE(addon, Init)
|
如果忘记导出 API,加载 Node 插件时会报错:
1
| Error: Module did not self-register: '\\?\D:\node-addson-sample\build\Debug\node-addson-sample.node'.
|
现在执行npm run build-debug编译 Debug 版本插件,编译生成的 node 插件路径为build\Debug\node-addson-sample.node。
测试用例
新建test.js,测试代码如下:
1 2 3
| const sample = require("bindings")("node-addson-sample.node");
console.log(sample.Add(100, 200));
|
使用bindings模块可以不用考虑插件的具体位置,该模块会自动帮我们在项目目录下遍历查找。
数据类型
在napi.h头文件中有很多继承自Napi::Value的子类,这些类分别对应 JavaScript 中的数据类型,如:
- Napi::Boolean -> Boolean
- Napi::Number -> Number
- Napi::String -> String
- Napi::Function -> Function
- Napi::Symbol -> Symbol
- Napi::Array -> Array
- Napi::Object -> Object
Node-Api 还定义 Promise、Date、Buffer 等数据类型。
Null 和 Undefined
Null 和 Undefined 比较特殊,没有定义专门的类,由Env类的成员函数返回。
1 2 3
| env.Null()
env.Undefined()
|
创建对象
有两种方式可以用来创建指定类型的对象,以创建 Boolean 类型为例:
1 2
| Napi::Boolean::New(env, true) Napi::Value::From(env, false)
|
以创建一个对象数组为例介绍对象和数组的使用方法:
1 2 3 4 5 6 7 8
| Napi::Array result = Napi::Array::New(env); for (size_t i = 0; i < 3; i++) { Napi::Object obj = Napi::Object::New(env); obj.Set(Napi::String::New(env, "filePath"), Napi::String::New(env, "/root/" + std::to_string(i) + ".txt")); obj.Set(Napi::String::New(env, "fileSize"), Napi::Number::New(env, i * 100));
result.Set(Napi::Number::New(env, i), obj); }
|
类型校验
Napi::Value提供了若干方法用于判断当前对象是否为指定类型,如:
- IsUndefined
- IsNull
- IsBoolean
- IsNumber
- IsString
- IsSymbol
- IsArray
- IsObject
- IsFunction
- IsPromise
- IsBuffer
异常
可以在编译脚本binding.gyp中通过预编译宏指定是否启用 C++异常:
1 2 3
| NAPI_CPP_EXCEPTIONS
NAPI_DISABLE_CPP_EXCEPTIONS
|
如果启用 C++异常,则Napi::Error会继承自 std::exception。
1 2 3 4 5 6
| class Error : public ObjectReference #ifdef NAPI_CPP_EXCEPTIONS , public std::exception #endif ...
|
在启动 C++异常的情况下,从 Node 插件抛出异常的方式如下:
1
| throw Napi::TypeError::New(env, "Wrong number of arguments");
|
将会中断当前函数 throw 后面代码的执行。
TypeError 继承自 Error,通常用于表示与类型错误相关的异常。类似的错误类型还有RangeError等,也可以直接抛出Error类型的错误:
1
| throw Napi::Error::New(env, "Wrong number of arguments");
|
在没有启用 C++ 异常的情况下,采用如下方式从 Node 插件抛出异常:
1 2
| Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException(); return;
|
抛出异常后需要使用 return 语句终止下面流程的执行。
Node-API 官方文档:node-addon-api doc
Node.js 官方 addon 示例:node-addon-examples
📌限于篇幅,不在此提供完整的示例代码,如需获取完整的示例代码,可以联系我