鸿蒙中Node-API与NAPI区别:Node-API 是标准,NAPI(Native API) 是具体实现,但在鸿蒙(HarmonyOS)语境下,它们通常指向同一个东西。
Node-API(原名 N-API) 是 Node.js 提供的一个用于构建本地插件(Native Addons) 的 API 层。它允许开发者用 C、C++ 或 Rust 等系统编程语言编写可直接被 Node.js 调用的模块。
一、HarmonyOS Node-API简介
场景介绍
HarmonyOS Node-API是基于Node.js 18.x LTS的Node-API规范扩展开发的机制,为开发者提供了ArkTS/JS与C/C++模块之间的交互能力。它提供了一组稳定的、跨平台的API,可以在不同的操作系统上使用。
本文中如无特别说明,后续均使用Node-API指代HarmonyOS Node-API能力。

说明
HarmonyOS Node-API与Node.js 18.x LTS的Node-API规范的接口异同点,详见Node-API参考文档
一般情况下HarmonyOS应用开发使用ArkTS/JS语言,但部分场景由于性能、效率等要求,比如游戏、物理模拟等,需要依赖使用现有的C/C++库。Node-API规范封装了I/O、CPU密集型、OS底层等能力并对外暴露C接口,使用C/C++模块的注册机制,向ArkTS/JS对象上挂载属性和方法的方式来实现ArkTS/JS和C/C++的交互。主要场景如下:
-
系统可以将框架层丰富的模块功能通过Node-API的模块注册机制对外暴露ArkTS/JS的接口,将C/C++的能力开放给应用的ArkTS/JS层。
-
应用开发者也可以选择将一些对性能、底层系统调用有要求的核心功能用C/C++封装实现,再通过ArkTS/JS接口使用,提高应用本身的执行效率。
Node-API的组成架构
图1 Node-API的组成架构

-
Native Module:开发者使用Node-API开发的模块,用于在ArkTS侧导入使用。
-
Node-API:实现ArkTS与C/C++交互的逻辑。
-
ModuleManager:Native模块管理,包括加载、查找等。
-
ScopeManager:管理napi_value的生命周期。
-
ReferenceManager:管理napi_ref的生命周期。
-
NativeEngine:ArkTS引擎抽象层,统一ArkTS引擎在Node-API层的接口行为。
-
ArkCompiler ArkTS Runtime:ArkTS运行时。
Node-API的关键交互流程
图2 Node-API的关键交互流程

ArkTS和C++之间的交互流程,主要分为以下两步:
-
初始化阶段 :当ArkTS侧在import一个Native模块时,ArkTS引擎会调用ModuleManager加载模块对应的so及其依赖。首次加载时会触发模块的注册,将模块定义的方法属性挂载到exports对象上并返回该对象。后续再次import时从缓存中查找Native模块信息并返回,不会进行多次加载。
-
调用阶段:当ArkTS侧通过上述import返回的对象调用方法时,ArkTS引擎会找到并调用对应的C/C++方法。
上图中ArkTS引擎和ModuleManager都是由系统管理的,开发者仅需要关注ArkTS模块与Native模块。ArkTS与Native模块间的调用时序图如下所示

二、常用的Node-API数据类型和接口
常用数据类型
napi_value
napi_value是一个C的结构体指针,表示一个ArkTS/JS对象的引用。在Native侧代码中,用于承载ArkTS侧任意类型的数据,开发者不需要感知不同的ArkTS侧数据类型,统一都用napi_value表示。例如ArkTS中的number、string等各种数据类型都可以用napi_value表示。退出native方法后,napi_value将失效。

napi_env
一般作为Native侧函数的参数来使用,在ArkTS侧调用Native侧函数时,默认会传递napi_env和napi_callback_info这两个参数信息。注意:native提供给ArkTS的函数必须有且只有这两个参数。
napi_env用于表示Node-API执行时的上下文,可以传递给函数中的Node-APl接口。napi_env与ArkTS线程绑定,ArkTS线程退出后, napi_env将失效。
-
napi_env与ArkTS/JS线程的上下文环境绑定,每一个napi_env都持有独立的运行时上下文环境,当ArkTS/JS线程退出之后,相应的napi_env将不再有效。
-
禁止缓存napi_env,禁止在不同线程间传递napi_env。
napi_callback_info
一般作为Native侧函数的参数来使用,在ArkTS侧调用Native侧函数时,默认会传递napi_env和napi_callback_info这两个参数信息。注意:native提供给ArkTS的函数必须有且只有这两个参数。
napi_callback_info保存了ArkTS侧的传入的所有参数,用于传递给napi_get_cb_info()函数获取ArkTS侧入参信息。
napi_status
是一个枚举数据类型,表示Node-API接口返回的状态信息,主要用于判断Node-APl接口调用是否有异常。每当调用一个Node-API函数,都会返回该值,表示操作成功与否的相关信息。
每当调用一个Node-API函数,都会返回该值,表示操作成功与否的相关信息。举例如下:

TypeScript
typedef enum {
napi_ok,//默认从0开始。若返回结果非0,则调用异常。
napi_invalid_arg,
napi_object_expected,
napi_string_expected,
napi_name_expected,
napi_function_expected,
napi_number_expected,
napi_boolean_expected,
napi_array_expected,
napi_generic_failure,
napi_pending_exception,
napi_cancelled,
napi_escape_called_twice,
napi_handle_scope_mismatch,
napi_callback_scope_mismatch,
napi_queue_full,
napi_closing,
napi_bigint_expected,
napi_date_expected,
napi_arraybuffer_expected,
napi_detachable_arraybuffer_expected,
napi_would_deadlock, /* unused */
napi_no_external_buffers_allowed,
napi_cannot_run_js
} napi_status;
常用NAPI接口
napi_get_cb_info
napi_get_cb_info:从给定的napi_callback_info对象中获取ArkTS侧传入参数的详细信息,如参数和this对象。
转换napi_value类型为C/C++类型相关接口
HarmonyOs Node-APl提供了一系列的接口用于将napi_value类型参数转化为C/C++标准数据类型。相关类型接口较多下面只列举部分基础数据类型相关接口。

转换C/C++类型为napi_value类型相关接口
除了将napi_value类型参数转化为C/C++标准数据类型相关接口外,HarmonyOS Node-APl还提供了一些将C/C++标准数据类型参数转换为napi_value类型参数的接口。相关类型接口较多,下面只列举部分基础数据类型相关接口。

三、Node-API开发流程
使用Node-API实现跨语言交互,首先需要按照Node-API的机制实现模块的注册和加载等相关动作。
-
ArkTS/JS侧:实现C++方法的调用,通过import所需的so库后,可以调用C++方法。
-
Native侧:.cpp文件,实现模块的注册。需要提供注册lib库的名称,并在注册回调方法中定义接口的映射关系,即Native方法及对应的JS/ArkTS接口名称等。
此处以在ArkTS/JS侧调用callNative()接口、在Native侧实现加法操作的CallNative()接口,从而实现跨语言交互为例,呈现使用Node-API进行跨语言交互的流程。
创建Native C++工程
1、在DevEco Studio中New > Create Project ,选择Native C++模板,点击Next ,选择API版本,设置好工程名称,点击Finish,创建得到新工程。
创建工程后工程结构可以分两部分,cpp目录部分和ets目录部分,工程结构如下图,详细介绍可见C++工程目录结构。

上图进一步说明:
index.d.ts中定义了C++侧需要暴露在ArkTS侧的接口,ArkTS侧后续使用就是调用这里面定义的接口;
CMakeLists.txt中定义了编译的模块名、依赖的文件,包括最终生成是静态库还是动态库;
总的实现流程如下图

Native侧方法的实现
设置模块 注册信息
ArkTS侧import native模块时,会加载其对应的so。加载so时,首先会调用napi_module_register方法,将模块注册到系统中,并调用模块初始化函数。
napi_module_register方法入参是napi_module对象,它有两个关键属性:一个是.nm_register_func,定义模块初始化函数;另一个是.nm_modname,定义模块的名称,也就是ArkTS侧引入的so库的名称,模块系统会根据此名称来区分不同的so。
TypeScript
// entry/src/main/cpp/napi_init.cpp
// 准备模块加载相关信息,将上述Init函数与本模块名等信息记录下来。
static napi_module demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = ((void*)0),
.reserved = {0},
};
// 加载so时,该函数会自动被调用,将上述demoModule模块注册到系统中。
extern "C" __attribute__((constructor)) void RegisterDemoModule() {
napi_module_register(&demoModule);
}
注:以上代码无须复制,创建Native C++工程以后在napi_init.cpp代码中已配置好。
官网视频课程对代码的说明如下:

模块初始化:Native接口映射
实现ArkTS接口与C++接口的绑定和映射。
TypeScript
// entry/src/main/cpp/napi_init.cpp
//定义Native侧函数,用于后续实现业务功能
static napi_value CallNative(napi_env env, napi_callback_info info)
{
//...待实现
}
static napi_value NativeCallArkTS(napi_env env, napi_callback_info info)
{
//...待实现
}
EXTERN_C_START
// 模块初始化
static napi_value Init(napi_env env, napi_value exports) {
// ArkTS接口与C++接口的绑定和映射
napi_property_descriptor desc[] = {
// 注:Init函数结构在Native C++工程创建时自动实现,开发者仅需实现以下两行代码。
//下面两行代码将Native侧的函数映射到ArkTS侧方法上。其中第一个参数是在ArkTS侧暴露的接回名,第三个参数是native侧的方法名
{"callNative", nullptr, CallNative, nullptr, nullptr, nullptr, napi_default, nullptr},
{"nativeCallArkTS", nullptr, NativeCallArkTS, nullptr, nullptr, nullptr, napi_default, nullptr}
};
// 在exports对象上挂载CallNative/NativeCallArkTS两个Native方法
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
定义ArkTS侧接口:在index.d.ts文件中,描述提供TS侧的方法。
TypeScript
// entry/src/main/cpp/types/libentry/index.d.ts
export const callNative: (a: number, b: number) => number;
export const nativeCallArkTS: (cb: (a: number) => number) => number;
定义ArkTS侧接口:在oh-package.json5文件中将index.d.ts与cpp文件关联起来。
TypeScript
// entry/src/main/cpp/types/libentry/oh-package.json5
{
"name": "libentry.so",
"types": "./index.d.ts",
"version": "",
"description": "Please describe the basic information."
}
模块构建配置:在CMakeLists.txt文件中配置CMake构建脚本
主要关注以下几个配置:
TypeScript
# entry/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
project(MyApplication2)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
//设置头文件目录,一般使用默认配置即可。默认配置整个cpp目录和cpp下include目录
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include)
# 配置最终编译生成的产物名称、产物类型、以及编译所依赖的文件。
# 第一个参数是编译的模块名称,这里写了entry,最后编译出来的产物就是libentry.so;
# 第二个参数设置编译生成的产物类型,SHARED表示编译为动态库;
# 第三个参数配置参与编译的所有cpp文件,由于上述步骤中的业务功能、接回映射以及模块注册信息都在napi_init.cpp文件中,没有新建其他cpp文件,所以这里只配置了napi_init.cpp文件。。
add_library(entry SHARED napi_init.cpp)
# 配置编译当前模块需要依赖(链接)的库
target_link_libraries(entry PUBLIC libace_napi.z.so)
如果希望代码工程最终可以在模拟器上运行,还需要在模块级的build-profle,json5文件中通过配置项abilFilters,配置模拟器对应的架构信息x86_64

实现Native侧的CallNative以及NativeCallArkTS接口
TypeScript
// entry/src/main/cpp/napi_init.cpp
static napi_value CallNative(napi_env env, napi_callback_info info)
{
size_t argc = 2;
// 声明参数数组
napi_value args[2] = {nullptr};
// 获取传入的参数并依次放入参数数组中
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// 依次获取参数
double value0;
napi_get_value_double(env, args[0], &value0);
double value1;
napi_get_value_double(env, args[1], &value1);
// 返回两数相加的结果
napi_value sum;
napi_create_double(env, value0 + value1, &sum);
return sum;
}
static napi_value NativeCallArkTS(napi_env env, napi_callback_info info)
{
size_t argc = 1;
// 声明参数数组
napi_value args[1] = {nullptr};
// 获取传入的参数并依次放入参数数组中
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// 创建一个int,作为ArkTS的入参
napi_value argv = nullptr;
napi_create_int32(env, 2, &argv);
// 调用传入的callback,并将其结果返回
napi_value result = nullptr;
napi_call_function(env, nullptr, args[0], 1, &argv, &result);
return result;
}
ArkTS侧调用C/C++方法实现
ArkTS侧通过import引入Native侧包含处理逻辑的so来使用C/C++的方法。
TypeScript
// entry/src/main/ets/pages/Index.ets
// 通过import的方式,引入Native能力。
import nativeModule from 'libentry.so'
@Entry
@Component
struct Index {
@State message: string = 'Test Node-API callNative result: ';
@State message2: string = 'Test Node-API nativeCallArkTS result: ';
build() {
Row() {
Column() {
// 第一个按钮,调用callNative方法,对应到Native侧的CallNative方法,进行两数相加。
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.message += nativeModule.callNative(2, 3);
})
// 第二个按钮,调用nativeCallArkTS方法,对应到Native的NativeCallArkTS,在Native调用ArkTS function。
Text(this.message2)
.fontSize(50)
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.message2 += nativeModule.nativeCallArkTS((a: number)=> {
return a * 2;
});
})
}
.width('100%')
}
.height('100%')
}
}
Node-API的约束限制
SO命名规则
导入使用的模块名和注册时的模块名大小写保持一致,如模块名为entry,则so的名字为libentry.so,napi_module中nm_modname字段应为entry,ArkTS侧使用时写作:import xxx from 'libentry.so'。
注册建议
-
nm_register_func对应的函数(如上述Init函数)需要加上static,防止与其他so里的符号冲突。
-
模块注册的入口,即使用__attribute__((constructor))修饰的函数的函数名(如上述RegisterDemoModule函数)需要确保不与其它模块重复。
多线程限制
每个引擎实例对应一个ArkTS线程,实例上的对象不能跨线程操作,否则会引起应用crash。使用时需要遵循如下原则:
- Node-API接口只能在ArkTS线程使用。
- Native接口入参env与特定ArkTS线程绑定,只能在创建该env的线程使用。
- 使用Node-API接口创建的数据需在env完全销毁前进行释放,避免内存泄漏。此外,在napi_env销毁后访问/使用这些数据,可能会导致进程崩溃。
代码调试设备选择
建议开发者优先使用真机进行代码调试,若无真机或者真机无权限则可使用模拟器进行调试,模拟器调试中遇到的问题详见bm工具
开发者不要使用预览器进行功能调试,预览器的主要功能是调试界面组件,若用于功能调试可能会出现如下报错:
- TypeError: undefined is not callable
部分常见错误用法已增加维测手段覆盖,详见使用Node-API接口产生的异常日志/崩溃分析。
四、25.9鸿蒙化中C++库使用整理
简介
鸿蒙除了支持使用ArkTS开发外,开发者还可以通过Node-API实现ArkTS与C/C++(Native)的跨语言交互能力。参考ArkTS跨语言交互官方指南、NDK开发(包括NDK开发导读、创建NDK工程、构建NDK工程、代码开发、调试和性能分析、硬件兼容性)。备注:NDK 的全称是 Native Development Kit(原生开发工具包)。
HarmonyOS的Node-API是基于Node.js社区版本的扩展实现,但与原生Node-API并不完全兼容。
开发者可参考使用Node-API进行跨语言开发流程,基于Node-API支持的数据类型和接口进行Native能力的开发和封装,并通过在ArkTS侧导入Native模块的方式实现跨语言调用。
Node-API扩展能力接口提供了增强功能,支持更灵活的ArkTS交互和自定义对象创建。开发者可结合Node-API的扩展能力进行功能扩展,并参考Node-API开发规范和Node-API常见问题进行跨语言功能开发。
下面整理两种调用C++的方式,流程如下:
一、生成har包供外部module调用
1、创建Native C++类型module
2、CMakeLists.txt配置
默认的CMakeLists.txt脚本中添加了编译所需的源代码、头文件以及三方库,开发者可根据实际工程添加自定义编译参数、函数声明、简单的逻辑控制等。核心代码说明如下:
add_library(entry SHARED napi_init.cpp) # 添加名为entry的库
target_link_libraries(entry PUBLIC libace_napi.z.so) # 构建此可执行文件需要链接的库
参考:native工程实现流程举例、native工程举例、Native工程举例
3、build-profile.json5
模块级build-profile.json5文件中externalNativeOptions参数配置NDK工程C/C++文件编译参数,可以通过path指定CMake脚本路径、arguments配置CMake参数、cppFlags配置C++编译器参数、abiFilters配置编译架构等。举例如下:
TypeScript
"externalNativeOptions": {
"path": "./src/main/cpp/test/CMakeLists.txt",
"arguments": "",
"cppFlags": "",
"abiFilters": [
"arm64-v8a",
"x86_64"
]
}
4、oh-package.json5
文件配置依赖将index.d.ts与cpp文件关联起来。
5、napi_init.cpp
1)、设置模块信息;2)、模块初始化;注意这个文件可以自定义为想要的name。
本文件的设置步骤如下(参考:native工程实现流程举例):
1)、本文件中对外方法的大致流程:调用napi_XXX方法从ArkTS获取参数,然后调用C++方法,然后将结果通过napi_XXX返回给ArkTS。比如napi_get_cb_info 用于在Native(C/C++)代码中,获取从ArkTS(或JavaScript)侧调用时传入的参数、this 对象等信息
2)、通过napi_define_properties在exports对象上挂载 上一步 中实现的Native方法。如napi_define_properties(env, exports, sizeof(desc)/sizeof(desc[0]), desc);
3)、通过napi_module_register方法将导出的模块完成注册
6、index.d.ts
文件中声明提供给TS侧的方法。
- ArkTs调用C/C++模块的方法
在oh-package.json5文件中通过dependencies配置依赖库
在ets文件从so中导入C/C++的方法,如import { getRandomTestData, yyy } from 'libxxx.so';
二、将代码植入其他现有module中使用
Native C++类型module中,TS可以调用native代码,因此其他module中也可以直接调用。步骤如下:
1、按照"生成har包供外部module调用"模块步骤创建Native C++类型module,name=test。C++代码创建了test目录存放,原因:为了支持多个so模块,如果是只有一个可以直接放到src/main/cpp下,无需分子目录。
2、在现有xxx模块的src/main目录下新建一个cpp的目录,然后将步骤1中native module中test目录下的cpp目录拷贝到src/main/cpp/目录下,并改名为test。
3、从"生成har包供外部module调用"中的第2步开始逐步检查并配置
