我是兰瓶Coding,一枚刚踏入鸿蒙领域的转型小白,原是移动开发中级,如下是我学习笔记《零基础学鸿蒙》,若对你所有帮助,还请不吝啬的给个大大的赞~

前言

直说吧:把 C/C++ 能力塞进 ArkTS,不是“能跑”就完了,关键在“线程与事件循环怎么握手”“内存谁说了算”“同步/异步回调怎么不卡 UI”“ABI/架构怎么一次打齐”。这篇我把桥接模型、内存所有权、回调与任务模型、跨架构打包、错误与类型映射掰开揉碎,并给可复用的 C++/ArkTS 样例。风格有人味儿,工程上真能抄;最后给一页 Checklist,交付质检更踏实。走起~

总览:NAPI 桥接三层剖面图

ArkTS (UI/业务)   ←→   NAPI 边界层 (C 接口、类型编解码)   ←→   C/C++ 核心库 (线程/IO/算法)
    |                                     |                                    |
    | Promise/回调/TS类型                  | napi_value / napi_env / TSFN        | std::thread / epoll / libxxx
    | 事件循环(ArkUI/JS引擎)               | AsyncWork/ThreadSafeFunction        | RAII/智能指针/无阻塞
  • ArkTS 层:只做“声明式 API + Promise/回调封装 + 最少同步调用”。
  • NAPI 层把“JS世界”的值安全地翻译成 C/C++,并负责跨线程把结果抛回 JS 事件循环
  • C/C++ 层不感知 JS 引擎。专注算法/IO,遵守不可阻塞 UI 线程明确内存所有权

一、线程与事件循环:别把 ArkTS 主线程“堵死”

1) 同步 vs 异步:什么时候可以同步?

  • 可以同步纯 CPU 小计算(<1ms)、轻量内存转换快速属性读写
  • 必须异步:IO、任何可能>4–8ms 的计算、涉及锁等待的调用、外设访问。

2) NAPI 的两个“异步武器”

  • napi_create_async_work:后台线程跑活,结束后在 JS 线程调用完成回调。适合一次性任务
  • napi_create_threadsafe_function(TSFN):C++ 后台线程多次推送事件到 JS 线程。适合流式/进度

口诀:一次性活→AsyncWork;多次推送→TSFN。两者都会把回调的执行“投递回 JS 事件循环”,避免跨线程直接“碰 JS 引擎”。

二、内存所有权:谁 new 的谁负责,谁传进来的别乱 free

1) JS ↔ C++ 对象的“生死簿”

  • JS 拥有权napi_create_external_arraybuffer / napi_create_external 时,把“释放回调”登记给 GC
  • C++ 拥有权:NAPI 只拿指针,不 free;需在 finalize 回调或**类实例 Finalize**里释放。

2) 零拷贝与大对象

  • 大块二进制(图像/音频):优先用 ArrayBuffer/TypedArray + 外部内存,并注册 finalizer
  • 字符串:避免频繁的 std::string ↔ JS string 大量复制;可考虑一次性转换 + 缓存

三、API 设计:同步/Promise/回调,三种姿势、清清楚楚

  • 同步函数(慎用):function add(a:number,b:number): number
  • Promisefunction compress(data: ArrayBuffer): Promise<Result>
  • 事件流(TSFN):onProgress((n: number) => void) / startStream(): AsyncIterator<Chunk>

TS 端一定要有类型声明.d.ts 或 ArkTS 声明),让调用方看一眼就知道会不会阻塞

四、最小可用样例:同步小函数 + 异步任务 + 进度推送

4.1 CMake 与导出(示意)

# cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(native_addon)
add_library(native_addon SHARED addon.cpp)
target_compile_features(native_addon PRIVATE cxx_std_17)
target_compile_options(native_addon PRIVATE -fvisibility=hidden)
target_link_libraries(native_addon PRIVATE # 需要的系统/第三方库
)

4.2 C++:NAPI 注册、同步/异步/TSFN

// cpp/addon.cpp
#include <napi/native_api.h>
#include <napi/native_node_api.h> // 视平台头文件命名而定
#include <string>
#include <thread>
#include <vector>
#include <atomic>

#define NAPI_CALL(env, call) if ((call) != napi_ok) { napi_throw_error(env, nullptr, "NAPI call failed"); return nullptr; }

/////////////////////////// 同步add ///////////////////////////
static napi_value Add(napi_env env, napi_callback_info info) {
  size_t argc = 2; napi_value argv[2]; napi_value thisArg;
  NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, &thisArg, nullptr));
  double a, b; NAPI_CALL(env, napi_get_value_double(env, argv[0], &a));
  NAPI_CALL(env, napi_get_value_double(env, argv[1], &b));
  napi_value out; NAPI_CALL(env, napi_create_double(env, a + b, &out));
  return out;
}

/////////////////////////// 异步compress(Promise) ///////////////////////////
struct CompressWork {
  napi_async_work work{};
  napi_deferred deferred{};
  std::vector<uint8_t> in, out;
  std::string err;
};

static void DoCompress(napi_env env, void* data) {
  auto* w = static_cast<CompressWork*>(data);
  try {
    // 这里用真实压缩库替换:zstd/lz4等
    w->out = w->in; // demo:假装压缩
  } catch (const std::exception& e) {
    w->err = e.what();
  }
}

static void DoneCompress(napi_env env, napi_status status, void* data) {
  auto* w = static_cast<CompressWork*>(data);
  if (status != napi_ok || !w->err.empty()) {
    napi_value err;
    napi_create_string_utf8(env, w->err.c_str(), NAPI_AUTO_LENGTH, &err);
    napi_reject_deferred(env, w->deferred, err);
  } else {
    napi_value ab; uint8_t* dst;
    napi_create_arraybuffer(env, w->out.size(), (void**)&dst, &ab);
    memcpy(dst, w->out.data(), w->out.size());
    napi_resolve_deferred(env, w->deferred, ab);
  }
  napi_delete_async_work(env, w->work);
  delete w;
}

static napi_value Compress(napi_env env, napi_callback_info info) {
  size_t argc = 1; napi_value argv[1]; napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);
  void* data; size_t len; napi_get_arraybuffer_info(env, argv[0], &data, &len);

  auto* w = new CompressWork();
  w->in.assign((uint8_t*)data, (uint8_t*)data + len);

  napi_value promise;
  napi_create_promise(env, &w->deferred, &promise);

  napi_value resource_name; napi_create_string_utf8(env, "compress", NAPI_AUTO_LENGTH, &resource_name);
  napi_create_async_work(env, nullptr, resource_name, DoCompress, DoneCompress, w, &w->work);
  napi_queue_async_work(env, w->work);
  return promise;
}

/////////////////////////// 进度推送(TSFN) ///////////////////////////
struct ProgressCtx {
  napi_threadsafe_function tsfn{};
  std::atomic<bool> running{true};
  std::thread worker;
};

static void TSFN_Finalize(napi_env env, void* finalize_data, void* /*finalize_hint*/) {
  auto* ctx = static_cast<ProgressCtx*>(finalize_data);
  if (ctx) delete ctx;
}

static void CallJs(napi_env env, napi_value js_cb, void* /*context*/, void* data) {
  // data 是一个 int*,表示进度
  int* p = static_cast<int*>(data);
  napi_value arg; napi_create_int32(env, *p, &arg);
  napi_call_function(env, nullptr, js_cb, 1, &arg, nullptr);
  delete p;
}

static napi_value StartProgress(napi_env env, napi_callback_info info) {
  size_t argc = 1; napi_value argv[1]; napi_get_cb_info(env, info, &argc, argv, nullptr, nullptr);

  napi_value js_cb = argv[0];
  auto* ctx = new ProgressCtx();

  napi_value name; napi_create_string_utf8(env, "progress", NAPI_AUTO_LENGTH, &name);
  napi_create_threadsafe_function(env, js_cb, nullptr, name, 0, 1, ctx, TSFN_Finalize, nullptr, CallJs, &ctx->tsfn);

  ctx->worker = std::thread([ctx] {
    for (int i = 0; i <= 100 && ctx->running.load(); ++i) {
      std::this_thread::sleep_for(std::chrono::milliseconds(50));
      int* payload = new int(i);
      napi_call_threadsafe_function(ctx->tsfn, payload, napi_tsfn_nonblocking);
    }
    napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release);
  });

  // 返回一个停止函数
  napi_value stopFn;
  napi_create_function(env, "stop", NAPI_AUTO_LENGTH,
    [](napi_env env, napi_callback_info info)->napi_value{
      size_t argc=1; napi_value argv[1]; void* data;
      napi_get_cb_info(env, info, &argc, argv, &data, nullptr);
      auto* ctx = static_cast<ProgressCtx*>(data);
      if (ctx) { ctx->running.store(false); if (ctx->worker.joinable()) ctx->worker.join(); }
      return nullptr;
    }, ctx, &stopFn);
  return stopFn;
}

/////////////////////////// 模块注册 ///////////////////////////
static napi_value Init(napi_env env, napi_value exports) {
  napi_property_descriptor desc[] = {
    { "add", 0, Add, 0, 0, 0, napi_default, 0 },
    { "compress", 0, Compress, 0, 0, 0, napi_default, 0 },
    { "startProgress", 0, StartProgress, 0, 0, 0, napi_default, 0 },
  };
  napi_define_properties(env, exports, sizeof(desc)/sizeof(*desc), desc);
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) // 不同平台宏名可能不同,按 NAPI 工具链调整

4.3 ArkTS:声明 + 封装(Promise/回调)

载入方式随平台与 API Level 不同,常见做法:

  • 按模块名加载const addon = globalThis.requireNapi?.('native_addon')
  • 按 so 名称导入import addon from 'libnative_addon.so'(部分版本支持)
    ↓ 这里写成“自适应”封装:
// ets/native/index.ets
type Addon = {
  add(a: number, b: number): number
  compress(data: ArrayBuffer): Promise<ArrayBuffer>
  startProgress(cb: (p: number) => void): () => void // 返回停止函数
}

function loadAddon(): Addon {
  const anyGlobal: any = globalThis as any
  const mod = anyGlobal.requireNapi ? anyGlobal.requireNapi('native_addon') : (anyGlobal.__native_addon__)
  if (!mod) throw new Error('native_addon not loaded')
  return mod as Addon
}

const addon = loadAddon()

// 同步小函数(慎用)
export function add(a: number, b: number) { return addon.add(a, b) }

// 异步压缩(Promise)
export function compress(buf: ArrayBuffer) { return addon.compress(buf) }

// 进度推送
export function runWithProgress(onProgress: (n:number)=>void) {
  const stop = addon.startProgress(onProgress)
  return { stop }
}

五、对象封装:原生类 ↔ ArkTS 对象的绑定(析构要对称)

当你需要跨多次调用维护原生状态(句柄/上下文),用 napi_define_class 暴露一个 ArkTS 可 new 的类。

要点

  • constructor 里 new C++ 对象,挂在 napi_wrap
  • finalize 里 delete,确保内存不漏
  • 方法里 napi_unwrap 拿回 this 的 C++ 指针。

(此处省略长代码,实战时把“压缩器/相机句柄/流客户端”都封成类,配合 close() 与 finalize 双保险。)

六、错误处理与类型映射:错误要“可诊断”,类型要“可预期”

  • NAPI 错误napi_throw_error / napi_throw_type_error;或在 Promise 路径里 reject 标准化错误对象 { code, message }
  • 类型检查:入口先 napi_typeof/napi_is_arraybuffer,参数不对直接 TypeError,别帮用户“糊上去”。
  • 数值边界:C++ size_t → JS number 可能溢出,传BigInt时用 napi_create_bigint_uint64
  • 字符串编码:统一 UTF-8;遇到二进制路径就用 ArrayBuffer/Uint8Array

七、ABI/架构差异:一次产物,打齐多架构

  • 常见 ABIarm64-v8a(主力),armeabi-v7a(旧设备),x86_64(模拟/桌面),(有的目标还会有 riscv64)。

  • 产物布局libs/<abi>/libnative_addon.so

  • CMake 多架构:CI 矩阵编译,产出多份 .so,打入对应 HAP/包体。

  • 可移植性

    • 避免使用未对齐的内存读写(不同架构崩得很干脆)。
    • 统一 sizeof(long) 差异(win/linux/arm 不一)→ 用固定宽度类型
    • 打开 -fvisibility=hidden,只导出 NAPI Init 符号;避免符号冲突。
    • 外部三方库(zstd/openssl…)要同 ABI 编译,别“拿错架构”。

八、性能与安全:快,还得稳

  • 线程数线程池复用;不要每次调用都 new thread。
  • 锁粒度:细化锁或用无锁结构(环形缓冲)避免 TSFN 回调抖动。
  • 大数据路径:优先零拷贝/外部 ArrayBuffer;连续内存更友好。
  • 资源清理:TSFN 记得 napi_release_threadsafe_function;AsyncWork 记得 napi_delete_async_work
  • 安全:所有外来指针/尺寸都边界检查;拒绝非法 len 触发 OOB/UB。
  • 可观测:原生层也打印结构化日志(级别/耗时/错误码),定位问题不靠猜。

九、端到端小演示(ArkTS 调用)

// pages/Demo.ets
@Component
export struct DemoPage {
  @State progress: number = 0
  @State outputSize: number = 0

  async aboutToAppear() {
    // 同步:快进快出
    const s = add(40, 2) // 42
    console.log(`add = ${s}`)

    // 异步:Promise
    const buf = new Uint8Array([1,2,3,4]).buffer
    const out = await compress(buf)
    this.outputSize = (out as ArrayBuffer).byteLength

    // 进度:TSFN
    const { stop } = runWithProgress((p) => this.progress = p)
    setTimeout(() => stop(), 3000) // 3 秒后停止
  }

  build() {
    Column({ space: 12 }) {
      Text(`Output: ${this.outputSize} bytes`)
      Text(`Progress: ${this.progress}%`)
    }.padding(16)
  }
}

十、交付前 Checklist(把坑盖上 ✅)

线程与事件循环

  • 任何可能>4–8ms 的任务均走 AsyncWork/TSFN,绝不阻塞 UI。
  • TSFN 有 release,AsyncWork 有 delete,worker 线程能 join/stop

内存与生命周期

  • 外部内存注册 finalizer;原生类有 Finalizeclose() 双保险。
  • 无内存泄漏:跑 5 分钟压测,RSS 稳定。

API 行为

  • TS 类型声明齐全,标明同步/异步与异常。
  • 错误统一为 {code,message}/TypeError,拒绝“沉默失败”。

ABI/构建

  • 多 ABI 构建矩阵通过,产物正确落到 libs/<abi>/libxxx.so
  • 第三方库与主库 ABI 一致;符号可见性收紧。

性能

  • 大对象一路 零拷贝 或最少拷贝;无重复转换。
  • 压测 P95 延迟达标(根据业务设阈值),回调不抖。

小结

桥接不是把“C++ 函数丢给 ArkTS”那么简单。你要管住线程/事件循环的边界,盯紧内存所有权,把异步回调“安安稳稳”送回 JS,再让ABI一次打齐。只要按上面的套路落地:小活同步、大活异步、流式用 TSFN、对象有 finalize、产物有多 ABI——原生能力就能在 ArkTS 里快且稳地飞起来。

(未完待续)

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐