news 2026/9/30 8:28:16

【HarmonyOS开发小实践】Node-API 开发流程:从创建工程到 ArkTS 调用 C++

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【HarmonyOS开发小实践】Node-API 开发流程:从创建工程到 ArkTS 调用 C++

Node-API 开发流程:从创建工程到 ArkTS 调用 C++

ArkTS 跑在虚拟机里,C++ 跑在 native 层,两者之间隔着一层抽象。要把图像编解码、加密、信号处理这类计算密集的活儿交给 C++ 干,又想让 ArkTS 调过去,就得靠 Node-API 这套桥。这篇就把从建工程到 ArkTS 调通 C++ 函数的整条链路走一遍,每一处该改什么文件、写什么代码都给出来。

Node-API 是什么

Node-API 是 ArkTS/JS 与 C/C++ 交互的桥梁,本质是一组 C 接口(napi_* 系列),由方舟运行时提供。Native 侧用 C++ 实现功能,通过 Node-API 把 C++ 函数注册成 ArkTS 可调用的方法;ArkTS 侧 import 一个 .so 库就能像调普通函数一样调过去。

整个交互的链路是这样的:

ArkTS 侧
import libentry.so

加载 so
触发 napi_module_register

调用 Init
注册 napi_property_descriptor

ArkTS 调 callNative

Native 侧 CallNative 执行

返回 napi_value
回 ArkTS

关键在于:so 加载时会自动调用一个用__attribute__((constructor))修饰的函数,这个函数把模块注册到系统中;注册时指定的Init函数负责把 ArkTS 名字和 C++ 函数绑定起来。理解了这两步,剩下的就是填空。

创建 Native C++ 工程

在 DevEco Studio 里走New > Create Project,选Native C++模板,Next,选 API 版本,填工程名,Finish。模板会自动把 cpp 侧骨架和 ets 侧调用样例都生成好,不用从零搭。

建完之后工程结构分两块:

entry/src/main/ ├── cpp/ # Native 侧 │ ├── napi_init.cpp # 模块注册 + 方法实现 │ ├── CMakeLists.txt # CMake 打包配置 │ └── types/libentry/ │ ├── index.d.ts # ArkTS 侧的类型声明 │ └── oh-package.json5 # 把 d.ts 和 cpp 关联起来 └── ets/pages/Index.ets # ArkTS 侧调用方

模板生成的napi_init.cpp里已经写好了模块注册的样板代码,开发者只需要补 Init 里的描述符和具体的 C++ 函数实现。

Native 侧的实现

模块注册

so 被加载时,第一个被调用的是napi_module_register。它把一个napi_module结构体注册到系统里。这个结构体有两个关键字段:

  • nm_register_func:模块初始化函数,负责把 ArkTS 接口和 C++ 函数绑定起来
  • nm_modname:模块名,决定了 ArkTS 侧 import 的 so 名
// entry/src/main/cpp/napi_init.cpp// 准备模块加载相关信息staticnapi_module demoModule={.nm_version=1,.nm_flags=0,.nm_filename=nullptr,.nm_register_func=Init,// 模块初始化函数.nm_modname="entry",// 模块名,对应 libentry.so.nm_priv=((void*)0),.reserved={0},};// 加载 so 时该函数自动被调用,把 demoModule 注册到系统中extern"C"__attribute__((constructor))voidRegisterDemoModule(){napi_module_register(&demoModule);}

__attribute__((constructor))这个 GCC 扩展告诉链接器:so 一被 dlopen 进来就先跑这个函数。所以注册是自动发生的,ArkTS 侧 import 时就触发了整条链路。

模块初始化

Init函数拿到一个napi_env(代表当前 ArkTS 环境)和一个napi_value exports(导出对象),把 C++ 函数挂到 exports 上,ArkTS 侧就能调到。

EXTERN_C_STARTstaticnapi_valueInit(napi_env env,napi_value exports){// 描述符数组:每一行把一个 ArkTS 名字绑定到一个 C++ 函数napi_property_descriptor desc[]={{"callNative",nullptr,CallNative,nullptr,nullptr,nullptr,napi_default,nullptr},{"nativeCallArkTS",nullptr,NativeCallArkTS,nullptr,nullptr,nullptr,napi_default,nullptr}};napi_define_properties(env,exports,sizeof(desc)/sizeof(desc[0]),desc);returnexports;}EXTERN_C_END

napi_property_descriptor这个结构体字段很多,但常用的就前三个:ArkTS 侧的方法名、C++ 侧的实现函数指针。其余的 setter/getter、属性特性这里用不上,填nullptr就行。

类型声明与包关联

ArkTS 侧 import 进来要有类型提示,靠index.d.ts提供:

// entry/src/main/cpp/types/libentry/index.d.tsexportconstcallNative:(a:number,b:number)=>number;exportconstnativeCallArkTS:(cb:(a:number)=>number)=>number;

再用oh-package.json5把这个 d.ts 和 so 关联起来:

// 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 配置决定 so 怎么编出来。模板生成的版本已经够用,关键是add_library那一行决定了 so 的名字,target_link_libraries把 Node-API 的运行时库链进来:

# entry/src/main/cpp/CMakeLists.txt cmake_minimum_required(VERSION 3.4.1) project(MyApplication) set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) include_directories(${NATIVERENDER_ROOT_PATH} ${NATIVERENDER_ROOT_PATH}/include) # 添加名为 entry 的库 → 产物是 libentry.so add_library(entry SHARED napi_init.cpp) # 链接 Node-API 运行时 target_link_libraries(entry PUBLIC libace_napi.z.so)

add_library(entry SHARED ...)的第一个参数entry就是 so 名的来源,最后产物是libentry.so,必须和napi_module.nm_modname一致。

实现 C++ 函数

模板里两个示例函数刚好覆盖了两种典型用法:ArkTS 调 C++、C++ 调 ArkTS。

CallNative:ArkTS 调 C++,做加法

staticnapi_valueCallNative(napi_env env,napi_callback_info info){size_t argc=2;napi_value args[2]={nullptr};// 从 info 里取出 ArkTS 传进来的参数napi_get_cb_info(env,info,&argc,args,nullptr,nullptr);// 把 napi_value 转成 C++ 的 doubledoublevalue0;napi_get_value_double(env,args[0],&value0);doublevalue1;napi_get_value_double(env,args[1],&value1);// 算完,把结果包回 napi_value 返回napi_value sum;napi_create_double(env,value0+value1,&sum);returnsum;}

NativeCallArkTS:C++ 调 ArkTS 回调

staticnapi_valueNativeCallArkTS(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);// 构造一个 int32 作为调用 ArkTS callback 时的入参napi_value argv=nullptr;napi_create_int32(env,2,&argv);// 调用 ArkTS 传进来的 callbacknapi_value result=nullptr;napi_call_function(env,nullptr,args[0],1,&argv,&result);returnresult;}

这里有个套路要记住:所有跨边界的数据都是napi_value,C++ 这边不能直接当成int/double用,必须通过napi_get_value_*取出来,算完再用napi_create_*包回去。这一进一出的转换就是 Node-API 的主要开销所在。

ArkTS 侧调用

ArkTS 侧就一行 import,之后当普通模块用:

// entry/src/main/ets/pages/Index.etsimportnativeModulefrom'libentry.so'@Entry@Componentstruct Index{@Statemessage:string='Test Node-API callNative result: ';@Statemessage2:string='Test Node-API nativeCallArkTS result: ';build(){Row(){Column(){Text(this.message).fontSize(50).fontWeight(FontWeight.Bold).onClick(()=>{// 调 C++ 的 CallNative,做 2 + 3this.message+=nativeModule.callNative(2,3);})Text(this.message2).fontSize(50).fontWeight(FontWeight.Bold).onClick(()=>{// 把箭头函数传给 C++,C++ 内部再调回来this.message2+=nativeModule.nativeCallArkTS((a:number)=>{returna*2;});})}.width('100%')}.height('100%')}}

import nativeModule from 'libentry.so'这一句背后发生的事情:dlopen 加载 libentry.so → 触发 RegisterDemoModule → 调用 Init → 把 callNative/nativeCallArkTS 挂到 exports 上 → ArkTS 拿到一个有这两个方法的对象。

完整数据流

把一次callNative(2, 3)的完整调用过程拆开看:

CallNative(C++)libentry.soArkTSCallNative(C++)libentry.soArkTSnapi_get_cb_info 取参napi_get_value_double ×2value0 + value1import 'libentry.so'dlopen → RegisterDemoModulenapi_module_register(&demoModule)Init(env, exports)返回 exports 对象nativeModule.callNative(2, 3)napi_create_double → 返回 5

各文件职责一览

文件作用谁来写
napi_init.cpp模块注册 + C++ 函数实现开发者补 Init 描述符和函数体
CMakeLists.txt决定 so 名、链接运行时库模板生成,按需加源文件
index.d.tsArkTS 侧类型声明开发者按导出方法写
oh-package.json5关联 d.ts 和 so模板生成
Index.etsArkTS 调用方开发者写业务

案例:在 Native 侧做字符串拼接

加法例子太轻,来个稍微像样点的——ArkTS 传两个字符串进 C++,C++ 拼接后返回。重点看字符串类型怎么处理。

C++ 侧:

staticnapi_valueConcatStrings(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);// 取字符串:先拿长度,再分配 buffer,再拷贝size_t len1=0,len2=0;napi_get_value_string_utf8(env,args[0],nullptr,0,&len1);napi_get_value_string_utf8(env,args[1],nullptr,0,&len2);std::strings1(len1,'\0'),s2(len2,'\0');napi_get_value_string_utf8(env,args[0],&s1[0],len1+1,&len1);napi_get_value_string_utf8(env,args[1],&s2[0],len2+1,&len2);// 拼接std::string result=s1+" "+s2;// 包回 napi_valuenapi_value ret;napi_create_string_utf8(env,result.c_str(),result.size(),&ret);returnret;}

Init 里挂上:

{"concatStrings",nullptr,ConcatStrings,nullptr,nullptr,nullptr,napi_default,nullptr},

d.ts 里声明:

exportconstconcatStrings:(a:string,b:string)=>string;

ArkTS 调用:

constr=nativeModule.concatStrings('Hello','Node-API');console.log(r);// "Hello Node-API"

字符串 API 的套路是"两次调用":第一次传nullptr拿长度,第二次分配好 buffer 再拷贝。这是 Node-API 里字符串处理的固定模式,写多了就形成肌肉记忆。

实践中要注意的

  • so 名必须对齐:add_library(entry ...)决定 so 名为libentry.so,napi_module.nm_modname必须是"entry",ArkTS 侧import必须写'libentry.so',三处不一致就加载不到。
  • Init 函数加 static:防止和其他 so 里的同名函数符号冲突,多模块工程里这点容易漏。
  • 注册入口函数名别重复:__attribute__((constructor))修饰的函数名(如RegisterDemoModule)要保证全工程唯一,否则符号表会打架。
  • napi_value 不能跨调用缓存:每次调用拿到的napi_value只在本次调用上下文有效,想长期持有要用napi_create_reference创建引用。
  • env 不能跨线程:napi_env和创建它的 ArkTS 线程绑定,跨线程用会 crash。这条下一篇细讲。
  • 预览器调不通 Native:DevEco 的预览器只渲染组件,不加载 so,调 native 会报TypeError: undefined is not callable。功能调试得用模拟器或真机。

总结一下下

  • 模板生成的代码已经把最繁琐的注册部分写好了,别复制粘贴网上的旧示例覆盖掉,容易把EXTERN_C_START/EXTERN_C_END这些宏搞丢,导致 C++ 符号 name mangling 不对,加载时报符号找不到。
  • 写完 C++ 改了 .cpp 不生效,多半是 CMake 缓存没刷新。DevEco 里 clean 一下再 build,或者直接删entry/build重建。
  • 类型声明index.d.ts别偷懒不写。不写也能跑,但 ArkTS 侧 import 进来是any,IDE 不提示,传错类型 Native 侧取出来是垃圾值,排查很痛苦。
  • 性能敏感的调用尽量批量传数据,别在循环里反复跨边界调 native。一次跨边界传一个 ArrayBuffer 进去做完再拿回来,比循环里调 N 次快得多。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 8:28:02

COMSOL横波激励仿真全攻略:从物理原理到建模实操

搞过超声仿真或者接触过压电换能器的人应该都听过“横波激励”这个词。刚入行那会儿,我对着COMSOL里那堆物理场接口和边界条件看了好几天,愣是没搞明白怎么让模型里产生一列干净的横波。后来踩了不少坑,翻了无数篇案例文档,才算是…

作者头像 李华
网站建设 2026/9/30 8:26:59

基于WinPcap的ARP数据包解析:绕过以太网帧头实现协议字段提取

简介:面向计算机网络课程设计,这份报告以“解析Ethernet ARP 数据包”为主题,完整呈现了基于WinPcap/PCAP库的网络抓包与解析方案。内容涵盖问题描述、ARP基本原理、概要设计、详细设计及代码实现,包括PCAP_findalldevs、pcap_ope…

作者头像 李华
网站建设 2026/9/30 8:26:41

LeetCode 49 字母异位词分组:哈希key设计是通关关键

如果你刷过LeetCode,尤其是按着“热门100题”列表一路练过去,那第49题《字母异位词分组》大概率是你很早就碰到的又高频又亲民的一道。我第一次刷它的时候,觉得这题不过如此,无非是排序一下、用哈希表存一存。可后来在一次模拟面试…

作者头像 李华
网站建设 2026/9/30 8:26:02

从10G到400G:结构化布线必须重构的链路架构要点

简介:《10G到400G结构化布线指南》是康宁光通信推出的网络基础设施参考文档,面向网络管理员、数据中心运维及技术人员,围绕企业网络从10G向400G演进的实际需求,提供结构化规划、设计与升级路径。整包仅1个PDF文件,大小…

作者头像 李华
网站建设 2026/9/30 8:25:35

LeetCode 5 最长回文子串:从暴力到中心扩展与动态规划

不想花里胡哨,直接说结论: 最长回文子串 这道题,是 LeetCode 第 5 题,也是我刷题生涯中遇到的第一道“标准 DP 入门题”,更是很多人面试时被问到手心冒汗的经典题。它表面上是求一个字符串里的最长回文片段&#xff…

作者头像 李华
网站建设 2026/9/30 8:25:32

打印表格字体变异排查与实战:@media print字体回退与样式修复指南

1. 问题现场:一张表格从屏幕到纸张的“变形记”1.1 用户看到的是“字体变异”,我看到的是另一个世界上个月在维护一个内部报表系统的时候,我突然被一条语音炸到了。用户一边发消息一边拍屏幕说:“屏幕上面明明好好的,一…

作者头像 李华