摘要 : 公司有一套跑了六年的 C 语言校验和/加解密库,要搬进鸿蒙 App。我带着 JNI 的经验上手,以为 NAPI 就是"换个名字的 JNI",结果第一天就卡在模块加载失败------
.so名、注册名、import路径三处对不上,日志里只有一句module not found。本文把这条桥接链路完整走一遍:工程骨架怎么搭(CMakeLists + 类型声明 + 模块注册)、ArkTS 与 C 的类型怎么映射、耗时操作怎么走异步不卡 UI、5 个真实卡点的定位过程。文中代码基于官方 Native 模板与 Node-API 标准接口,性能数据标注"待实测",不做数字编造。
适用版本: HarmonyOS NEXT 7.x / API 14+ / DevEco Studio 5.x(Native 开发,以当前 SDK 文档为准)
环境说明 : 本文工程结构与接口签名基于官方 Native 模板和 Node-API 标准整理,机型性能数据、批量耗时均标注"待实测",实测后回补。
文章目录
-
- [开篇:带着 JNI 经验上手,第一天卡在模块加载](#开篇:带着 JNI 经验上手,第一天卡在模块加载)
- [一、先看清 NAPI 在鸿蒙里的位置](#一、先看清 NAPI 在鸿蒙里的位置)
- [二、工程骨架:native 模块怎么搭](#二、工程骨架:native 模块怎么搭)
-
- [2.1 目录结构](#2.1 目录结构)
- [2.2 CMakeLists.txt](#2.2 CMakeLists.txt)
- [2.3 模块注册与函数导出](#2.3 模块注册与函数导出)
- [2.4 类型声明(容易漏的一步)](#2.4 类型声明(容易漏的一步))
- [三、类型转换:ArkTS 与 C 的边界](#三、类型转换:ArkTS 与 C 的边界)
- [四、异步:别让 native 卡住 UI](#四、异步:别让 native 卡住 UI)
- [五、5 个真实卡点与定位过程](#五、5 个真实卡点与定位过程)
-
- [1. `libentry.so` 找不到](#1.
libentry.so找不到) - [2. 类型声明与实现不一致](#2. 类型声明与实现不一致)
- [3. 字符串被截断](#3. 字符串被截断)
- [4. 工作线程里调 NAPI 接口直接崩](#4. 工作线程里调 NAPI 接口直接崩)
- [5. 引用没释放,内存只涨](#5. 引用没释放,内存只涨)
- [1. `libentry.so` 找不到](#1.
- 六、构建产物与调试
- 七、边界与已知限制
- 小结
开篇:带着 JNI 经验上手,第一天卡在模块加载
公司那套 C 库是六年前写的,纯 C89 风格,几千行,跑在服务端和 Android 上。鸿蒙项目启动后,第一个要解决的问题就是:这堆 C 代码怎么让 ArkTS 调得到。
我当时的判断是"不就是 JNI 换皮"。毕竟概念上很像:Java 有 JNI,ArkTS 有 NAPI;Android 编译出 .so 打进 APK,鸿蒙也是编译出 .so 打进 HAP。命名都差不多,napi_ 前缀对应 JNI 的 (*env)->。
真正动手才发现,最容易翻车的不是写代码,是"三处名字对不上"。第一天的报错只有一句:
text
Error: Cannot find module 'libentry.so' or its corresponding type declarations.
我把这个报错贴到搜索框里,答案五花八门。最后自己把链路捋了一遍才明白:.so 的文件名、napi_module 的注册名、ArkTS 里 import 的模块名、以及类型声明目录的命名,这四处必须严丝合缝,缺一个就报上面这句------而报错信息完全不会告诉你到底是哪一处错了。
这篇文章就是把这个链路按"能跑通"的顺序写清楚,顺便把后面踩到的 5 个坑一起说掉。
一、先看清 NAPI 在鸿蒙里的位置
NAPI 是 Node-API 标准在鸿蒙上的实现,接口签名与 Node.js 的 Node-API 基本一致(基于 Node 18 的 ABI 稳定接口)。它的作用就是给 ArkTS 和 C/C++ 之间划一条受控的边界 :ArkTS 侧不直接操作 native 内存,所有数据都要经过 napi_value 装箱转换。
#mermaid-svg-VE5duFHOddHUI2Tk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VE5duFHOddHUI2Tk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VE5duFHOddHUI2Tk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VE5duFHOddHUI2Tk .error-icon{fill:#552222;}#mermaid-svg-VE5duFHOddHUI2Tk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VE5duFHOddHUI2Tk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VE5duFHOddHUI2Tk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VE5duFHOddHUI2Tk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VE5duFHOddHUI2Tk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VE5duFHOddHUI2Tk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VE5duFHOddHUI2Tk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VE5duFHOddHUI2Tk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VE5duFHOddHUI2Tk .marker.cross{stroke:#333333;}#mermaid-svg-VE5duFHOddHUI2Tk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VE5duFHOddHUI2Tk p{margin:0;}#mermaid-svg-VE5duFHOddHUI2Tk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VE5duFHOddHUI2Tk .cluster-label text{fill:#333;}#mermaid-svg-VE5duFHOddHUI2Tk .cluster-label span{color:#333;}#mermaid-svg-VE5duFHOddHUI2Tk .cluster-label span p{background-color:transparent;}#mermaid-svg-VE5duFHOddHUI2Tk .label text,#mermaid-svg-VE5duFHOddHUI2Tk span{fill:#333;color:#333;}#mermaid-svg-VE5duFHOddHUI2Tk .node rect,#mermaid-svg-VE5duFHOddHUI2Tk .node circle,#mermaid-svg-VE5duFHOddHUI2Tk .node ellipse,#mermaid-svg-VE5duFHOddHUI2Tk .node polygon,#mermaid-svg-VE5duFHOddHUI2Tk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VE5duFHOddHUI2Tk .rough-node .label text,#mermaid-svg-VE5duFHOddHUI2Tk .node .label text,#mermaid-svg-VE5duFHOddHUI2Tk .image-shape .label,#mermaid-svg-VE5duFHOddHUI2Tk .icon-shape .label{text-anchor:middle;}#mermaid-svg-VE5duFHOddHUI2Tk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VE5duFHOddHUI2Tk .rough-node .label,#mermaid-svg-VE5duFHOddHUI2Tk .node .label,#mermaid-svg-VE5duFHOddHUI2Tk .image-shape .label,#mermaid-svg-VE5duFHOddHUI2Tk .icon-shape .label{text-align:center;}#mermaid-svg-VE5duFHOddHUI2Tk .node.clickable{cursor:pointer;}#mermaid-svg-VE5duFHOddHUI2Tk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VE5duFHOddHUI2Tk .arrowheadPath{fill:#333333;}#mermaid-svg-VE5duFHOddHUI2Tk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VE5duFHOddHUI2Tk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VE5duFHOddHUI2Tk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VE5duFHOddHUI2Tk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VE5duFHOddHUI2Tk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VE5duFHOddHUI2Tk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VE5duFHOddHUI2Tk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VE5duFHOddHUI2Tk .cluster text{fill:#333;}#mermaid-svg-VE5duFHOddHUI2Tk .cluster span{color:#333;}#mermaid-svg-VE5duFHOddHUI2Tk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VE5duFHOddHUI2Tk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VE5duFHOddHUI2Tk rect.text{fill:none;stroke-width:0;}#mermaid-svg-VE5duFHOddHUI2Tk .icon-shape,#mermaid-svg-VE5duFHOddHUI2Tk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VE5duFHOddHUI2Tk .icon-shape p,#mermaid-svg-VE5duFHOddHUI2Tk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VE5duFHOddHUI2Tk .icon-shape .label rect,#mermaid-svg-VE5duFHOddHUI2Tk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VE5duFHOddHUI2Tk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VE5duFHOddHUI2Tk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VE5duFHOddHUI2Tk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Native 层
桥接层
ArkTS 层
ArkTS 业务代码
import xxx from 'libxxx.so'
index.d.ts 类型声明
napi_module 注册表
Init 中 napi_define_properties
C/C++ 实现函数
三方库源码 / 预编译 .so
libace_napi.z.so
libhilog_ndk.z.so
和 JNI 的差异,按我实际踩过的点整理成表:
| 对比项 | JNI(Android) | NAPI(HarmonyOS) |
|---|---|---|
| 接口风格 | (*env)->GetStringUTFChars(env, ...) |
napi_get_value_string_utf8(env, ...) |
| 模块注册 | JNI_OnLoad / RegisterNatives |
napi_module 结构体 + constructor 注册 |
| 加载方式 | System.loadLibrary |
import xxx from 'libxxx.so' |
| 类型声明 | 手写 native 方法签名 |
index.d.ts + oh-package.json5 的 types 字段 |
| 异常机制 | ThrowNew + ExceptionCheck |
napi_throw_error + 返回值 napi_status |
| 异步 | 自己开线程 + AttachCurrentThread |
napi_create_async_work / threadsafe_function |
| 日志 | __android_log_print |
OH_LOG_Print(libhilog_ndk.z.so) |
一句话总结差异:JNI 是"自己管一切" ,线程、引用、异常都要手动处理;NAPI 提供了受控的异步与引用管理机制,但也因此多了 async_work、reference、threadsafe_function 这些必须理解的概念。
二、工程骨架:native 模块怎么搭
2.1 目录结构
一个最小可用的 native 模块长这样(以模块名 entry 为例):
text
entry/src/main/cpp/
├── CMakeLists.txt # 编译配置
├── napi_init.cpp # 模块注册 + 导出函数
└── types/libentry/ # 类型声明目录,目录名必须与 .so 名一致
├── index.d.ts
└── oh-package.json5
2.2 CMakeLists.txt
cmake
cmake_minimum_required(VERSION 3.4.1)
project(NativeBridge)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
include_directories(
${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include # 三方库头文件
)
# 三方库源码(也可以用预编译 .so + IMPORTED 方式引入)
add_library(legacy STATIC src/legacy_checksum.c)
# native 模块,输出 libentry.so
add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC
legacy
libace_napi.z.so
libhilog_ndk.z.so
)
2.3 模块注册与函数导出
cpp
#include <napi/native_api.h>
#include <hilog/log.h>
#include "legacy_checksum.h"
// 实际导出给 ArkTS 的函数
static napi_value Checksum(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);
// 字符串长度要"先取长度、再取内容"两步
size_t len = 0;
napi_get_value_string_utf8(env, args[0], nullptr, 0, &len);
char* buf = new char[len + 1];
napi_get_value_string_utf8(env, args[0], buf, len + 1, &len);
uint32_t result = legacy_checksum(buf);
delete[] buf;
napi_value out;
napi_create_uint32(env, result, &out);
return out;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc[] = {
{"checksum", nullptr, Checksum, nullptr, nullptr, nullptr, napi_default, nullptr},
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
static napi_module bridgeModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry", // 必须与 .so 名一致(libentry.so → entry)
.nm_priv = ((void*)0),
.reserved = {0},
};
extern "C" __attribute__((constructor)) void RegisterBridgeModule(void) {
napi_module_register(&bridgeModule);
}
2.4 类型声明(容易漏的一步)
types/libentry/index.d.ts:
typescript
export const checksum: (input: string) => number;
types/libentry/oh-package.json5:
json5
{
"name": "libentry.so",
"types": "./index.d.ts",
"version": "1.0.0",
"description": "native bridge"
}
ArkTS 侧就可以直接用了:
typescript
import nativeBridge from 'libentry.so';
const result = nativeBridge.checksum('hello harmony');
三、类型转换:ArkTS 与 C 的边界
所有跨边界的数据都要过一次转换。常用的映射关系:
| ArkTS 类型 | NAPI 取值 | NAPI 构造 | 注意点 |
|---|---|---|---|
| string | napi_get_value_string_utf8 |
napi_create_string_utf8 |
必须两次调用,第一次取长度 |
| number | napi_get_value_int32/uint32/double |
napi_create_int32/double |
注意整型溢出 |
| boolean | napi_get_value_bool |
napi_get_boolean |
|
| ArrayBuffer | napi_get_arraybuffer_info |
napi_create_arraybuffer |
大数据首选,避免拷贝 |
| Array | napi_get_array_length + napi_get_element |
napi_create_array |
逐元素操作,量大时慢 |
| Object | napi_get_named_property |
napi_create_object |
深层嵌套建议改 JSON 字符串 |
| Function | napi_get_cb_info 取出 |
--- | 跨线程回调要用 threadsafe_function |
经验是:大数据一律走 ArrayBuffer 或文件 Uri,别用 Array 逐元素搬。我第一次把 2000 个点的坐标用数组传过去,转换本身就吃掉几十毫秒(待实测具体量级),换成 ArrayBuffer 一次传完就顺了。
四、异步:别让 native 卡住 UI
NAPI 的导出函数运行在 JS 线程上。如果直接在里面跑耗时计算,UI 就会卡住。官方给的机制是 napi_create_async_work:execute 在工作线程跑,complete 回到 JS 线程拿结果。
工作线程 NAPI 运行时 ArkTS(JS 线程) 工作线程 NAPI 运行时 ArkTS(JS 线程) #mermaid-svg-DeCIGneu5esc5193{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DeCIGneu5esc5193 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DeCIGneu5esc5193 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DeCIGneu5esc5193 .error-icon{fill:#552222;}#mermaid-svg-DeCIGneu5esc5193 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DeCIGneu5esc5193 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DeCIGneu5esc5193 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DeCIGneu5esc5193 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DeCIGneu5esc5193 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DeCIGneu5esc5193 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DeCIGneu5esc5193 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DeCIGneu5esc5193 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DeCIGneu5esc5193 .marker.cross{stroke:#333333;}#mermaid-svg-DeCIGneu5esc5193 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DeCIGneu5esc5193 p{margin:0;}#mermaid-svg-DeCIGneu5esc5193 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DeCIGneu5esc5193 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-DeCIGneu5esc5193 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DeCIGneu5esc5193 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-DeCIGneu5esc5193 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-DeCIGneu5esc5193 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-DeCIGneu5esc5193 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-DeCIGneu5esc5193 .sequenceNumber{fill:white;}#mermaid-svg-DeCIGneu5esc5193 #sequencenumber{fill:#333;}#mermaid-svg-DeCIGneu5esc5193 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-DeCIGneu5esc5193 .messageText{fill:#333;stroke:none;}#mermaid-svg-DeCIGneu5esc5193 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DeCIGneu5esc5193 .labelText,#mermaid-svg-DeCIGneu5esc5193 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-DeCIGneu5esc5193 .loopText,#mermaid-svg-DeCIGneu5esc5193 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-DeCIGneu5esc5193 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-DeCIGneu5esc5193 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-DeCIGneu5esc5193 .noteText,#mermaid-svg-DeCIGneu5esc5193 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-DeCIGneu5esc5193 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DeCIGneu5esc5193 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DeCIGneu5esc5193 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-DeCIGneu5esc5193 .actorPopupMenu{position:absolute;}#mermaid-svg-DeCIGneu5esc5193 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-DeCIGneu5esc5193 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-DeCIGneu5esc5193 .actor-man circle,#mermaid-svg-DeCIGneu5esc5193 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-DeCIGneu5esc5193 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 调用 native 异步方法(传入 callback) 把入参拷进上下文结构体 napi_create_async_work + queue execute 回调(禁止访问 napi_env) 执行 C 库耗时计算 完成,写回上下文 complete 回调(回到 JS 线程) 调用 ArkTS callback 更新状态
cpp
struct AsyncCtx {
napi_async_work work = nullptr;
napi_ref callbackRef = nullptr;
char* input = nullptr;
uint32_t result = 0;
};
static void Execute(napi_env env, void* data) {
auto* ctx = static_cast<AsyncCtx*>(data);
ctx->result = legacy_checksum(ctx->input); // 工作线程,不能调 napi_* 接口
}
static void Complete(napi_env env, napi_status status, void* data) {
auto* ctx = static_cast<AsyncCtx*>(data);
napi_value callback = nullptr;
napi_get_reference_value(env, ctx->callbackRef, &callback);
napi_value argv[1];
napi_create_uint32(env, ctx->result, &argv[0]);
napi_value undefined = nullptr;
napi_get_undefined(env, &undefined);
napi_call_function(env, undefined, callback, 1, argv, nullptr);
napi_delete_reference(env, ctx->callbackRef);
napi_delete_async_work(env, ctx->work);
delete[] ctx->input;
delete ctx;
}
两条硬规则,违反必崩:
execute里不能调用任何napi_*接口(除了少数线程安全的),它不在 JS 线程上。complete里才能把结果转成napi_value并回调 ArkTS。
五、5 个真实卡点与定位过程
1. libentry.so 找不到
现象 :编译通过,运行报 Cannot find module 'libentry.so'。
定位 :逐项比对四处命名------CMakeLists 里 add_library(entry SHARED ...) 产出 libentry.so;napi_module 的 nm_modname 必须是 "entry";types/ 下的目录名必须是 libentry;ArkTS 里 import 的必须是 'libentry.so'。
解决 :我当时错在 nm_modname 写成了 "libentry"(多加了 lib 前缀)。去掉前缀后正常。
教训:这四处命名没有任何编译期校验,只能靠人工对齐。建议建模块时直接复制模板,别手敲。
2. 类型声明与实现不一致
现象 :ArkTS 侧调用返回 undefined,编译期无报错。
定位 :index.d.ts 里声明了 checksum(input: string): number,但 native 侧 napi_define_properties 导出的名字写成了 checkSum(大写 S)。ArkTS 按声明认为函数存在,运行时找不到导出项。
解决 :统一命名;并在 index.d.ts 与 C 代码之间建立一一对应关系,改一处必须同步改另一处。
教训 :.d.ts 是承诺不是校验。命名不一致只在运行期暴露,且报错信息没有指向性。
3. 字符串被截断
现象:传入长字符串,计算结果偶尔不对。
定位 :只调了一次 napi_get_value_string_utf8,用固定大小的栈上 buffer 接收。超过 buffer 长度的部分被丢掉,且不报错。
解决 :改成标准的两步调用------第一次传 nullptr 和长度 0 拿到真实长度,再按长度分配内存取内容。
教训:NAPI 的字符串接口设计就是两步,想省一次调用必然踩坑。
4. 工作线程里调 NAPI 接口直接崩
现象 :把耗时计算放进 execute 后,偶发崩溃,日志只有信号号。
定位 :在 execute 里调用了 napi_create_string_utf8 想把中间结果转字符串打日志。这个接口必须在 JS 线程调用。
解决 :日志改用 OH_LOG_Print(hilog 是线程安全的),napi_* 调用全部挪到 complete。
教训 :execute 与 complete 的边界是硬性的,不是"建议"。
5. 引用没释放,内存只涨
现象:反复调用异步方法,内存持续增长。
定位 :保存 callback 用了 napi_create_reference,但异常分支提前 return,没走 napi_delete_reference。
解决 :所有退出路径都要释放;napi_async_work 同理,complete 里必须 napi_delete_async_work。
教训:NAPI 的引用不是 GC 管理的,创建与删除必须成对,异常分支最容易漏。
六、构建产物与调试
#mermaid-svg-aTICb4Kf7qTQDysV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aTICb4Kf7qTQDysV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aTICb4Kf7qTQDysV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aTICb4Kf7qTQDysV .error-icon{fill:#552222;}#mermaid-svg-aTICb4Kf7qTQDysV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aTICb4Kf7qTQDysV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aTICb4Kf7qTQDysV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aTICb4Kf7qTQDysV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aTICb4Kf7qTQDysV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aTICb4Kf7qTQDysV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aTICb4Kf7qTQDysV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aTICb4Kf7qTQDysV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aTICb4Kf7qTQDysV .marker.cross{stroke:#333333;}#mermaid-svg-aTICb4Kf7qTQDysV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aTICb4Kf7qTQDysV p{margin:0;}#mermaid-svg-aTICb4Kf7qTQDysV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aTICb4Kf7qTQDysV .cluster-label text{fill:#333;}#mermaid-svg-aTICb4Kf7qTQDysV .cluster-label span{color:#333;}#mermaid-svg-aTICb4Kf7qTQDysV .cluster-label span p{background-color:transparent;}#mermaid-svg-aTICb4Kf7qTQDysV .label text,#mermaid-svg-aTICb4Kf7qTQDysV span{fill:#333;color:#333;}#mermaid-svg-aTICb4Kf7qTQDysV .node rect,#mermaid-svg-aTICb4Kf7qTQDysV .node circle,#mermaid-svg-aTICb4Kf7qTQDysV .node ellipse,#mermaid-svg-aTICb4Kf7qTQDysV .node polygon,#mermaid-svg-aTICb4Kf7qTQDysV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aTICb4Kf7qTQDysV .rough-node .label text,#mermaid-svg-aTICb4Kf7qTQDysV .node .label text,#mermaid-svg-aTICb4Kf7qTQDysV .image-shape .label,#mermaid-svg-aTICb4Kf7qTQDysV .icon-shape .label{text-anchor:middle;}#mermaid-svg-aTICb4Kf7qTQDysV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aTICb4Kf7qTQDysV .rough-node .label,#mermaid-svg-aTICb4Kf7qTQDysV .node .label,#mermaid-svg-aTICb4Kf7qTQDysV .image-shape .label,#mermaid-svg-aTICb4Kf7qTQDysV .icon-shape .label{text-align:center;}#mermaid-svg-aTICb4Kf7qTQDysV .node.clickable{cursor:pointer;}#mermaid-svg-aTICb4Kf7qTQDysV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aTICb4Kf7qTQDysV .arrowheadPath{fill:#333333;}#mermaid-svg-aTICb4Kf7qTQDysV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aTICb4Kf7qTQDysV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aTICb4Kf7qTQDysV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aTICb4Kf7qTQDysV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aTICb4Kf7qTQDysV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aTICb4Kf7qTQDysV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aTICb4Kf7qTQDysV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aTICb4Kf7qTQDysV .cluster text{fill:#333;}#mermaid-svg-aTICb4Kf7qTQDysV .cluster span{color:#333;}#mermaid-svg-aTICb4Kf7qTQDysV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aTICb4Kf7qTQDysV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aTICb4Kf7qTQDysV rect.text{fill:none;stroke-width:0;}#mermaid-svg-aTICb4Kf7qTQDysV .icon-shape,#mermaid-svg-aTICb4Kf7qTQDysV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aTICb4Kf7qTQDysV .icon-shape p,#mermaid-svg-aTICb4Kf7qTQDysV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aTICb4Kf7qTQDysV .icon-shape .label rect,#mermaid-svg-aTICb4Kf7qTQDysV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aTICb4Kf7qTQDysV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aTICb4Kf7qTQDysV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aTICb4Kf7qTQDysV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 无
有
否
是
否
是
否
是
运行报 module not found
或 native crash
HAP 内是否有
对应 ABI 的 .so
检查 abiFilters
与 CMake 产物路径
nm_modname 是否
等于去掉 lib 前缀的 so 名
修正 nm_modname
types 目录名
是否与 so 名一致
重命名 types 目录
并核对 oh-package.json5
index.d.ts 导出名
与 define_properties 是否一致
同步两处命名
看 hilog native 日志
必要时 addr2line 还原堆栈
配置 build-profile.json5 指定 CMake 与 ABI:
json5
{
"buildOption": {
"externalNativeOptions": {
"path": "./src/main/cpp/CMakeLists.txt",
"arguments": "",
"cppFlags": "",
"abiFilters": ["arm64-v8a", "x86_64"]
}
}
}
调试 native 问题时,我常用的三步:
- 确认 .so 是否打进包 :解压 HAP 看
libs/目录下有没有对应 ABI 的产物。 - native 日志 :用
OH_LOG_Print打点,hdc shell hilog过滤 domain。 - 崩溃定位 :native 崩溃日志只有地址,需要用工具链里的
addr2line配合带符号的.so还原堆栈。
七、边界与已知限制
| 限制项 | 具体表现 | 规避方式 |
|---|---|---|
| 实测缺口 | 文中性能数据(转换耗时、批量吞吐)标注"待实测" | 实测后回补,不做数字编造 |
| 线程约束 | execute 回调内不能调用 napi_* 接口 |
只用 OH_LOG_Print,结果回 complete 处理 |
| 类型边界 | 复杂嵌套对象转换成本高、易出错 | 大数据走 ArrayBuffer,复杂结构走 JSON 字符串 |
| ABI 产物 | 目标设备架构不匹配会安装失败或直接崩 | abiFilters 按目标设备配置齐全 |
| 调试成本 | native 崩溃只有地址,没有可读堆栈 | 保留带符号 .so,用 addr2line 还原 |
| 包体增量 | 引入 native 库会明显增大包体 | 裁剪不需要的模块与符号 |
| 许可证 | 三方库 LICENSE 可能带来合规风险 | 引入前核对许可证与声明要求 |
| 版本演进 | NAPI 接口随版本演进,旧示例可能失效 | 以当前 SDK 文档为准 |
小结
把 C/C++ 库搬进鸿蒙,第一步不是写代码,是把四处命名对齐 :add_library 名、nm_modname、类型声明目录名、import 名。这四处没有编译期校验,错一处就只有一句没有指向性的 module not found。
之后真正决定成败的是两个边界:类型转换用两步式 (尤其字符串),异步严格区分 execute 与 complete (前者不能碰 napi_*)。把这两条守住,剩下的就是常规 C 代码问题。
下一篇写移植的另一半:CMake 交叉编译配置,以及链接期符号冲突怎么定位。
评论区交流: 你在鸿蒙上桥接 C/C++ 库时卡在哪一步?是模块加载、类型转换,还是异步回调?评论区说说你的场景和报错,我按链路帮你定位。