1# Node-API开发规范 2<!--Kit: NDK--> 3<!--Subsystem: arkcompiler--> 4<!--Owner: @xliu-huanwei; @shilei123; @huanghello--> 5<!--Designer: @shilei123--> 6<!--Tester: @kirl75; @zsw_zhushiwei--> 7<!--Adviser: @fang-jinxu--> 8 9## 获取JS传入参数及其数量 10 11**【规则】** 当传入napi_get_cb_info的argv不为nullptr时,argv的长度必须大于等于传入argc声明的大小。 12 13当argv不为nullptr时,napi_get_cb_info会根据argc声明的数量将JS实际传入的参数写入argv。如果argc小于等于实际JS传入参数的数量,该接口仅会将声明的argc数量的参数写入argv;而当argc大于实际参数数量时,该接口会在argv的尾部填充undefined。 14 15**错误示例** 16 17```cpp 18static napi_value IncorrectDemo1(napi_env env, napi_callback_info info) { 19 // argc 未正确的初始化,其值为不确定的随机值,导致 argv 的长度可能小于 argc 声明的数量,数据越界。 20 size_t argc; 21 napi_value argv[10] = {nullptr}; 22 napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); 23 return nullptr; 24} 25 26static napi_value IncorrectDemo2(napi_env env, napi_callback_info info) { 27 // argc 声明的数量大于 argv 实际初始化的长度,导致 napi_get_cb_info 接口在写入 argv 时数据越界。 28 size_t argc = 5; 29 napi_value argv[3] = {nullptr}; 30 napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); 31 return nullptr; 32} 33``` 34 35**正确示例** 36 37```cpp 38static napi_value GetArgvDemo1(napi_env env, napi_callback_info info) { 39 size_t argc = 0; 40 // argv 传入 nullptr 来获取传入参数真实数量 41 napi_get_cb_info(env, info, &argc, nullptr, nullptr, nullptr); 42 // JS 传入参数为0,不执行后续逻辑 43 if (argc == 0) { 44 return nullptr; 45 } 46 // 创建数组用以获取JS传入的参数 47 napi_value* argv = new napi_value[argc]; 48 napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); 49 // 业务代码 50 // ... ... 51 // argv 为 new 创建的对象,在使用完成后手动释放 52 delete[] argv; 53 return nullptr; 54} 55 56static napi_value GetArgvDemo2(napi_env env, napi_callback_info info) { 57 size_t argc = 2; 58 napi_value argv[2] = {nullptr}; 59 // napi_get_cb_info 会向 argv 中写入 argc 个 JS 传入参数或 undefined 60 napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr); 61 // 业务代码 62 // ... ... 63 return nullptr; 64} 65``` 66 67## 生命周期管理 68 69**【规则】** 合理使用napi_open_handle_scope和napi_close_handle_scope管理napi_value的生命周期,做到生命周期最小化,避免发生内存泄漏问题。 70 71每个napi_value属于特定的HandleScope,HandleScope通过napi_open_handle_scope和napi_close_handle_scope来建立和关闭,HandleScope关闭后,所属的napi_value就会自动释放。 72 73**正确示例**: 74 75```cpp 76// 在for循环中频繁调用napi接口创建js对象时,要加handle_scope及时释放不再使用的资源。 77// 下面例子中,每次循环结束局部变量res的生命周期已结束,因此加scope及时释放其持有的js对象,防止内存泄漏 78for (int i = 0; i < 100000; i++) { 79 napi_handle_scope scope = nullptr; 80 napi_open_handle_scope(env, &scope); 81 if (scope == nullptr) { 82 return; 83 } 84 napi_value res; 85 napi_create_object(env, &res); 86 napi_close_handle_scope(env, scope); 87} 88``` 89 90## 上下文敏感 91 92**【规则】** 多引擎实例场景下,禁止通过Node-API跨引擎实例访问JS对象。 93 94引擎实例是一个独立运行环境,JS对象创建访问等操作必须在同一个引擎实例中进行。若在不同引擎实例中操作同一个对象,可能会引发程序崩溃。引擎实例在接口中体现为napi_env。 95 96**错误示例**: 97 98```cpp 99// 线程1执行,在env1创建string对象,值为"bar"、 100napi_create_string_utf8(env1, "bar", NAPI_AUTO_LENGTH, &string); 101// 线程2执行,在env2创建object对象,并将上述的string对象设置到object对象中 102napi_status status = napi_create_object(env2, &object); 103if (status != napi_ok) { 104 napi_throw_error(env, ...); 105 return; 106} 107 108status = napi_set_named_property(env2, object, "foo", string); 109if (status != napi_ok) { 110 napi_throw_error(env, ...); 111 return; 112} 113``` 114 115所有的JS对象都隶属于具体的某一napi_env,不可将env1的对象,设置到env2中的对象中。在env2中一旦访问到env1的对象,程序可能会发生崩溃。 116 117## 异常处理 118 119**【建议】** Node-API接口调用发生异常需要及时处理,不能遗漏异常到后续逻辑,否则程序可能发生不可预期行为。 120 121**正确示例**: 122 123```cpp 124// 1.创建对象 125napi_status status = napi_create_object(env, &object); 126if (status != napi_ok) { 127 napi_throw_error(env, ...); 128 return; 129} 130// 2.创建属性值 131status = napi_create_string_utf8(env, "bar", NAPI_AUTO_LENGTH, &string); 132if (status != napi_ok) { 133 napi_throw_error(env, ...); 134 return; 135} 136// 3.将步骤2的结果设置为对象object属性foo的值 137status = napi_set_named_property(env, object, "foo", string); 138if (status != napi_ok) { 139 napi_throw_error(env, ...); 140 return; 141} 142``` 143 144如上示例中,步骤1或者步骤2出现异常时,步骤3都不会正常进行。只有当方法的返回值是napi_ok时,才能保持继续正常运行;否则后续流程可能会出现不可预期的行为。 145 146## 异步任务 147 148**【规则】** 当使用uv_queue_work方法将任务抛到JS线程上面执行的时候,对JS线程的回调方法,一般情况下需要加上napi_handle_scope来管理回调方法创建的napi_value的生命周期。 149 150使用uv_queue_work方法,不会走Node-API框架,此时需要开发者自己合理使用napi_handle_scope来管理napi_value的生命周期。 151 152> **说明** 153> 154> 本规则旨在强调napi_value生命周期情况,若只想往JS线程抛任务,**不推荐**使用uv_queue_work方法。如有抛任务的需要,请使用[napi_threadsafe_function系列](./use-napi-thread-safety.md)接口。 155 156**正确示例**: 157 158```cpp 159void CallbackTest(CallbackContext* context) 160{ 161 uv_loop_s* loop = nullptr; 162 napi_get_uv_event_loop(context->env, &loop); 163 uv_work_t* work = new uv_work_t; 164 context->retData = 1; 165 work->data = (void*)context; 166 uv_queue_work( 167 loop, work, 168 // 请注意,uv_queue_work会创建一个线程并执行该回调函数,若开发者只想往JS线程抛任务,不推荐使用uv_queue_work,以避免冗余的线程创建 169 [](uv_work_t* work) { 170 // 执行一些业务逻辑 171 }, 172 // 该回调会执行在loop所在的JS线程上 173 [](uv_work_t* work, int status) { 174 CallbackContext* context = (CallbackContext*)work->data; 175 napi_handle_scope scope = nullptr; 176 napi_open_handle_scope(context->env, &scope); 177 if (scope == nullptr) { 178 if (work != nullptr) { 179 delete work; 180 } 181 return; 182 } 183 napi_value callback = nullptr; 184 napi_get_reference_value(context->env, context->callbackRef, &callback); 185 napi_value retArg; 186 napi_create_int32(context->env, context->retData, &retArg); 187 napi_value ret; 188 napi_call_function(context->env, nullptr, callback, 1, &retArg, &ret); 189 napi_delete_reference(context->env, context->callbackRef); 190 napi_close_handle_scope(context->env, scope); 191 if (work != nullptr) { 192 delete work; 193 } 194 delete context; 195 } 196 ); 197} 198``` 199 200## 对象绑定 201 202**【规则】** 使用napi_wrap接口,如果最后一个参数result传递不为nullptr,需要开发者在合适的时机调用napi_remove_wrap函数主动删除创建的napi_ref。 203 204napi_wrap接口定义如下: 205 206```cpp 207napi_wrap(napi_env env, napi_value js_object, void* native_object, napi_finalize finalize_cb, void* finalize_hint, napi_ref* result) 208``` 209 210当最后一个参数result不为空时,框架会创建一个napi_ref对象,指向js_object。此时开发者需要自己管理js_object的生命周期,即需要在合适的时机调用napi_remove_wrap删除napi_ref,这样GC才能正常释放js_object,从而触发绑定C++对象native_object的析构函数finalize_cb。 211 212一般情况下,根据业务情况最后一个参数result可以直接传递为nullptr。 213 214**正确示例**: 215 216```cpp 217// 用法1:napi_wrap不需要接收创建的napi_ref,最后一个参数传递nullptr,创建的napi_ref是弱引用,由系统管理,不需要用户手动释放 218napi_wrap(env, jsobject, nativeObject, cb, nullptr, nullptr); 219 220// 用法2:napi_wrap需要接收创建的napi_ref,最后一个参数不为nullptr,返回的napi_ref是强引用,需要用户手动释放,否则会内存泄漏 221napi_ref result; 222napi_wrap(env, jsobject, nativeObject, cb, nullptr, &result); 223// 当js_object和result后续不再使用时,及时调用napi_remove_wrap释放result 224void* nativeObjectResult = nullptr; 225napi_remove_wrap(env, jsobject, &nativeObjectResult); 226``` 227 228## 高性能数组 229 230**【建议】** 存储值类型数据时,使用ArrayBuffer代替JSArray来提高应用性能。 231 232使用JSArray作为容器储存数据,支持几乎所有的JS数据类型。 233 234使用napi_set_element方法对JSArray存储值类型数据(如int32)时,同样会涉及到与运行时的交互,造成不必要的开销。 235 236ArrayBuffer进行增改是直接对缓冲区进行更改,具有远优于使用napi_set_element操作JSArray的性能表现。 237 238因此此种场景下,更推荐使用napi_create_arraybuffer接口创建的ArrayBuffer对象。 239 240**示例:** 241 242```cpp 243// 以下代码使用常规JSArray作为容器,但其仅存储int32类型数据。 244// 但因为是JS对象,因此只能使用napi方法对其进行增改,性能较低。 245static napi_value ArrayDemo(napi_env env, napi_callback_info info) 246{ 247 constexpr size_t arrSize = 1000; 248 napi_value jsArr = nullptr; 249 napi_create_array(env, &jsArr); 250 for (int i = 0; i < arrSize; i++) { 251 napi_value arrValue = nullptr; 252 napi_create_int32(env, i, &arrValue); 253 // 常规JSArray使用napi方法对array进行读写,性能较差。 254 napi_set_element(env, jsArr, i, arrValue); 255 } 256 return jsArr; 257} 258 259// 推荐写法: 260// 同样以int32类型数据为例,但以下代码使用ArrayBuffer作为容器。 261// 因此可以使用C/C++的方法直接对缓冲区进行增改。 262static napi_value ArrayBufferDemo(napi_env env, napi_callback_info info) 263{ 264 constexpr size_t arrSize = 1000; 265 napi_value arrBuffer = nullptr; 266 void* data = nullptr; 267 268 napi_create_arraybuffer(env, arrSize * sizeof(int32_t), &data, &arrBuffer); 269 // data为空指针,取消对data进行写入 270 if (data == nullptr) { 271 return arrBuffer; 272 } 273 int32_t* i32Buffer = reinterpret_cast<int32_t*>(data); 274 for (int i = 0; i < arrSize; i++) { 275 // arrayBuffer直接对缓冲区进行修改,跳过运行时, 276 // 与操作原生C/C++对象性能相当 277 i32Buffer[i] = i; 278 } 279 280 return arrBuffer; 281} 282``` 283 284napi_create_arraybuffer等同于JS代码中的`new ArrayBuffer(size)`,其生成的对象不可直接在TS/JS中进行读取,需要将其包装为TyppedArray或DataView后方可进行读写。 285 286**基准性能测试结果如下:** 287 288> **说明:** 289> 290> 以下数据为千次循环写入累计数据,为更好的体现出差异,已对设备核心频率进行限制。 291 292| 容器类型 | Benchmark数据(us) | 293| ----------- | ------------------- | 294| JSArray | 1566.174 | 295| ArrayBuffer | 3.609 | 296 297## 数据转换 298 299**【建议】** 尽可能的减少数据转换次数,避免不必要的复制。 300 301- **减少数据转换次数:** 频繁的数据转换可能会导致性能下降,可以通过批量处理数据或者使用更高效的数据结构来优化性能。 302- **避免不必要的数据复制:** 在进行数据转换时,可以使用Node-API提供的接口来直接访问原始数据,而不是创建新的副本。 303- **使用缓存:** 如果某些数据在多次转换中都会被使用到,可以考虑使用缓存来避免重复的数据转换。缓存可以减少不必要的计算,提高性能。 304 305## 模块注册与模块命名 306 307**【规则】** 308nm_register_func对应的函数需要加上修饰符static,防止与其他so里的符号冲突。 309 310模块注册的入口,即使用__attribute__((constructor))修饰函数的函数名需要确保与其他模块不同。 311 312模块实现中.nm_modname字段需要与模块名完全匹配,区分大小写。 313 314**错误示例** 315以下代码为模块名为nativerender时的错误示例 316 317```cpp 318EXTERN_C_START 319napi_value Init(napi_env env, napi_value exports) 320{ 321 // ... 322 return exports; 323} 324EXTERN_C_END 325 326static napi_module nativeModule = { 327 .nm_version = 1, 328 .nm_flags = 0, 329 .nm_filename = nullptr, 330 //没有在nm_register_func对应的函数加上static 331 .nm_register_func = Init, 332 // 模块实现中.nm_modname字段没有与模块名完全匹配,会导致多线程场景模块加载失败 333 .nm_modname = "entry", 334 .nm_priv = nullptr, 335 .reserved = { 0 }, 336}; 337 338//模块注册的入口函数名为RegisterModule,容易与其他模块重复。 339extern "C" __attribute__((constructor)) void RegisterModule() 340{ 341 napi_module_register(&nativeModule); 342} 343``` 344 345**正确示例**: 346以下代码为模块名为nativerender时的正确示例 347 348```cpp 349EXTERN_C_START 350static napi_value Init(napi_env env, napi_value exports) 351{ 352 // ... 353 return exports; 354} 355EXTERN_C_END 356 357static napi_module nativeModule = { 358 .nm_version = 1, 359 .nm_flags = 0, 360 .nm_filename = nullptr, 361 .nm_register_func = Init, 362 .nm_modname = "nativerender", 363 .nm_priv = nullptr, 364 .reserved = { 0 }, 365}; 366 367extern "C" __attribute__((constructor)) void RegisterNativeRenderModule() 368{ 369 napi_module_register(&nativeModule); 370} 371``` 372 373## dlopen与模块注册 374 375**【规则】** 376如果注册的模块事先有被dlopen,需使用以下方式注册模块。 377 378模块需对外导出固定名称为napi_onLoad的函数,在该函数内调用注册函数。napi_onLoad函数只会在ArkTS代码的import语句中被主动调用,从而避免dlopen时提前触发模块的注册。 379 380**示例** 381 382```cpp 383EXTERN_C_START 384static napi_value Init(napi_env env, napi_value exports) 385{ 386 // ... 387 return exports; 388} 389EXTERN_C_END 390 391static napi_module nativeModule = { 392 .nm_version = 1, 393 .nm_flags = 0, 394 .nm_filename = nullptr, 395 .nm_register_func = Init, 396 .nm_modname = "nativerender", 397 .nm_priv = nullptr, 398 .reserved = { 0 }, 399}; 400 401extern "C" void napi_onLoad() 402{ 403 napi_module_register(&nativeModule); 404} 405``` 406 407## 正确的使用napi_create_external系列接口创建的JS Object 408 409**【规则】** napi_create_external系列接口创建出来的JS对象仅允许在当前线程传递和使用,跨线程传递(如使用worker的post_message)将会导致应用crash。若需跨线程传递绑定有Native对象的JS对象,请使用napi_coerce_to_native_binding_object接口绑定JS对象和Native对象。 410 411**错误示例** 412 413```cpp 414static void MyFinalizeCB(napi_env env, void *finalize_data, void *finalize_hint) { return; } 415 416static napi_value CreateMyExternal(napi_env env, napi_callback_info info) { 417 napi_value result = nullptr; 418 napi_create_external(env, nullptr, MyFinalizeCB, nullptr, &result); 419 return result; 420} 421 422// 此处已省略模块注册的代码,你可能需要自行注册 CreateMyExternal 方法 423``` 424 425```ts 426// index.d.ts 427export const createMyExternal: () => Object; 428 429// 应用代码 430import testNapi from 'libentry.so'; 431import worker from '@ohos.worker'; 432 433const mWorker = new worker.ThreadWorker('../workers/Worker'); 434 435{ 436 const mExternalObj = testNapi.createMyExternal(); 437 438 mWorker.postMessage(mExternalObj); 439 440} 441 442// 关闭worker线程 443// 应用可能在此步骤崩溃,或在后续引擎进行GC的时候崩溃 444mWorker.terminate(); 445// Worker的实现为默认模板,此处省略 446``` 447 448## 防止重复释放获取的buffer 449 450**【规则】** 使用napi_get_arraybuffer_info等接口,参数data资源开发者不允许释放,data的生命周期受引擎管理。 451 452这里以napi_get_arraybuffer_info为例,该接口定义如下: 453 454```cpp 455napi_get_arraybuffer_info(napi_env env, napi_value arraybuffer, void** data, size_t* byte_length) 456``` 457 458data获取的是ArrayBuffer的Buffer头指针,开发者只可以在范围内读写该Buffer区域,不可以进行释放操作。该段内存由引擎内部的ArrayBuffer Allocator管理,随JS对象ArrayBuffer的生命周期释放。 459 460**错误示例:** 461 462```cpp 463void* arrayBufferPtr = nullptr; 464napi_value arrayBuffer = nullptr; 465size_t createBufferSize = ARRAY_BUFFER_SIZE; 466napi_status verification = napi_create_arraybuffer(env, createBufferSize, &arrayBufferPtr, &arrayBuffer); 467size_t arrayBufferSize; 468napi_status result = napi_get_arraybuffer_info(env, arrayBuffer, &arrayBufferPtr, &arrayBufferSize); 469delete arrayBufferPtr; // 这一步是禁止的,创建的arrayBufferPtr生命周期由引擎管理,不允许用户自己delete,否则会double free 470``` 471 472|Node-API中受当前规则约束的接口有:| 473|----------------------------------| 474| napi_create_arraybuffer | 475| napi_create_sendable_arraybuffer | 476| napi_get_arraybuffer_info | 477| napi_create_buffer | 478| napi_get_buffer_info | 479| napi_get_typedarray_info | 480| napi_get_dataview_info | 481 482## 其他 483 484**【建议】** 合理使用napi_object_freeze和napi_object_seal来控制对象以及对象属性的可变性。 485 486napi_object_freeze等同于Object.freeze语义,freeze后对象的所有属性都不可能以任何方式被修改;napi_object_seal等同于Object.seal语义,对象不可增删属性。两者的主要区别是,freeze不能改属性的值,seal还可以改属性的值。 487 488开发者使用以上语义时,需确保约束条件是自己需要的,一旦违背以上语义严格模式下就会抛出Error(默认严格模式)。 489 490## 参考文档 491 492[Native侧子线程与UI主线程通信开发](https://developer.huawei.com/consumer/cn/doc/best-practices-V5/bpta-native-sub-main-comm-V5); 493 494[如何在Native侧C++子线程直接调用ArkTS接口,不用通过ArkTS侧触发回调](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs-V5/faqs-ndk-8-V5); 495 496[napi_env、napi_value实例是否可以跨worker线程共享](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs-V5/faqs-ndk-55-V5); 497 498[Native如何创建子线程,有什么约束,与主线程如何通信](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs-V5/faqs-ndk-68-V5).