【鸿蒙心迹】三方库移植(一)——NAPI 把 C/C++ 库桥接到 ArkTS 的完整链路(HarmonyOS 7.x)

摘要 : 公司有一套跑了六年的 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. 引用没释放,内存只涨)
    • 六、构建产物与调试
    • 七、边界与已知限制
    • 小结

开篇:带着 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;
}

两条硬规则,违反必崩:

  1. execute 里不能调用任何 napi_* 接口(除了少数线程安全的),它不在 JS 线程上。
  2. 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 问题时,我常用的三步:

  1. 确认 .so 是否打进包 :解压 HAP 看 libs/ 目录下有没有对应 ABI 的产物。
  2. native 日志 :用 OH_LOG_Print 打点,hdc shell hilog 过滤 domain。
  3. 崩溃定位 :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++ 库时卡在哪一步?是模块加载、类型转换,还是异步回调?评论区说说你的场景和报错,我按链路帮你定位。

相关推荐
2601_949480061 小时前
开源鸿蒙平台 CMP 三方库「reveal」适配全流程
华为·开源·harmonyos
2401_zq136y031 小时前
开源鸿蒙平台 CMP 三方库「KToast」适配全流程
华为·开源·harmonyos
AI2中文网1 小时前
App Inventor 2 构建纯血鸿蒙 HarmonyOS NEXT App:证书、签名、真机安装到上架全流程
华为·harmonyos
贾伟康1 小时前
【共创稿事节】@State与@Prop的单向流
harmonyos·arkts·状态管理·state·prop
大雷神1 小时前
HarmonyOS7应用《拼棋派对》实战:模拟器回归验证与签名打包
harmonyos·arkui·harmonyos 7.0·玩转 arkts
贾伟康3 小时前
【共创稿事节】@Link双向绑定的适用边界
性能优化·实战·harmonyos·arkts·arkui
熊猫钓鱼>_>3 小时前
上架审核常见驳回与隐私合规清单:过审前先自查这 8 条【鸿蒙心迹】
开发语言·前端·华为·华为云·harmonyos·openharmony·ai制作
贾伟康3 小时前
【共创稿事节】布局审查工具与边界调试
harmonyos·arkui·inspector·性能排查·布局调试
2501_946244783 小时前
开源鸿蒙平台 CMP 三方库「compose-collapsing-toolbar」适配全流程
华为·开源·harmonyos