Administrator
发布于 2026-09-02 / 8,020 阅读
0
0

鸿蒙 NAPI 实战:C++ 原生模块开发与 ArkTS 桥接

系列文章《鸿蒙开发从入门到专家》第 43 篇 · 高级

前两篇讲的都是 ArkTS 层面的东西。但有些场景 ArkTS 性能不够——加密解密、图像处理、音视频编解码、复用已有 C/C++ 库。这时候需要 NAPI。

NAPI(Node-API)是鸿蒙提供的跨语言交互框架,让 ArkTS 能调用 C/C++ 代码。名字带 “Node” 是因为它基于 Node-API 标准,但运行环境和 Node.js 没有关系。鸿蒙的 ArkTS 引擎和 V8 有 API 兼容层,NAPI 的接口风格和 Node.js 原生模块几乎一致。

这篇从创建一个 NAPI 模块开始,到类型转换、异步调用、性能优化,覆盖实际开发中最常遇到的环节。

一、创建 NAPI 模块

1.1 项目结构

在 DevEco Studio 中,右键工程 → New → Module → Native C++ Library。生成的模块结构如下:

library/
├── cpp/
│   ├── CMakeLists.txt
│   ├── napi_init.cpp
│   └── types/
│       └── libentry/
│           ├── Index.d.ts    // ArkTS 侧的类型声明
│           └── oh-package.json5
├── Index.ets                 // ArkTS 侧入口
└── oh-package.json5

关键文件有三个:

  • napi_init.cpp:C++ 实现和模块注册

  • Index.d.ts:TypeScript 类型声明,ArkTS 侧通过它知道有哪些方法可用

  • CMakeLists.txt:C++ 构建配置

1.2 注册模块

NAPI 模块通过 napi_module 结构体注册。这段代码看起来冗长,但理解了模式后每次都一样:

// napi_init.cpp
#include "napi/native_api.h"

// 同步加法
static napi_value Add(napi_env env, napi_callback_info info) {
    size_t argc = 2;
    napi_value args[2];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    double a, b;
    napi_get_value_double(env, args[0], &a);
    napi_get_value_double(env, args[1], &b);

    napi_value result;
    napi_create_double(env, a + b, &result);
    return result;
}

// 模块初始化
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        { "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr },
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

// 模块定义
static napi_module nativeModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "entry",   // 模块名,对应 ArkTS 中的 import 路径
    .nm_priv = nullptr,
    .reserved = { 0 },
};

// 注册(构造函数属性,模块加载时自动调用)
extern "C" __attribute__((constructor)) void RegisterEntryModule() {
    napi_module_register(&nativeModule);
}

1.3 类型声明

ArkTS 是强类型的,NAPI 模块需要在 Index.d.ts 中声明暴露的接口:

// types/libentry/Index.d.ts
export const add: (a: number, b: number) => number;

1.4 在 ArkTS 中调用

import { add } from 'library';

@Entry
@Component
struct NapiDemoPage {
  build() {
    Button('Calculate')
      .onClick(() => {
        const result = add(3, 14);
        console.info('Result:', result);  // 17
      })
  }
}

二、类型转换:最容易出错的地方

NAPI 的核心工作是 ArkTS 类型(JS 值)和 C++ 类型之间的转换。这是最容易出错的地方,因为类型不匹配不会在编译期报错,而是运行时崩溃。

2.1 基本类型转换

ArkTS 类型

C++ 类型

转换 API

number

double

napi_get_value_double / napi_create_double

number (int)

int32_t

napi_get_value_int32 / napi_create_int32

boolean

bool

napi_get_value_bool / napi_create_bool

string

char*

napi_get_value_string_utf8

字符串转换有个坑:napi_get_value_string_utf8 需要调用两次。第一次获取长度,第二次写入数据:

static napi_value ProcessString(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // 第一次调用获取字符串长度
    size_t len = 0;
    napi_get_value_string_utf8(env, args[0], nullptr, 0, &len);

    // 分配缓冲区(+1 给 null 终止符)
    char* buf = new char[len + 1];
    // 第二次调用获取实际内容
    napi_get_value_string_utf8(env, args[0], buf, len + 1, &len);

    // 处理字符串...
    std::string str(buf);
    delete[] buf;

    // 返回处理后的字符串
    napi_value result;
    napi_create_string_utf8(env, str.c_str(), str.length(), &result);
    return result;
}

2.2 对象和数组

ArkTS 对象和数组在 C++ 侧都是 napi_value,通过属性访问 API 操作:

// 读取对象属性
napi_value value;
napi_get_named_property(env, jsObject, "name", &value);
// 转为 C++ 类型
char name[256];
size_t nameLen;
napi_get_value_string_utf8(env, value, name, 256, &nameLen);

// 读取数组
uint32_t length;
napi_get_array_length(env, jsArray, &length);
for (uint32_t i = 0; i < length; i++) {
    napi_value element;
    napi_get_element(env, jsArray, i, &element);
    double num;
    napi_get_value_double(env, element, &num);
}

2.3 对象包装(Object Wrap)

如果要在 ArkTS 中操作一个 C++ 对象,用 napi_wrap 把 C++ 对象和 JS 对象绑定:

class ImageProcessor {
public:
    void process(unsigned char* data, int size) { /* ... */ }
    ~ImageProcessor() { /* 清理资源 */ }
};

// 构造函数
static napi_value CreateProcessor(napi_env env, napi_callback_info info) {
    napi_value jsThis;
    napi_get_cb_info(env, info, nullptr, nullptr, &jsThis, nullptr);

    auto* processor = new ImageProcessor();
    napi_wrap(env, jsThis, reinterpret_cast<void*>(processor),
        [](napi_env env, void* data, void* hint) {
            // GC 回收时调用析构
            delete static_cast<ImageProcessor*>(data);
        }, nullptr, nullptr);

    return jsThis;
}

类型声明:

export class ImageProcessor {
    constructor();
    process(data: Uint8Array): void;
}

三、异步调用

C++ 的计算密集型任务不能在主线程跑,否则 UI 会卡。NAPI 有两种异步模式。

3.1 回调模式

static napi_value AsyncCompute(napi_env env, napi_callback_info info) {
    size_t argc = 2;
    napi_value args[2];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    double input;
    napi_get_value_double(env, args[0], &input);
    napi_value callback = args[1];

    // 在 Worker 线程执行计算
    std::thread([env, callback, input]() {
        // 模拟耗时计算
        double result = heavyCompute(input);

        // 回到主线程执行回调
        napi_value global;
        napi_get_global(env, &global);
        napi_value cbResult;
        napi_value arg;
        napi_create_double(env, result, &arg);
        napi_call_function(env, global, callback, 1, &arg, &cbResult);
    }).detach();

    return nullptr;
}

上面这个写法有个严重问题:env 不能跨线程使用。NAPI 规定 napi_env 只能在创建它的线程上使用。跨线程回调要用 napi_threadsafe_function。

3.2 线程安全函数(Threadsafe Function)

static napi_value AsyncCompute(napi_env env, napi_callback_info info) {
    size_t argc = 2;
    napi_value args[2];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    double input;
    napi_get_value_double(env, args[0], &input);

    // 创建线程安全函数
    napi_value workName;
    napi_create_string_utf8(env, "AsyncCompute", NAPI_AUTO_LENGTH, &workName);

    napi_threadsafe_function tsfn;
    napi_create_threadsafe_function(env, args[1], nullptr, workName,
        0, 1, nullptr, nullptr, nullptr,
        [](napi_env env, napi_js_callback cb, void* data) {
            // 这个 lambda 在主线程执行
            napi_value result;
            napi_create_double(env, *(double*)data, &result);
            napi_value undefined;
            napi_get_undefined(env, &undefined);
            napi_call_function(env, undefined, cb, 1, &result, nullptr);
            delete (double*)data;
        }, &tsfn);

    // 在子线程执行计算
    std::thread([tsfn, input]() {
        double result = heavyCompute(input);
        double* pResult = new double(result);
        napi_call_threadsafe_function(tsfn, pResult, napi_tsfn_nonblocking);
        napi_release_threadsafe_function(tsfn, napi_tsfn_release);
    }).detach();

    return nullptr;
}

看起来啰嗦,但这是正确做法。线程安全函数处理了线程间的消息传递和生命周期管理,避免了 env 跨线程使用的崩溃。

四、性能优化建议

4.1 减少 ArkTS 与 C++ 的边界调用

每次跨语言调用都有开销(类型转换、函数派发)。100 次 add(a, b) 的调用,不如一次传入 100 个数字的数组在 C++ 内循环加。边界越少越好。

4.2 用 TypedArray 代替数组

ArkTS 的 Uint8Array / Float32Array 在 C++ 侧可以直接拿到内存指针,零拷贝:

static napi_value ProcessBuffer(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    void* data;
    size_t length;
    napi_get_typedarray_info(env, args[0], nullptr, &length, &data, nullptr, nullptr);

    // 直接操作内存,零拷贝
    auto* pixels = static_cast<uint8_t*>(data);
    for (size_t i = 0; i < length; i += 4) {
        // 处理 RGBA 像素
        pixels[i] = 255 - pixels[i];         // R
        pixels[i + 1] = 255 - pixels[i + 1]; // G
        pixels[i + 2] = 255 - pixels[i + 2]; // B
    }
    return nullptr;
}

如果传普通 Array,每个元素都要走一次 napi_get_element,性能差一个数量级。

4.3 缓存 napi_ref

如果一个 C++ 对象会被多次访问,用 napi_ref 缓存 JS 对象引用,避免反复创建:

static napi_ref g_constructor;

napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor properties[] = {
        { "process", nullptr, Process, nullptr, nullptr, nullptr, napi_default, nullptr },
    };
    napi_value cons;
    napi_define_class(env, "ImageProcessor", NAPI_AUTO_LENGTH,
        CreateProcessor, nullptr, 1, properties, &cons);
    napi_create_reference(env, cons, 1, &g_constructor);
    napi_set_named_property(env, exports, "ImageProcessor", cons);
    return exports;
}

4.4 反向调用 ArkTS 系统能力

有时候 C++ 侧需要调用 ArkTS 的系统 API(比如获取屏幕尺寸)。用 napi_load_module 加载系统模块:

static napi_value GetDisplaySize(napi_env env, napi_callback_info info) {
    // 加载 @ohos.display 模块
    napi_value sysModule;
    napi_load_module(env, "@ohos.display", &sysModule);

    // 获取 getDefaultDisplaySync 函数
    napi_value func;
    napi_get_named_property(env, sysModule, "getDefaultDisplaySync", &func);
    napi_value display;
    napi_call_function(env, sysModule, func, 0, nullptr, &display);

    // 读取属性
    napi_value widthVal;
    napi_get_named_property(env, display, "width", &widthVal);
    double width;
    napi_get_value_double(env, widthVal, &width);

    napi_value result;
    napi_create_double(env, width, &result);
    return result;
}

五、调试建议

  • C++ 代码崩溃不会显示在 ArkTS 的 try-catch 里,需要看 hilog 输出。过滤 tag: Cpp 或模块名查看崩溃堆栈。

  • 在 build-profile.json5 中配置 externalNativeOptions 指向 CMakeLists.txt,DevEco Studio 会编译 C++ 并打包到 HAP。

  • C++ 代码的断点调试需要在 DevEco Studio 的 Run Configuration 中勾选 “Debug native code”。

小结

NAPI 的核心工作就两件事:类型转换和线程安全。

  • 类型转换:基本类型用对应 API,字符串要调用两次,对象用 napi_wrap 包装。

  • 线程安全:跨线程回调必须用 napi_threadsafe_function,不能直接用 env。

  • 性能:减少边界调用、用 TypedArray 零拷贝传数据、缓存引用。

下一篇换一个方向,聊鸿蒙设计规范和人因工程。代码写得再好,UI 不符合规范也过不了应用市场审核。


评论