Node-API开发规范
============

#### 获取JS传入参数及其数量

**【规则】** 当传入napi\_get\_cb\_info的argv不为nullptr时，argv的长度必须大于等于传入argc声明的大小。

当argv不为nullptr时，napi\_get\_cb\_info会根据argc声明的数量将JS实际传入的参数写入argv。如果argc小于等于实际JS传入参数的数量，该接口仅会将声明的argc数量的参数写入argv；而当argc大于实际参数数量时，该接口会在argv的尾部填充undefined。

**错误示例**

static napi\_value IncorrectDemo1(napi\_env env, napi\_callback\_info info) {
    // argc 未正确的初始化，其值为不确定的随机值，导致 argv 的长度可能小于 argc 声明的数量，数据越界。
    size\_t argc;
    napi\_value argv\[10\] = {nullptr};
    napi\_get\_cb\_info(env, info, &argc, argv, nullptr, nullptr);
    return nullptr;
}

static napi\_value IncorrectDemo2(napi\_env env, napi\_callback\_info info) {
    // argc 声明的数量大于 argv 实际初始化的长度，导致 napi\_get\_cb\_info 接口在写入 argv 时数据越界。
    size\_t argc = 5;
    napi\_value argv\[3\] = {nullptr};
    napi\_get\_cb\_info(env, info, &argc, argv, nullptr, nullptr);
    return nullptr;
}

**正确示例**

static napi\_value GetArgvDemo1(napi\_env env, napi\_callback\_info info) {
    size\_t argc = 0;
    // argv 传入 nullptr 来获取传入参数真实数量
    napi\_get\_cb\_info(env, info, &argc, nullptr, nullptr, nullptr);
    // JS 传入参数为0，不执行后续逻辑
    if (argc == 0) {
        return nullptr;
    }
    // 创建数组用以获取JS传入的参数
    napi\_value\* argv = new napi\_value\[argc\];
    napi\_get\_cb\_info(env, info, &argc, argv, nullptr, nullptr);
    // 业务代码
    // ... ...
    // argv 为 new 创建的对象，在使用完成后手动释放
    delete\[\] argv;
    return nullptr;
}

static napi\_value GetArgvDemo2(napi\_env env, napi\_callback\_info info) {
    size\_t argc = 2;
    napi\_value argv\[2\] = {nullptr};
    // napi\_get\_cb\_info 会向 argv 中写入 argc 个 JS 传入参数或 undefined
    napi\_get\_cb\_info(env, info, &argc, argv, nullptr, nullptr);
    // 业务代码
    // ... ...
    return nullptr;
}

#### 生命周期管理

**【规则】** 合理使用napi\_open\_handle\_scope和napi\_close\_handle\_scope管理napi\_value的生命周期，做到生命周期最小化，避免发生内存泄漏问题。

每个napi\_value属于特定的HandleScope，HandleScope通过napi\_open\_handle\_scope和napi\_close\_handle\_scope来建立和关闭，HandleScope关闭后，所属的napi\_value就会自动释放。

**正确示例**：

// 在for循环中频繁调用napi接口创建js对象时，要加handle\_scope及时释放不再使用的资源。
// 下面例子中，每次循环结束局部变量res的生命周期已结束，因此加scope及时释放其持有的js对象，防止内存泄漏
for (int i = 0; i < 100000; i++) {
    napi\_handle\_scope scope = nullptr;
    napi\_open\_handle\_scope(env, &scope);
    if (scope == nullptr) {
        return;
    }
    napi\_value res;
    napi\_create\_object(env, &res);
    napi\_close\_handle\_scope(env, scope);
}

#### 上下文敏感

**【规则】** 多引擎实例场景下，禁止通过Node-API跨引擎实例访问JS对象。

引擎实例是一个独立运行环境，JS对象创建访问等操作必须在同一个引擎实例中进行。若在不同引擎实例中操作同一个对象，可能会引发程序崩溃。引擎实例在接口中体现为napi\_env。

**错误示例**：

// 线程1执行，在env1创建string对象，值为"bar"
napi\_create\_string\_utf8(env1, "bar", NAPI\_AUTO\_LENGTH, &string);
// 线程2执行，在env2创建object对象，并将上述的string对象设置到object对象中
napi\_status status = napi\_create\_object(env2, &object);
if (status != napi\_ok) {
    napi\_throw\_error(env, ...);
    return;
}

status = napi\_set\_named\_property(env2, object, "foo", string);
if (status != napi\_ok) {
    napi\_throw\_error(env, ...);
    return;
}

所有的JS对象都隶属于具体的某一napi\_env，不可将env1的对象，设置到env2中的对象中。在env2中一旦访问到env1的对象，程序可能会发生崩溃。

#### 异常处理

**【建议】** Node-API接口调用发生异常需要及时处理，不能遗漏异常到后续逻辑，否则程序可能发生不可预期行为。

**正确示例**：

// 1.创建对象
napi\_status status = napi\_create\_object(env, &object);
if (status != napi\_ok) {
    napi\_throw\_error(env, ...);
    return;
}
// 2.创建属性值
status = napi\_create\_string\_utf8(env, "bar", NAPI\_AUTO\_LENGTH, &string);
if (status != napi\_ok) {
    napi\_throw\_error(env, ...);
    return;
}
// 3.将步骤2的结果设置为对象object属性foo的值
status = napi\_set\_named\_property(env, object, "foo", string);
if (status != napi\_ok) {
    napi\_throw\_error(env, ...);
    return;
}

如上示例中，步骤1或者步骤2出现异常时，步骤3都不会正常进行。只有当方法的返回值是napi\_ok时，才能保持继续正常运行；否则后续流程可能会出现不可预期的行为。

#### 异步任务

**【规则】** 当使用uv\_queue\_work方法将任务抛到JS线程上面执行的时候，对JS线程的回调方法，一般情况下需要加上napi\_handle\_scope来管理回调方法创建的napi\_value的生命周期。

使用uv\_queue\_work方法，不会走Node-API框架，此时需要开发者自己合理使用napi\_handle\_scope来管理napi\_value的生命周期。

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/1d/v3/ojrYZOBGREmcFewFxUVUsA/note_3.0-zh-cn.png?HW-CC-KV=V1&HW-CC-Date=20260612T073807Z&HW-CC-Expire=86400&HW-CC-Sign=AE3A7F6B025B5511ACB87702940489C60BABEE0FE5386CE677A0A95170C58821)

本规则旨在强调napi\_value生命周期情况，若只想往JS线程抛任务，**不推荐**使用uv\_queue\_work方法。如有抛任务的需要，请使用[napi\_threadsafe\_function系列](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/use-napi-thread-safety)接口。

**正确示例**：

void CallbackTest(CallbackContext\* context)
{
    uv\_loop\_s\* loop = nullptr;
    napi\_get\_uv\_event\_loop(context->env, &loop);
    uv\_work\_t\* work = new uv\_work\_t;
    context->retData = 1;
    work->data = (void\*)context;
    uv\_queue\_work(
        loop, work,
        // 请注意，uv\_queue\_work会创建一个线程并执行该回调函数，若开发者只想往JS线程抛任务，不推荐使用uv\_queue\_work，以避免冗余的线程创建
        \[\](uv\_work\_t\* work) {
            // 执行一些业务逻辑
        },
        // 该回调会执行在loop所在的JS线程上
        \[\](uv\_work\_t\* work, int status) {
            CallbackContext\* context = (CallbackContext\*)work->data;
            napi\_handle\_scope scope = nullptr;
            napi\_open\_handle\_scope(context->env, &scope);
            if (scope == nullptr) {
                if (work != nullptr) {
                    delete work;
                }
                return;
            }
            napi\_value callback = nullptr;
            napi\_get\_reference\_value(context->env, context->callbackRef, &callback);
            napi\_value retArg;
            napi\_create\_int32(context->env, context->retData, &retArg);
            napi\_value ret;
            napi\_call\_function(context->env, nullptr, callback, 1, &retArg, &ret);
            napi\_delete\_reference(context->env, context->callbackRef);
            napi\_close\_handle\_scope(context->env, scope);
            if (work != nullptr) {
                delete work;
            }
            delete context;
        }
    );
}

#### 对象绑定

**【规则】** 使用napi\_wrap接口，如果最后一个参数result传递不为nullptr，需要开发者在合适的时机调用napi\_remove\_wrap函数主动删除创建的napi\_ref。

napi\_wrap接口定义如下：

napi\_wrap(napi\_env env, napi\_value js\_object, void\* native\_object, napi\_finalize finalize\_cb, void\* finalize\_hint, napi\_ref\* result)

当最后一个参数result不为空时，框架会创建一个napi\_ref对象，指向js\_object。此时开发者需要自己管理js\_object的生命周期，即需要在合适的时机调用napi\_remove\_wrap删除napi\_ref，这样GC才能正常释放js\_object，从而触发绑定C++对象native\_object的析构函数finalize\_cb。

一般情况下，根据业务情况最后一个参数result可以直接传递为nullptr。

**正确示例**：

// 用法1：napi\_wrap不需要接收创建的napi\_ref，最后一个参数传递nullptr，创建的napi\_ref是弱引用，由系统管理，不需要用户手动释放
napi\_wrap(env, jsobject, nativeObject, cb, nullptr, nullptr);

// 用法2：napi\_wrap需要接收创建的napi\_ref，最后一个参数不为nullptr，返回的napi\_ref是强引用，需要用户手动释放，否则会内存泄漏
napi\_ref result;
napi\_wrap(env, jsobject, nativeObject, cb, nullptr, &result);
// 当js\_object和result后续不再使用时，及时调用napi\_remove\_wrap释放result
void\* nativeObjectResult = nullptr;
napi\_remove\_wrap(env, jsobject, &nativeObjectResult);

#### 高性能数组

**【建议】** 存储值类型数据时，使用ArrayBuffer代替JSArray来提高应用性能。

使用JSArray作为容器储存数据，支持几乎所有的JS数据类型。

使用napi\_set\_element方法对JSArray存储值类型数据（如int32）时，同样会涉及到与运行时的交互，造成不必要的开销。

ArrayBuffer进行增改是直接对缓冲区进行更改，具有远优于使用napi\_set\_element操作JSArray的性能表现。

因此此种场景下，更推荐使用napi\_create\_arraybuffer接口创建的ArrayBuffer对象。

**示例：**

// 以下代码使用常规JSArray作为容器，但其仅存储int32类型数据。
// 但因为是JS对象，因此只能使用napi方法对其进行增改，性能较低。
static napi\_value ArrayDemo(napi\_env env, napi\_callback\_info info)
{
    constexpr size\_t arrSize = 1000;
    napi\_value jsArr = nullptr;
    napi\_create\_array(env, &jsArr);
    for (int i = 0; i < arrSize; i++) {
        napi\_value arrValue = nullptr;
        napi\_create\_int32(env, i, &arrValue);
        // 常规JSArray使用napi方法对array进行读写，性能较差。
        napi\_set\_element(env, jsArr, i, arrValue);
    }
    return jsArr;
}

// 推荐写法：
// 同样以int32类型数据为例，但以下代码使用ArrayBuffer作为容器。
// 因此可以使用C/C++的方法直接对缓冲区进行增改。
static napi\_value ArrayBufferDemo(napi\_env env, napi\_callback\_info info)
{
    constexpr size\_t arrSize = 1000;
    napi\_value arrBuffer = nullptr;
    void\* data = nullptr;

    napi\_create\_arraybuffer(env, arrSize \* sizeof(int32\_t), &data, &arrBuffer);
    // data为空指针，避免对data进行写入
    if (data == nullptr) {
        return arrBuffer;
    }
    int32\_t\* i32Buffer = reinterpret\_cast<int32\_t\*>(data);
    for (int i = 0; i < arrSize; i++) {
        // arrayBuffer直接对缓冲区进行修改，跳过运行时，
        // 与操作原生C/C++对象性能相当
        i32Buffer\[i\] = i;
    }

    return arrBuffer;
}

napi\_create\_arraybuffer等同于JS代码中的new ArrayBuffer(size)，其生成的对象不可直接在TS/JS中进行读取，需要将其包装为TypedArray或DataView后方可进行读写。

**基准性能测试结果如下：**

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/c4/v3/v37DEH3FSAqTFuEMiCOTDA/note_3.0-zh-cn.png?HW-CC-KV=V1&HW-CC-Date=20260612T073807Z&HW-CC-Expire=86400&HW-CC-Sign=A5D68A8FBC1669733055F1478BAEA1DFADBAA6107079B17F13A13433690EA7EB)

以下数据为千次循环写入累计数据，为更好的体现出差异，已对设备核心频率进行限制。

| 容器类型 | Benchmark数据（us） |
| :-- | :-- |
| JSArray | 1566.174 |
| ArrayBuffer | 3.609 |

#### 数据转换

**【建议】** 尽可能的减少数据转换次数，避免不必要的复制。

*   **减少数据转换次数：** 频繁的数据转换可能会导致性能下降，可以通过批量处理数据或者使用更高效的数据结构来优化性能。
*   **避免不必要的数据复制：** 在进行数据转换时，可以使用Node-API提供的接口来直接访问原始数据，而不是创建新的副本。
*   **使用缓存：** 如果某些数据在多次转换中都会被使用到，可以考虑使用缓存来避免重复的数据转换。缓存可以减少不必要的计算，提高性能。

#### 模块注册与模块命名

**【规则】**

nm\_register\_func对应的函数需要加上修饰符static，防止与其他二进制so文件里的符号冲突。

模块注册的入口，即使用\_\_attribute\_\_((constructor))修饰函数的函数名需要确保与其他模块不同。

模块实现中.nm\_modname字段需要与二进制so文件的名字完全匹配，区分大小写。

一个so文件只能注册一个模块，即通过napi\_module\_register注册的模块。禁止在同一个so文件中注册多个不同模块，否则框架加载这个so时可能匹配到错误的模块，引发非预期行为。

**错误示例**

以下代码为二进制so文件的名为nativerender时的错误示例

EXTERN\_C\_START
napi\_value Init(napi\_env env, napi\_value exports)
{
    // ...
    return exports;
}

static napi\_value InitOther(napi\_env env, napi\_value exports)
{
    // ...
    return exports;
}
EXTERN\_C\_END

static napi\_module nativeModule = {
    .nm\_version = 1,
    .nm\_flags = 0,
    .nm\_filename = nullptr,
    // 没有在nm\_register\_func对应的函数加上static
    .nm\_register\_func = Init,
    // 模块实现中.nm\_modname字段没有与模块名完全匹配，会导致多线程场景模块加载失败
    .nm\_modname = "entry",
    .nm\_priv = nullptr,
    .reserved = { 0 },
};

// 同一个so中定义了第二个不同的napi\_module
static napi\_module otherModule = {
    .nm\_version = 1,
    .nm\_flags = 0,
    .nm\_filename = nullptr,
    .nm\_register\_func = InitOther,
    .nm\_modname = "other",
    .nm\_priv = nullptr,
    .reserved = { 0 },
};

// 模块注册的入口函数名为RegisterModule，容易与其他模块重复
extern "C" \_\_attribute\_\_((constructor)) void RegisterModule()
{
    napi\_module\_register(&nativeModule);
    // 同一个so中注册了两个不同的模块，框架加载时可能匹配到错误的模块，
    // 无论通过何种方式（如多个构造函数、全局对象构造等）多次调用napi\_module\_register，
    // 框架加载so时都可能匹配到错误的模块，导致非预期行为
    napi\_module\_register(&otherModule);
}

图一

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/e4/v3/xIlj6G04SpSvBprFfxuXXA/zh-cn_image_0000002594829604.png?HW-CC-KV=V1&HW-CC-Date=20260612T073807Z&HW-CC-Expire=86400&HW-CC-Sign=1B9B15D27C9A990001D95F4CA05E801574CEC4164D2388D2635768AB75332E05)

图二

![](https://contentcenter-vali-drcn.dbankcdn.cn/pvt_2/DeveloperAlliance_scene_100_1/ac/v3/4gE9J-luT7GI_O4jswkHZg/zh-cn_image_0000002625189051.png?HW-CC-KV=V1&HW-CC-Date=20260612T073807Z&HW-CC-Expire=86400&HW-CC-Sign=FF38E956328576F976277602635D33C70BB611478B42FDBC06F1517D82661FBD)

**正确示例**：

以下代码为模块名为nativerender时的正确示例

EXTERN\_C\_START
static napi\_value Init(napi\_env env, napi\_value exports)
{
    // ...
    return exports;
}
EXTERN\_C\_END

static napi\_module nativeModule = {
    .nm\_version = 1,
    .nm\_flags = 0,
    .nm\_filename = nullptr,
    .nm\_register\_func = Init,
    .nm\_modname = "nativerender",
    .nm\_priv = nullptr,
    .reserved = { 0 },
};

extern "C" \_\_attribute\_\_((constructor)) void RegisterNativeRenderModule()
{
    napi\_module\_register(&nativeModule);
}

#### dlopen与模块注册

**【规则】**

如果注册的模块事先有被dlopen，需使用以下方式注册模块。

模块需对外导出固定名称为napi\_onLoad的函数，在该函数内调用注册函数。napi\_onLoad函数只会在ArkTS代码的import语句中被主动调用，从而避免dlopen时提前触发模块的注册。

**示例**

EXTERN\_C\_START
static napi\_value Init(napi\_env env, napi\_value exports)
{
    // ...
    return exports;
}
EXTERN\_C\_END

static napi\_module nativeModule = {
    .nm\_version = 1,
    .nm\_flags = 0,
    .nm\_filename = nullptr,
    .nm\_register\_func = Init,
    .nm\_modname = "nativerender",
    .nm\_priv = nullptr,
    .reserved = { 0 },
};

extern "C" void napi\_onLoad()
{
    napi\_module\_register(&nativeModule);
}

#### 正确的使用napi\_create\_external系列接口创建的JS Object

**【规则】** napi\_create\_external系列接口创建出来的JS对象仅允许在当前线程传递和使用，跨线程传递（如使用worker的post\_message）将会导致应用crash。若需跨线程传递绑定有Native对象的JS对象，请使用napi\_coerce\_to\_native\_binding\_object接口绑定JS对象和Native对象。具体API说明详见[API参考](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/use-napi-about-object#napi_create_external)。

**错误示例**

static void MyFinalizeCB(napi\_env env, void \*finalize\_data, void \*finalize\_hint) { return; }

static napi\_value CreateMyExternal(napi\_env env, napi\_callback\_info info) {
    napi\_value result = nullptr;
    napi\_create\_external(env, nullptr, MyFinalizeCB, nullptr, &result);
    return result;
}

// 此处已省略模块注册的代码，你可能需要自行注册 CreateMyExternal 方法

// index.d.ts
export const createMyExternal: () => Object;

// 应用代码
import testNapi from 'libentry.so';
import { worker } from '@kit.ArkTS';

const mWorker = new worker.ThreadWorker('../workers/Worker');

{
    const mExternalObj = testNapi.createMyExternal();

    mWorker.postMessage(mExternalObj);

}

// 关闭worker线程
// 应用可能在此步骤崩溃，或在后续引擎进行GC的时候崩溃
mWorker.terminate();
// Worker的实现为默认模板，此处省略

#### 防止重复释放获取的buffer

**【规则】** 使用napi\_get\_arraybuffer\_info等接口，参数data资源开发者不允许释放，data的生命周期受引擎管理。

这里以napi\_get\_arraybuffer\_info为例，该接口定义如下：

napi\_get\_arraybuffer\_info(napi\_env env, napi\_value arraybuffer, void\*\* data, size\_t\* byte\_length)

data获取的是ArrayBuffer的Buffer头指针，开发者只可以在范围内读写该Buffer区域，不可以进行释放操作。该段内存由引擎内部的ArrayBuffer Allocator管理，随JS对象ArrayBuffer的生命周期释放。

**错误示例：**

void\* arrayBufferPtr = nullptr;
napi\_value arrayBuffer = nullptr;
size\_t createBufferSize = ARRAY\_BUFFER\_SIZE;
napi\_status verification = napi\_create\_arraybuffer(env, createBufferSize, &arrayBufferPtr, &arrayBuffer);
size\_t arrayBufferSize;
napi\_status result = napi\_get\_arraybuffer\_info(env, arrayBuffer, &arrayBufferPtr, &arrayBufferSize);
delete arrayBufferPtr; // 这一步是禁止的，创建的arrayBufferPtr生命周期由引擎管理，不允许用户自己delete，否则会double free

| Node-API中受当前规则约束的接口有： |
| :-- |
| napi\_create\_arraybuffer |
| napi\_create\_sendable\_arraybuffer |
| napi\_get\_arraybuffer\_info |
| napi\_create\_buffer |
| napi\_get\_buffer\_info |
| napi\_get\_typedarray\_info |
| napi\_get\_dataview\_info |

#### 其他

**【建议】** 合理使用napi\_object\_freeze和napi\_object\_seal来控制对象以及对象属性的可变性。

napi\_object\_freeze等同于Object.freeze语义，freeze后对象的所有属性都不可能以任何方式被修改；napi\_object\_seal等同于Object.seal语义，对象不可增删属性。两者的主要区别是，freeze不能改属性的值，seal还可以改属性的值。

开发者使用以上语义时，需确保约束条件是自己需要的，一旦违背以上语义严格模式下就会抛出Error（默认严格模式）。

#### 参考文档

[Native侧子线程与UI主线程通信开发](https://developer.huawei.com/consumer/cn/doc/best-practices-V5/bpta-native-sub-main-comm-V5);

[如何在Native侧C++子线程直接调用ArkTS接口，不用通过ArkTS侧触发回调](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs-V5/faqs-ndk-8-V5);

[napi\_env、napi\_value实例是否可以跨worker线程共享](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs-V5/faqs-ndk-55-V5);

[Native如何创建子线程，有什么约束，与主线程如何通信](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs-V5/faqs-ndk-68-V5).