Node插件开发(1)-快速入门

在使用 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安装依赖项。

各个依赖项的作用如下:

  • node-addon-api用于提供了 Node-API 相关的头文件;

  • bindings用于帮助插件开发者快速导入编译后的.node 插件,方便调试,这个依赖是非必须;

build-debugbuild-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)); // 输出300

使用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 // NAPI_CPP_EXCEPTIONS
...

在启动 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

📌限于篇幅,不在此提供完整的示例代码,如需获取完整的示例代码,可以联系我