为啥 C++ 的“野性性能”进不了 ArkTS 的“优雅花园”?——使用 NAPI 跨语言集成:C/C++ 与 ArkTS 的桥

举报
bug菌 发表于 2025/10/27 19:49:38 2025/10/27
【摘要】 🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8 ❓前言当你在 ArkTS 里写到手抽筋,还是觉得有些数值计算、图像处理...

🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

❓前言

当你在 ArkTS 里写到手抽筋,还是觉得有些数值计算图像处理音视频编解码用纯 TS/JS 有点吃力;而你手边又有一坨现成 C/C++ 算法库,这时最顺滑的姿势就是:用 NAPI(Native API)把 C/C++ 模块桥接到 ArkTS
  这篇一口气讲全:NAPI 与 ArkTS 的调用与数据传递跨语言通信的性能考量线程管理与内存优化,最后给出一个从零实现 C++ 模块并在 ArkTS 调用的完整示例(同步、异步、零拷贝、线程安全回调全覆盖)。
坐稳了,我们把“野性性能”请进“优雅花园”😉。

🧭 目录

  • 🪄 前言:为什么选 NAPI 做桥接

  • 🔌 一、NAPI × ArkTS:原生调用与数据模型

  • 🚀 二、跨语言通信的性能要点(零拷贝、TypedArray、批量调用)

  • 🧵 三、线程管理与内存优化(async work、TSFN、Finalizer)

  • 🧰 四、从零到一:C++ 模块 + NAPI + ArkTS 调用实战

    • 4.1 目录与工程文件
    • 4.2 C++:同步函数、异步任务、零拷贝缓冲区、线程安全回调
    • 4.3 CMake / 构建要点
    • 4.4 ArkTS:类型声明、同步/异步调用、UI 绑定
  • ✅ 五、常见坑位与最佳实践清单

  • 🌈 结语:把复杂留给底层,把简单还给上层

🪄 前言:为什么选 NAPI 做桥接

  • 稳定 ABI:NAPI 层提供稳定的二进制接口;C/C++ 实现与上层 JS/ArkTS 解耦,升级运行时更稳。
  • 跨语言数据模型:内建 Number/String/Array/Object/ArrayBuffer/TypedArray/Promise 等常用类型转换。
  • 线程与异步模型napi_create_async_worknapi_create_threadsafe_function 方便把重活丢到工作线程,计算完再回主线程更新 UI。
  • 性能:避开频繁解释器开销,把热点逻辑放 C/C++,配合零拷贝把大块数据直通。

🔌 一、NAPI × ArkTS:原生调用与数据模型

1) 基本调用路线

  1. C/C++ 暴露 Init(env, exports)napi_module_register
  2. ArkTS(或 JS)import addon from 'libxxx.so' / 通过模块名加载;调用导出函数。
  3. NAPI 在两端做类型转换:ArkTS → NAPI → C++、C++ → NAPI → ArkTS。

2) 关键类型映射

ArkTS/JS NAPI C/C++ 说明
number napi_create_double / napi_get_value_double 整数可用 int32/uint32 接口
string napi_create_string_utf8 / napi_get_value_string_utf8 UTF-8
boolean napi_get_value_bool / napi_get_boolean
Array napi_create_array / napi_get_element 慎用,频繁跨界访问成本高
ArrayBuffer napi_create_arraybuffer / napi_get_arraybuffer_info 大数据首选
TypedArray napi_create_typedarray / napi_get_typedarray_info 零拷贝视图
Promise napi_create_promise / napi_resolve_deferred 异步返回
回调函数 napi_create_function / napi_call_function 同步/异步皆可
外部对象 napi_create_external / napi_add_finalizer 绑定 C++ 实例与析构

🚀 二、跨语言通信的性能要点

  1. 尽量少次跨界
    多次 JS↔C++ 往返比一次性传大块数据更慢。把循环搬到 C++,只返回结果或状态。

  2. ArrayBuffer/TypedArray 代替数组

  • 避免逐元素转换;
  • 可以与 C++ 共享同一段内存(必要时 napi_create_external_arraybuffer)。
  1. 批量接口
    一组请求塞进一个对象/缓冲区,C++ 一次性处理,减少 N 次调用的开销。

  2. 异步 + Worker
    重 CPU 活放 napi_create_async_work 的工作线程,ArkTS 主线程保持流畅;回调再上主线程。

  3. 避免隐式字符串拼装与 JSON 循环解析
    能用数值/二进制就不要动 JSON;若必须 JSON,尽量在 C++ 使用成熟库一次性完成解析与生成。

🧵 三、线程管理与内存优化

1) 异步工作(napi_async_work

  • Execute 在后台线程执行,不要用 NAPI JS API;
  • Complete 回到主线程,可以创建 JS 值和回调/resolve。

2) 线程安全回调(napi_threadsafe_function,简称 TSFN)

  • 后台线程可安全地多次向主线程发送事件(进度、日志、流数据)。
  • 主线程消费回调,避免竞争。

3) 外部内存与 Finalizer

  • napi_create_external_arraybuffer 把 C++ 内存直接“交给” JS;
  • napi_add_finalizer 在对象 GC 时触发析构,回收 native 资源(文件句柄、指针池、显存等)。

4) 对象生命周期

  • ArkTS 持有:把 C++ 指针挂在 ExternalInstance 上,按需释放;
  • C++ 管理:引用计数或 shared_ptr,配合 Finalizer 避免悬挂指针。

🧰 四、从零到一:C++ 模块 + NAPI + ArkTS 调用实战

目标:实现一个名为 fastmath 的原生库,导出:

  • add(a, b): number(同步)
  • heavySum(buffer: Float64Array): Promise<number>(后台线程做大数组求和)
  • processInPlace(buf: ArrayBuffer)零拷贝原地倍增)
  • startStream(cb: (chunk: number) => void)(后台线程周期性回调主线程,演示 TSFN

4.1 目录与关键文件(示意)

entry/
  src/main/
    cpp/
      fastmath.cpp
      CMakeLists.txt
    ets/
      pages/Index.ets
      common/types/fastmath.d.ts   # ArkTS 类型声明
    module.json5
  build-profile.json5
  oh-package.json5

实际工程结构以你项目模版为准,关键是:C++ 源码 + CMake + ArkTS 类型三件套。

4.2 C++:NAPI 实现

// entry/src/main/cpp/fastmath.cpp
#include <node_api.h>
#include <assert.h>
#include <string.h>
#include <vector>
#include <thread>
#include <chrono>

#define NAPI_OK_OR_THROW(env, status, msg)                 \
  if ((status) != napi_ok) {                               \
    napi_throw_error((env), nullptr, (msg));               \
    return nullptr;                                        \
  }

struct AsyncSumContext {
  napi_async_work work{nullptr};
  napi_deferred deferred{nullptr};
  double result{0.0};
  // 保存一份数据拷贝(演示;也可直接借用外部内存,只要生命周期安全)
  std::vector<double> data;
};

// ---------- 同步:add(a,b) ----------
napi_value Add(napi_env env, napi_callback_info info) {
  size_t argc = 2;
  napi_value args[2];
  napi_status s = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
  NAPI_OK_OR_THROW(env, s, "get_cb_info failed");
  if (argc < 2) {
    napi_throw_type_error(env, nullptr, "Two numbers required");
    return nullptr;
  }
  double a, b;
  napi_get_value_double(env, args[0], &a);
  napi_get_value_double(env, args[1], &b);
  napi_value out;
  napi_create_double(env, a + b, &out);
  return out;
}

// ---------- 异步:heavySum(Float64Array) -> Promise<number> ----------
void ExecuteSum(napi_env env, void* data) {
  auto* ctx = static_cast<AsyncSumContext*>(data);
  // 后台线程中:纯计算,不要调用任何 NAPI JS API
  double sum = 0.0;
  for (double v : ctx->data) sum += v;
  // 模拟重活
  std::this_thread::sleep_for(std::chrono::milliseconds(50));
  ctx->result = sum;
}

void CompleteSum(napi_env env, napi_status status, void* data) {
  auto* ctx = static_cast<AsyncSumContext*>(data);
  if (status != napi_ok) {
    napi_value errStr, err;
    napi_create_string_utf8(env, "Async work failed", NAPI_AUTO_LENGTH, &errStr);
    napi_create_error(env, nullptr, errStr, &err);
    napi_reject_deferred(env, ctx->deferred, err);
  } else {
    napi_value num;
    napi_create_double(env, ctx->result, &num);
    napi_resolve_deferred(env, ctx->deferred, num);
  }
  napi_delete_async_work(env, ctx->work);
  delete ctx;
}

napi_value HeavySum(napi_env env, napi_callback_info info) {
  size_t argc = 1;
  napi_value args[1];
  napi_status s = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
  NAPI_OK_OR_THROW(env, s, "get_cb_info failed");
  if (argc < 1) {
    napi_throw_type_error(env, nullptr, "Float64Array required");
    return nullptr;
  }

  // 读取 TypedArray 信息
  bool isTypedArray = false;
  napi_is_typedarray(env, args[0], &isTypedArray);
  if (!isTypedArray) {
    napi_throw_type_error(env, nullptr, "Argument must be TypedArray");
    return nullptr;
  }

  napi_typedarray_type type;
  size_t length;
  void* data;
  napi_value ab;
  size_t offset;
  napi_get_typedarray_info(env, args[0], &type, &length, &data, &ab, &offset);
  if (type != napi_float64_array) {
    napi_throw_type_error(env, nullptr, "Float64Array expected");
    return nullptr;
  }

  // 拷贝一份到上下文(演示;也可以直接在 ExecuteSum 使用 data 指针,但要保证 ArrayBuffer 在异步期间不被 GC)
  auto* ctx = new AsyncSumContext();
  double* p = static_cast<double*>(data);
  ctx->data.assign(p, p + length);

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

  napi_value resourceName;
  napi_create_string_utf8(env, "heavySum", NAPI_AUTO_LENGTH, &resourceName);
  napi_create_async_work(env, nullptr, resourceName, ExecuteSum, CompleteSum, ctx, &ctx->work);
  napi_queue_async_work(env, ctx->work);

  return promise;
}

// ---------- 零拷贝:processInPlace(ArrayBuffer) ----------
napi_value ProcessInPlace(napi_env env, napi_callback_info info) {
  size_t argc = 1;
  napi_value args[1];
  napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
  if (argc < 1) {
    napi_throw_type_error(env, nullptr, "ArrayBuffer required");
    return nullptr;
  }
  void* data;
  size_t byteLen;
  napi_get_arraybuffer_info(env, args[0], &data, &byteLen);

  // 假设是 double 的缓冲区,原地 *2
  size_t n = byteLen / sizeof(double);
  double* p = static_cast<double*>(data);
  for (size_t i = 0; i < n; ++i) p[i] *= 2.0;

  napi_value undefined;
  napi_get_undefined(env, &undefined);
  return undefined;
}

// ---------- TSFN:startStream(cb) 周期性推送 ----------
struct StreamContext {
  napi_threadsafe_function tsfn{nullptr};
  bool running{true};
  std::thread th;
};

void StreamWorker(StreamContext* ctx) {
  for (int i = 1; i <= 10 && ctx->running; ++i) {
    int* heapNum = new int(i); // 交给 js 回调再释放
    napi_call_threadsafe_function(ctx->tsfn, heapNum, napi_tsfn_blocking);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
  }
  napi_release_threadsafe_function(ctx->tsfn, napi_tsfn_release);
}

void TSFNCallJS(napi_env env, napi_value js_cb, void* /*context*/, void* data) {
  // 主线程:把数据转换成 JS 值并调用回调
  int* num = static_cast<int*>(data);
  napi_value argv[1];
  napi_create_int32(env, *num, &argv[0]);
  napi_value global;
  napi_get_global(env, &global);
  napi_value result;
  napi_call_function(env, global, js_cb, 1, argv, &result);
  delete num;
}

napi_value StartStream(napi_env env, napi_callback_info info) {
  size_t argc = 1;
  napi_value args[1];
  napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
  if (argc < 1) {
    napi_throw_type_error(env, nullptr, "Callback required");
    return nullptr;
  }

  auto* ctx = new StreamContext();
  napi_value resourceName;
  napi_create_string_utf8(env, "stream", NAPI_AUTO_LENGTH, &resourceName);
  napi_create_threadsafe_function(
      env, args[0], nullptr, resourceName, 0, 1, nullptr, nullptr, nullptr, TSFNCallJS, &ctx->tsfn);

  // 启动后台线程
  ctx->th = std::thread([ctx](){ StreamWorker(ctx); });
  ctx->th.detach();

  napi_value undefined;
  napi_get_undefined(env, &undefined);
  return undefined;
}

// ---------- 模块导出 ----------
napi_value Init(napi_env env, napi_value exports) {
  napi_property_descriptor desc[] = {
    { "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr },
    { "heavySum", nullptr, HeavySum, nullptr, nullptr, nullptr, napi_default, nullptr },
    { "processInPlace", nullptr, ProcessInPlace, nullptr, nullptr, nullptr, napi_default, nullptr },
    { "startStream", nullptr, StartStream, nullptr, nullptr, nullptr, napi_default, nullptr },
  };
  napi_define_properties(env, exports, sizeof(desc)/sizeof(desc[0]), desc);
  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

说明:

  • heavySumnapi_async_work 在后台线程算和,结果用 Promise 返回;
  • processInPlace 直接操作 ArrayBuffer 的底层内存,零拷贝
  • startStreamTSFN 从后台线程多次回调 ArkTS。

4.3 CMake / 构建要点(示意)

# entry/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(fastmath)

add_library(fastmath SHARED fastmath.cpp)
# NAPI 头文件与链接库通常由 SDK/NDK 提供;下行路径按你的环境调整
# target_include_directories(fastmath PRIVATE ${NAPI_INCLUDE_DIR})
# target_link_libraries(fastmath PUBLIC ${NAPI_LIBS})
set_target_properties(fastmath PROPERTIES OUTPUT_NAME "fastmath")
  • 产物为 libfastmath.so,打包进 HAP;ArkTS 侧按模块名加载。
  • 如果你使用 DevEco Studio 原生模板,选中 添加 C/C++ 支持,IDE 会生成基础 CMake 与构建脚本,把 fastmath.cpp 和上面的导出加进去即可。

4.4 ArkTS:类型声明、调用与 UI 绑定

(1) 类型声明(entry/src/main/ets/common/types/fastmath.d.ts

有了 .d.ts,ArkTS 写起来更丝滑,IDE 有智能提示。

declare module 'libfastmath.so' {
  const add: (a: number, b: number) => number;
  const heavySum: (arr: Float64Array) => Promise<number>;
  const processInPlace: (buf: ArrayBuffer) => void;
  const startStream: (cb: (n: number) => void) => void;
  export default {
    add, heavySum, processInPlace, startStream
  }
}

说明:模块名与生成的 libfastmath.so 对应。不同模板可能需要通过自动加载器或 globalThis.requireNapi('fastmath') 等方式引入,以下示例采用常见的直接导入风格。如果你的工程使用的是“通过模块名加载”的 API,请把 import addon from 'libfastmath.so' 替换为实际项目的加载方法(例如 const addon = requireNapi('fastmath'))。

(2) ArkTS 调用示例(Index.ets

import addon from 'libfastmath.so'

@Entry
@Component
struct Index {
  @State sum: number = 0
  @State heavyResult: number = 0
  @State streamLog: string[] = []

  aboutToAppear() {
    // 1) 同步
    this.sum = addon.add(3, 4) // 7

    // 2) 异步:后台线程计算大数组和
    const data = new Float64Array(1_0000) // 1w 元素
    for (let i = 0; i < data.length; i++) data[i] = i + 1
    addon.heavySum(data).then(res => this.heavyResult = res)

    // 3) 零拷贝:原地 *2
    const buf = new ArrayBuffer(8 * 4)
    const view = new Float64Array(buf)
    view.set([1, 2, 3, 4])
    addon.processInPlace(buf) // -> [2,4,6,8]

    // 4) TSFN:后台线程推送数据
    addon.startStream((n: number) => {
      this.streamLog = [`chunk:${n}`, ...this.streamLog].slice(0, 8)
    })
  }

  build() {
    Column({ space: 8 }) {
      Text(`add(3,4) = ${this.sum}`).fontSize(18)
      Text(`heavySum = ${this.heavyResult}`).fontSize(18)
      List() {
        ForEach(this.streamLog, (row: string) => ListItem() {
          Text(row)
        })
      }.height(160)
      Button('Re-Sum big array')
        .onClick(async () => {
          const arr = new Float64Array(200000)
          for (let i = 0; i < arr.length; i++) arr[i] = Math.random()
          const t0 = Date.now()
          const r = await addon.heavySum(arr)
          const t1 = Date.now()
          this.streamLog = [`sum=${r.toFixed(2)} in ${t1 - t0}ms`, ...this.streamLog]
        })
    }.padding(16)
  }
}

亮点:

  • ArkTS 主线程丝滑,大数组求和丢给原生后台线程;
  • processInPlaceArrayBuffer 零拷贝
  • startStream 展示原生线程 → 主线程的多次回调。

✅ 五、常见坑位与最佳实践清单

数据与性能

  • 优先 ArrayBuffer/TypedArray,少用对象数组传大数据。
  • 一次处理一批,减少 JS↔C++ 的往返次数。
  • 避免在 C++ 里频繁构造 JS 字符串;日志请合并或传数字/二进制。

线程与异步

  • 后台线程禁止调用 NAPI JS API(除 TSFN 内回调);
  • napi_async_workExecute 只做计算;Complete 才能创建 JS 值与 resolve;
  • 持久回调用 TSFN,不用自己造锁去跨线程调 JS。
  • 需要取消/停止流时,自己维护状态,并释放 TSFN:napi_release_threadsafe_function(...)

内存与生命周期

  • napi_add_finalizer 绑定 C++ 资源的释放,避免 native 泄漏;
  • 外部内存要明确所有权:谁分配、谁释放,GC 何时触发。
  • 大内存块尽量复用(池化),避免频繁分配释放。

构建与发布

  • 保证 libfastmath.so 跟随 HAP 正确打包;
  • .d.ts 与实际导出一致,避免运行期才发现方法名不匹配;
  • Release 构建打开 O2/O3 与 LTO(按工具链支持),Native 性能更稳。

调试与崩溃

  • 调试期可开启 AddressSanitizer/UndefinedBehaviorSanitizer(若工具链支持);
  • 崩溃排查:先最小化调用路径(用同步小函数),再逐步引入异步与零拷贝。

🌈 结语:把复杂留给底层,把简单还给上层

NAPI 像是一座坚固的桥:一头连接 C/C++ 的性能世界,一头连接 ArkTS 的开发效率与生态。当你把热点计算、流式处理、系统级能力交给原生层,ArkTS 只需关注产品与交互,两者各尽其长,才是工程上真正的“既要又要”。
  最后留个小反问——当跨语言通信的开销被压到最低,你会把省下的预算用来堆更炫的动画,还是做一个让用户“离不开”的小功能?😉

📎 附录 · 速查代码片段

创建 Promise

napi_deferred d; napi_value p;
napi_create_promise(env, &d, &p);
// 完成时:napi_resolve_deferred(env, d, value);

External ArrayBuffer(把 C++ 内存交给 JS)

void* buf = malloc(size);
napi_value ab;
napi_create_external_arraybuffer(env, buf, size,
  [](napi_env env, void* data, void* hint){ free(data); }, nullptr, &ab);

Finalizer(对象随 GC 释放原生资源)

struct Holder { int fd; };
napi_value obj; napi_create_object(env, &obj);
auto* h = new Holder{ /*fd*/ 3 };
napi_add_finalizer(env, obj, h,
  [](napi_env, void* data, void*){ delete static_cast<Holder*>(data); }, nullptr, nullptr);

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」专栏(全网一个名),bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

  最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

  同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。

✨️ Who am I?

我是bug菌(全网一个名),CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主/价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。