为啥 C++ 的“野性性能”进不了 ArkTS 的“优雅花园”?——使用 NAPI 跨语言集成:C/C++ 与 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_work与napi_create_threadsafe_function方便把重活丢到工作线程,计算完再回主线程更新 UI。 - 性能:避开频繁解释器开销,把热点逻辑放 C/C++,配合零拷贝把大块数据直通。
🔌 一、NAPI × ArkTS:原生调用与数据模型
1) 基本调用路线
- C/C++ 暴露
Init(env, exports)并napi_module_register。 - ArkTS(或 JS)
import addon from 'libxxx.so'/ 通过模块名加载;调用导出函数。 - 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++ 实例与析构 |
🚀 二、跨语言通信的性能要点
-
尽量少次跨界
多次 JS↔C++ 往返比一次性传大块数据更慢。把循环搬到 C++,只返回结果或状态。 -
用
ArrayBuffer/TypedArray代替数组
- 避免逐元素转换;
- 可以与 C++ 共享同一段内存(必要时
napi_create_external_arraybuffer)。
-
批量接口
把一组请求塞进一个对象/缓冲区,C++ 一次性处理,减少 N 次调用的开销。 -
异步 + Worker
重 CPU 活放napi_create_async_work的工作线程,ArkTS 主线程保持流畅;回调再上主线程。 -
避免隐式字符串拼装与 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++ 指针挂在
External或Instance上,按需释放; - 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)
说明:
heavySum用napi_async_work在后台线程算和,结果用 Promise 返回;processInPlace直接操作ArrayBuffer的底层内存,零拷贝;startStream用 TSFN 从后台线程多次回调 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 主线程丝滑,大数组求和丢给原生后台线程;
processInPlace用ArrayBuffer零拷贝;startStream展示原生线程 → 主线程的多次回调。
✅ 五、常见坑位与最佳实践清单
数据与性能
- 优先
ArrayBuffer/TypedArray,少用对象数组传大数据。 - 一次处理一批,减少 JS↔C++ 的往返次数。
- 避免在 C++ 里频繁构造 JS 字符串;日志请合并或传数字/二进制。
线程与异步
- 后台线程禁止调用 NAPI JS API(除 TSFN 内回调);
napi_async_work的Execute只做计算;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-
- 点赞
- 收藏
- 关注作者
评论(0)