• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Node-API开发规范
2
3## 获取JS传入参数及其数量
4
5**【规则】** 当传入napi_get_cb_info的argv不为nullptr时,argv的长度必须大于等于传入argc声明的大小。
6
7当argv不为nullptr时,napi_get_cb_info会根据argc声明的数量将JS实际传入的参数写入argv。如果argc小于等于实际JS传入参数的数量,该接口仅会将声明的argc数量的参数写入argv;而当argc大于实际参数数量时,该接口会在argv的尾部填充undefined。
8
9**错误示例**
10
11```cpp
12static napi_value IncorrectDemo1(napi_env env, napi_callbackk_info info) {
13    // argc 未正确的初始化,其值为不确定的随机值,导致 argv 的长度可能小于 argc 声明的数量,数据越界。
14    size_t argc;
15    napi_value argv[10] = {nullptr};
16    napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
17    return nullptr;
18}
19
20static napi_value IncorrectDemo2(napi_env env, napi_callback_info info) {
21    // argc 声明的数量大与 argv 实际初始化的长度,导致 napi_get_cb_info 接口在写入 argv 时数据越界。
22    size_t argc = 5;
23    napi_value argv[3] = {nullptr};
24    napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
25    return nullptr;
26}
27```
28
29**正确示例**
30
31```cpp
32static napi_value GetArgvDemo1(napi_env env, napi_callback_info info) {
33    size_t argc = 0;
34    // argv 传入 nullptr 来获取传入参数真实数量
35    napi_get_cb_info(env, info, &argc, nullptr, nullptr, nullptr);
36    // JS 传入参数为0,不执行后续逻辑
37    if (argc == 0) {
38        return nullptr;
39    }
40    // 创建数组用以获取JS传入的参数
41    napi_value* argv = new napi_value[argc];
42    napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
43    // 业务代码
44    // ... ...
45    // argv 为 new 创建的对象,在使用完成后手动释放
46    delete argv;
47    return nullptr;
48}
49
50static napi_value GetArgvDemo2(napi_env env, napi_callback_info info) {
51    size_t argc = 2;
52    napi_value* argv[2] = {nullptr};
53    // napi_get_cb_info 会向 argv 中写入 argc 个 JS 传入参数或 undefined
54    napi_get_cb_info(env, info, &argc, nullptr, nullptr, nullptr);
55    // 业务代码
56    // ... ...
57    return nullptr;
58}
59```
60
61## 生命周期管理
62
63**【规则】** 合理使用napi_open_handle_scope和napi_close_handle_scope管理napi_value的生命周期,做到生命周期最小化,避免发生内存泄漏问题。
64
65每个napi_value属于特定的HandleScope,HandleScope通过napi_open_handle_scope和napi_close_handle_scope来建立和关闭,HandleScope关闭后,所属的napi_value就会自动释放。
66
67**正确示例**:
68
69```cpp
70// 在for循环中频繁调用napi接口创建js对象时,要加handle_scope及时释放不再使用的资源。
71// 下面例子中,每次循环结束局部变量res的生命周期已结束,因此加scope及时释放其持有的js对象,防止内存泄漏
72for (int i = 0; i < 100000; i++) {
73    napi_handle_scope scope = nullptr;
74    napi_open_handle_scope(env, &scope);
75    if (scope == nullptr) {
76        return;
77    }
78    napi_value res;
79    napi_create_object(env, &res);
80    napi_close_handle_scope(env, scope);
81}
82```
83
84## 上下文敏感
85
86**【规则】** 多引擎实例场景下,禁止通过Node-API跨引擎实例访问JS对象。
87
88引擎实例是一个独立运行环境,JS对象创建访问等操作必须在同一个引擎实例中进行。若在不同引擎实例中操作同一个对象,可能会引发程序崩溃。引擎实例在接口中体现为napi_env。
89
90**错误示例**:
91
92```cpp
93// 线程1执行,在env1创建string对象,值为"bar"、
94napi_create_string_utf8(env1, "bar", NAPI_AUTO_LENGTH, &string);
95// 线程2执行,在env2创建object对象,并将上述的string对象设置到object对象中
96napi_status status = napi_create_object(env2, &object);
97if (status != napi_ok) {
98    napi_throw_error(env, ...);
99    return;
100}
101
102status = napi_set_named_property(env2, object, "foo", string);
103if (status != napi_ok) {
104    napi_throw_error(env, ...);
105    return;
106}
107```
108
109所有的JS对象都隶属于具体的某一napi_env,不可将env1的对象,设置到env2中的对象中。在env2中一旦访问到env1的对象,程序可能会发生崩溃。
110
111## 异常处理
112
113**【建议】** Node-API接口调用发生异常需要及时处理,不能遗漏异常到后续逻辑,否则程序可能发生不可预期行为。
114
115**正确示例**:
116
117```cpp
118// 1.创建对象
119napi_status status = napi_create_object(env, &object);
120if (status != napi_ok) {
121    napi_throw_error(env, ...);
122    return;
123}
124// 2.创建属性值
125status = napi_create_string_utf8(env, "bar", NAPI_AUTO_LENGTH, &string);
126if (status != napi_ok) {
127    napi_throw_error(env, ...);
128    return;
129}
130// 3.将步骤2的结果设置为对象object属性foo的值
131status = napi_set_named_property(env, object, "foo", string);
132if (status != napi_ok) {
133    napi_throw_error(env, ...);
134    return;
135}
136```
137
138如上示例中,步骤1或者步骤2出现异常时,步骤3都不会正常进行。只有当方法的返回值是napi_ok时,才能保持继续正常运行;否则后续流程可能会出现不可预期的行为。
139
140## 异步任务
141
142**【规则】** 当使用uv_queue_work方法将任务抛到JS线程上面执行的时候,对JS线程的回调方法,一般情况下需要加上napi_handle_scope来管理回调方法创建的napi_value的生命周期。
143
144使用uv_queue_work方法,不会走Node-API框架,此时需要开发者自己合理使用napi_handle_scope来管理napi_value的生命周期。
145
146**正确示例**:
147
148```cpp
149void callbackTest(CallbackContext* context)
150{
151    uv_loop_s* loop = nullptr;
152    napi_get_uv_event_loop(context->env, &loop);
153    uv_work_t* work = new uv_work_t;
154    context->retData = 1;
155    work->data = (void*)context;
156    uv_queue_work(
157        loop, work, [](uv_work_t* work) {},
158        // using callback function back to JS thread
159        [](uv_work_t* work, int status) {
160            CallbackContext* context = (CallbackContext*)work->data;
161            napi_handle_scope scope = nullptr; napi_open_handle_scope(context->env, &scope);
162            if (scope == nullptr) {
163                return;
164            }
165            napi_value callback = nullptr;
166            napi_get_reference_value(context->env, context->callbackRef, &callback);
167            napi_value retArg;
168            napi_create_int32(context->env, context->retData, &retArg);
169            napi_value ret;
170            napi_call_function(context->env, nullptr, callback, 1, &retArg, &ret);
171            napi_delete_reference(context->env, context->callbackRef);
172            napi_close_handle_scope(context->env, scope);
173            if (work != nullptr) {
174                delete work;
175            }
176            delete context;
177        }
178    );
179}
180```
181
182## 对象绑定
183
184**【规则】** 使用napi_wrap接口,如果最后一个参数result传递不为nullptr,需要开发者在合适的时机调用napi_remove_wrap函数主动删除创建的napi_ref。
185
186napi_wrap接口定义如下:
187
188```cpp
189napi_wrap(napi_env env, napi_value js_object, void* native_object, napi_finalize finalize_cb, void* finalize_hint, napi_ref* result)
190```
191
192当最后一个参数result不为空时,框架会创建一个napi_ref对象,指向js_object。此时开发者需要自己管理js_object的生命周期,即需要在合适的时机调用napi_remove_wrap删除napi_ref,这样GC才能正常释放js_object,从而触发绑定C++对象native_object的析构函数finalize_cb。
193
194一般情况下,根据业务情况最后一个参数result可以直接传递为nullptr。
195
196**正确示例**:
197
198```cpp
199// 用法1:napi_wrap不需要接收创建的napi_ref,最后一个参数传递nullptr,创建的napi_ref是弱引用,由系统管理,不需要用户手动释放
200napi_wrap(env, jsobject, nativeObject, cb, nullptr, nullptr);
201
202// 用法2:napi_wrap需要接收创建的napi_ref,最后一个参数不为nullptr,返回的napi_ref是强引用,需要用户手动释放,否则会内存泄漏
203napi_ref result;
204napi_wrap(env, jsobject, nativeObject, cb, nullptr, &result);
205// 当js_object和result后续不再使用时,及时调用napi_remove_wrap释放result
206napi_value result1;
207napi_remove_wrap(env, jsobject, result1);
208```
209
210## 高性能数组
211
212**【建议】** 存储值类型数据时,使用ArrayBuffer代替JSArray来提高应用性能。
213
214使用JSArray作为容器储存数据,支持几乎所有的JS数据类型。
215
216使用napi_set_element方法对JSArray存储值类型数据(如int32)时,同样会涉及到与运行时的交互,造成不必要的开销。
217
218ArrayBuffer进行增改是直接对缓冲区进行更改,具有远优于使用napi_set_element操作JSArray的性能表现。
219
220因此此种场景下,更推荐使用napi_create_arraybuffer接口创建的ArrayBuffer对象。
221
222**示例:**
223
224```cpp
225// 以下代码使用常规JSArray作为容器,但其仅存储int32类型数据。
226// 但因为是JS对象,因此只能使用napi方法对其进行增改,性能较低。
227static napi_value ArrayDemo(napi_env env, napi_callback_info info)
228{
229    constexpr size_t arrSize = 1000;
230    napi_value jsArr = nullptr;
231    napi_create_array(env, &jsArr);
232    for (int i = 0; i < arrSize; i++) {
233        napi_value arrValue = nullptr;
234        napi_create_int32(env, i, &arrValue);
235        // 常规JSArray使用napi方法对array进行读写,性能较差。
236        napi_set_element(env, jsArr, i, arrValue);
237    }
238    return jsArr;
239}
240
241// 推荐写法:
242// 同样以int32类型数据为例,但以下代码使用ArrayBuffer作为容器。
243// 因此可以使用C/C++的方法直接对缓冲区进行增改。
244static napi_value ArrayBufferDemo(napi_env env, napi_callback_info info)
245{
246    constexpr size_t arrSize = 1000;
247    napi_value arrBuffer = nullptr;
248    void* data = nullptr;
249
250    napi_create_arraybuffer(env, arrSize * sizeof(int32_t), &data, &arrBuffer);
251    int32_t* i32Buffer = reinterpret_cast<int32_t*>(data);
252    for (int i = 0; i < arrSize; i++) {
253        // arrayBuffer直接对缓冲区进行修改,跳过运行时,
254        // 与操作原生C/C++对象性能相当
255        i32Buffer[i] = i;
256    }
257
258    return arrBuffer;
259}
260```
261
262napi_create_arraybuffer等同于JS代码中的`new ArrayBuffer(size)`,其生成的对象不可直接在TS/JS中进行读取,需要将其包装为TyppedArray或DataView后方可进行读写。
263
264**基准性能测试结果如下:**
265
266> **说明:** 以下数据为千次循环写入累计数据,为更好的体现出差异,已对设备核心频率进行限制。
267
268| 容器类型    | Benchmark数据(us) |
269| ----------- | ------------------- |
270| JSArray     | 1566.174            |
271| ArrayBuffer | 3.609               |
272
273## 数据转换
274
275**【建议】** 尽可能的减少数据转换次数,避免不必要的复制。
276
277- **减少数据转换次数:** 频繁的数据转换可能会导致性能下降,可以通过批量处理数据或者使用更高效的数据结构来优化性能;
278- **避免不必要的数据复制:** 在进行数据转换时,可以使用Node-API提供的接口来直接访问原始数据,而不是创建新的副本;
279- **使用缓存:** 如果某些数据在多次转换中都会被使用到,可以考虑使用缓存来避免重复的数据转换。缓存可以减少不必要的计算,提高性能。
280
281## 模块注册与模块命名
282
283**【规则】**
284nm_register_func对应的函数需要加上修饰符static,防止与其他so里的符号冲突。
285
286模块注册的入口,即使用__attribute__((constructor))修饰函数的函数名需要确保与其他模块不同。
287
288模块实现中.nm_modname字段需要与模块名完全匹配,区分大小写。
289
290**错误示例**
291以下代码为模块名为nativerender时的错误示例
292```cpp
293EXTERN_C_START
294napi_value Init(napi_env env, napi_value exports)
295{
296    // ...
297    return exports;
298}
299EXTERN_C_END
300
301static napi_module nativeModule = {
302    .nm_version = 1,
303    .nm_flags = 0,
304    .nm_filename = nullptr,
305    //没有在nm_register_func对应的函数加上static
306    .nm_register_func = Init,
307    // 模块实现中.nm_modname字段没有与模块名完全匹配,会导致多线程场景模块加载失败
308    .nm_modname = "entry",
309    .nm_priv = nullptr,
310    .reserved = { 0 },
311};
312
313//模块注册的入口函数名为RegisterModule,容易与其他模块重复。
314extern "C" __attribute__((constructor)) void RegisterModule()
315{
316    napi_module_register(&nativeModule);
317}
318```
319
320**正确示例**:
321以下代码为模块名为nativerender时的正确示例
322```cpp
323EXTERN_C_START
324static napi_value Init(napi_env env, napi_value exports)
325{
326    // ...
327    return exports;
328}
329EXTERN_C_END
330
331static napi_module nativeModule = {
332    .nm_version = 1,
333    .nm_flags = 0,
334    .nm_filename = nullptr,
335    .nm_register_func = Init,
336    .nm_modname = "nativerender",
337    .nm_priv = nullptr,
338    .reserved = { 0 },
339};
340
341extern "C" __attribute__((constructor)) void RegisterNativeRenderModule()
342{
343    napi_module_register(&nativeModule);
344}
345```
346
347## 其它
348
349**【规则】** 使用napi_get_arraybuffer_info接口,第三个参数data资源开发者不允许释放,data的生命周期受引擎管理。
350
351napi_get_arraybuffer_info接口定义如下:
352
353```cpp
354napi_get_arraybuffer_info(napi_env env, napi_value arraybuffer, void** data, size_t* byte_length)
355```
356
357data获取的是ArrayBuffer的Buffer头指针,开发者只可以在范围内读写该Buffer区域,不可以进行释放操作。该段内存由引擎内部的ArrayBuffer Allocator管理,随JS对象ArrayBuffer的生命周期释放。
358
359**错误示例:**
360
361```cpp
362void* arrayBufferPtr = nullptr;
363napi_value arrayBuffer = nullptr;
364size_t createBufferSize = ARRAY_BUFFER_SIZE;
365napi_status verification = napi_create_arraybuffer(env, createBufferSize, &arrayBufferPtr, &arrayBuffer);
366size_t arrayBufferSize;
367napi_status result = napi_get_arraybuffer_info(env, arrayBuffer, &arrayBufferPtr, &arrayBufferSize);
368delete arrayBufferPtr; // 这一步是禁止的,创建的arrayBufferPtr生命周期由引擎管理,不允许用户自己delete,否则会double free
369```
370
371**【建议】** 合理使用napi_object_freeze和napi_object_seal来控制对象以及对象属性的可变性。
372
373napi_object_freeze等同于Object.freeze语义,freeze后对象的所有属性都不可能以任何方式被修改;napi_object_seal等同于Object.seal语义,对象不可增删属性。两者的主要区别是,freeze不能改属性的值,seal还可以改属性的值。
374
375开发者使用以上语义时,需确保约束条件是自己需要的,一旦违背以上语义严格模式下就会抛出Error(默认严格模式)。
376