系列文章《鸿蒙开发从入门到专家》第 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 基本类型转换
字符串转换有个坑: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 不符合规范也过不了应用市场审核。