鸿蒙造物者之路 10 · 南北打通
续写章节|沿用「造物者」系列的武侠讲解体,面向电子信息 / 嵌入式方向的新手
=================前情提要===============
分布式的世界把我震住了——手机能读开发板的传感器,应用能在设备间流转。
但老陈一句话把我拉回地面:
“开发板上跑的是 C 代码,手机上跑的是 ArkTS。它们俩,怎么直接对话?”
是啊。我一直在想设备之间怎么连,却没想过——设备内部,两种语言怎么打通。
========================================
这一章你会得到什么
🎯 学完这一章,你应该能:
- ☐ 理解为什么不能直接调用,NAPI 这座桥到底桥接了什么
- ☐ 写出第一个 NAPI 模块(同步方法 + 返回对象)
- ☐ 会用 Promise + async_work 做异步,不卡界面
- ☐ 记住线程红线:什么能碰、什么绝对不能碰
- ☐ 知道传大块数据时,ArrayBuffer 比逐个 push 快多少
**🧰 需要准备:**第 04 章(C 侧)+ 第 06 章(ArkTS 侧)两套环境都要。
**⏱ 预计用时:**约 6 小时
📊 难度:★★★★★ 全书技术难度最高的一章
一、先看清问题:两个世界
"画个图。"老陈把笔递给我。
我在白板上画了我理解的结构:
┌────────────────────────────────────┐
│ ArkTS 世界 │
│ · ArkTS 代码 │
│ · 跑在方舟运行时(ArkTS Runtime) │
│ · 有垃圾回收(GC) │
│ · 类型安全、内存安全 │
│ · 拿不到指针、拿不到寄存器 │
├────────────────────────────────────┤
│ ? │ ← 这里断了
├────────────────────────────────────┤
│ C/C++ 世界 │
│ · 原生代码 │
│ · 直接编译成机器码 │
│ · 手动管理内存 │
│ · 有指针、能碰硬件、能调驱动 │
│ · 快,但容易崩 │
└────────────────────────────────────┘
"对,这就是问题。"老陈说,“两个世界的规则完全不同。”
“为什么不能直接调用?”
“三个原因:”
1. 【调用约定不同】
ArkTS 函数调用要查运行时状态、要做类型检查;
C 函数就是跳到某个地址。两者 ABI(二进制接口)对不上。
2. 【内存管理不同】
ArkTS 的对象由 GC 管,随时可能被移动或回收;
C 拿到一个指针,可能下一刻就失效了。
3. 【类型系统不同】
ArkTS 的 number 是双精度浮点;
C 的 int 是 32 位整数。怎么对应?
“所以需要一座桥。”
“对。这座桥叫 NAPI。”
二、NAPI 是什么
2.1 定义
“NAPI(Node-API)是鸿蒙提供的、让 ArkTS 调用 C/C++ 的标准机制。”
“Node?跟 Node.js 有关系?”
"有渊源。"老陈说,“鸿蒙的 NAPI 参考了 Node.js 的 N-API 规范——那是一套跨版本稳定的原生模块 API。你写的原生模块,不会因为运行时升级就编译不过。”
"这一点非常关键。"他强调,“以前有些桥接方案,引擎一升级 ABI 就变,你的 so 库全得重编。NAPI 保证 ABI 稳定——编一次,长期能用。”
2.2 它在架构里的位置
┌──────────────────────────────────────┐
│ ArkTS 应用 │
│ import native from 'libentry.so' │
├──────────────────────────────────────┤
│ NAPI(桥梁层) │ ← 类型转换、生命周期管理
├──────────────────────────────────────┤
│ C/C++ 原生代码(.so) │
├──────────────────────────────────────┤
│ 系统能力 / 驱动(HDF)/ 硬件 │
└──────────────────────────────────────┘
2.3 什么时候需要它
“不是所有项目都需要 NAPI。先搞清楚什么时候必须用它:”
| 场景 | 是否要用 NAPI | 说明 |
|---|---|---|
| 纯 UI 应用(记事本、计算器) | ❌ | ArkTS 够了 |
| 调系统 API(网络、传感器、文件) | ❌ | 框架已经封装好了 |
| 接入 C/C++ 开源库(OpenCV、FFmpeg) | ✅ | 生态都在 C/C++ |
| 操作硬件 / 调自己的驱动 | ✅ | 你的场景 |
| 性能敏感的计算(信号处理、图像算法) | ✅ | C 快得多 |
| 复用已有的 C 代码资产 | ✅ | 不用重写 |
| 加解密、协议编解码 | ✅ | 已有成熟 C 实现 |
"你的温湿度项目,属于第二和第三种。"老陈说,“你要让 ArkTS 读到只有 C 能碰的传感器。”
三、NAPI 的四个核心概念
“先记住四个东西,剩下的都是它们的组合:”
| 概念 | 是什么 | 类比 |
|---|---|---|
napi_env |
运行环境的上下文 | 每个调用都带着它,相当于"当前虚拟机" |
napi_value |
一个 ArkTS 值的 C 表示 | ArkTS 里所有东西(数字、字符串、对象)在 C 里都长这样 |
napi_callback_info |
调用信息 | 用来取参数、取 this |
napi_status |
返回状态 | napi_ok 表示成功 |
“最重要的理解:napi_value 是一个’句柄’,不是真正的数据。”
// ❌ 错误理解:以为 napi_value 是数据本身
napi_value val;
int x = (int)val; // 完全错误!
// ✅ 正确理解:napi_value 是"票根",要兑换
napi_value val;
int32_t x = 0;
napi_get_value_int32(env, val, &x); // 用 API 兑换成 C 类型
四、第一个 NAPI 模块
“从一个最简单的开始——加法。”
4.1 目录结构
entry/src/main/
├── cpp/ ← C/C++ 代码都在这
│ ├── CMakeLists.txt ← 编译配置
│ ├── napi_init.cpp ← NAPI 入口
│ ├── types/ ← 类型声明
│ │ └── libentry/
│ │ ├── index.d.ts ← 给 ArkTS 用的类型声明
│ │ └── oh-package.json5
│ └── sensor/ ← 你的业务逻辑
│ ├── sensor_napi.cpp
│ └── CMakeLists.txt
└── ets/
└── pages/Index.ets ← ArkTS 调用方
4.2 C++ 侧完整代码
// entry/src/main/cpp/napi_init.cpp
#include "napi/native_api.h"
#include <string>
// ============ 1. 一个同步方法:加法 ============
static napi_value Add(napi_env env, napi_callback_info info)
{
// ① 取参数
size_t argc = 2; // 期望 2 个参数
napi_value args[2] = {nullptr};
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// ② 检查参数个数
if (argc < 2) {
napi_throw_error(env, "EINVAL", "需要两个参数");
return nullptr;
}
// ③ 检查类型 + 转成 C 类型
napi_valuetype type0, type1;
napi_typeof(env, args[0], &type0);
napi_typeof(env, args[1], &type1);
if (type0 != napi_number || type1 != napi_number) {
napi_throw_type_error(env, "ETYPE", "参数必须是数字");
return nullptr;
}
int32_t a = 0, b = 0;
napi_get_value_int32(env, args[0], &a);
napi_get_value_int32(env, args[1], &b);
// ④ 计算结果,转成 napi_value 返回
napi_value result = nullptr;
napi_create_int32(env, a + b, &result);
return result;
}
// ============ 2. 一个返回对象的同步方法 ============
static napi_value GetDeviceInfo(napi_env env, napi_callback_info info)
{
napi_value obj = nullptr;
napi_create_object(env, &obj); // 创建一个 JS 对象
// 塞属性
napi_value nameVal = nullptr;
napi_create_string_utf8(env, "SHT30", NAPI_AUTO_LENGTH, &nameVal);
napi_set_named_property(env, obj, "name", nameVal);
napi_value addrVal = nullptr;
napi_create_int32(env, 0x44, &addrVal);
napi_set_named_property(env, obj, "i2cAddr", addrVal);
napi_value onlineVal = nullptr;
napi_get_boolean(env, true, &onlineVal);
napi_set_named_property(env, obj, "online", onlineVal);
return obj;
}
// ============ 3. 模块注册 ============
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
// 把方法挂到 exports 上
napi_property_descriptor desc[] = {
{ "add", nullptr, Add, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "getDeviceInfo", nullptr, GetDeviceInfo, nullptr, nullptr, nullptr, napi_default, nullptr },
};
napi_define_properties(env, exports,
sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
// 模块描述结构
static napi_module g_module = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init, // ← 注册函数
.nm_modname = "entry", // ← 模块名!import 时用这个
.nm_priv = nullptr,
.reserved = { nullptr },
};
// 构造函数:so 被加载时自动调用
extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
napi_module_register(&g_module);
}
"几个关键点:"老陈指着屏幕。
1. nm_modname = "entry" → ArkTS 里 import 的模块名(对应 libentry.so)
2. __attribute__((constructor)) → so 加载时自动执行,无需手动调
3. napi_get_cb_info 取参数,napi_create_xxx 造返回值
4. 一定要做参数检查!C 层的崩溃是整个应用崩溃,不是抛异常
4.3 类型声明(给 ArkTS 用)
// entry/src/main/cpp/types/libentry/index.d.ts
export const add: (a: number, b: number) => number;
export interface DeviceInfo {
name: string;
i2cAddr: number;
online: boolean;
}
export const getDeviceInfo: () => DeviceInfo;
// entry/src/main/cpp/types/libentry/oh-package.json5
{
"name": "libentry.so",
"types": "./index.d.ts",
"version": "1.0.0",
"description": "原生模块类型声明"
}
4.4 编译配置
# entry/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
project(MyNative)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
# 头文件搜索路径
include_directories(
${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include
)
# 编译成动态库(.so)
add_library(entry SHARED
napi_init.cpp
sensor/sensor_napi.cpp # 你的其他源文件
)
# 链接 NAPI 库
target_link_libraries(entry PUBLIC
libace_napi.z.so # NAPI
libhilog_ndk.z.so # 日志
libc++.a
)
// entry/build-profile.json5
{
"apiType": "stageMode",
"buildOption": {
"externalNativeOptions": { // ← 原生编译配置
"path": "./src/main/cpp/CMakeLists.txt",
"arguments": "",
"cppFlags": "",
"abiFilters": ["arm64-v8a"] // 目标架构
}
}
}
4.5 ArkTS 侧调用
// entry/src/main/ets/pages/Index.ets
import nativeModule from 'libentry.so'; // ← 导入原生模块
import hilog from '@ohos.hilog';
@Entry
@Component
struct Index {
@State sum: number = 0;
@State deviceName: string = '';
aboutToAppear(): void {
// 调用同步方法
this.sum = nativeModule.add(3, 5); // 8
hilog.info(0x0000, 'NAPI', 'add 结果: %{public}d', this.sum);
let info: nativeModule.DeviceInfo = nativeModule.getDeviceInfo();
this.deviceName = info.name;
hilog.info(0x0000, 'NAPI', '设备: %{public}s', this.deviceName);
}
build() {
Column({ space: 16 }) {
Text(`3 + 5 = ${this.sum}`).fontSize(24)
Text(`设备: ${this.deviceName}`).fontSize(16)
}
}
}
"import nativeModule from 'libentry.so'——注意这个 .so 后缀。"老陈说,“模块名是 entry,so 文件名是 libentry.so,import 时写后者。”
我点编译,跑起来,屏幕上显示:
3 + 5 = 8
设备: SHT30
“……通了。”
"通了。"老陈点头,“但刚才那个是同步调用。真实项目里,你会死在同步调用上。”
五、异步:不能在 UI 线程里等硬件
“为什么?”
"因为读传感器要时间。"老陈说,“你同步调用 readTemperature(),C 层要等 I2C 时序走完——几十毫秒到几百毫秒。这段时间主线程被卡死,界面直接冻住。”
“所以要用异步?”
"必须。"他说,“NAPI 提供两种异步方式。”
5.1 方式一:Callback 回调
// C++ 侧:异步读温度,用 callback 回调
struct AsyncData {
napi_async_work work = nullptr;
napi_ref callbackRef = nullptr; // 保存回调函数
double temperature = 0.0;
int32_t status = 0;
};
// ① 在【后台线程】执行:干重活
static void ExecuteRead(napi_env env, void *data)
{
AsyncData *d = static_cast<AsyncData *>(data);
// 这里不在主线程!可以放心做耗时操作
d->temperature = ReadSensorHardware(); // 调你的驱动/硬件
d->status = 0;
}
// ② 在【主线程】执行:回调 JS
static void CompleteRead(napi_env env, napi_status status, void *data)
{
AsyncData *d = static_cast<AsyncData *>(data);
// 取回保存的回调函数
napi_value callback = nullptr;
napi_get_reference_value(env, d->callbackRef, &callback);
// 构造参数:[错误码, 结果]
napi_value result[2] = {nullptr};
napi_create_int32(env, d->status, &result[0]);
napi_create_double(env, d->temperature, &result[1]);
napi_value undefined = nullptr, ret = nullptr;
napi_get_undefined(env, &undefined);
napi_call_function(env, undefined, callback, 2, result, &ret);
// 清理
napi_delete_reference(env, d->callbackRef);
napi_delete_async_work(env, d->work);
delete d;
}
// ③ 对外暴露的方法
static napi_value ReadTempAsync(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);
// 第一个参数是回调函数
auto *d = new AsyncData();
napi_create_reference(env, args[0], 1, &d->callbackRef); // 保存引用
// 创建异步工作
napi_value resourceName = nullptr;
napi_create_string_utf8(env, "ReadTempAsync", NAPI_AUTO_LENGTH, &resourceName);
napi_create_async_work(env, nullptr, resourceName,
ExecuteRead, // 后台线程执行
CompleteRead, // 主线程回调
d, &d->work);
// 丢进队列,立即返回(不阻塞!)
napi_queue_async_work(env, d->work);
return nullptr; // 同步返回 undefined,结果通过回调给
}
“看清楚这个模型:”
ArkTS 调用 readTempAsync(cb)
│
▼
创建 async_work,丢进队列 ──────┐
│ │
▼ ▼
【立即返回 undefined】 【后台线程 ExecuteRead】
(UI 不卡!) 读硬件、算数据
│
▼
【主线程 CompleteRead】
调用 JS 回调
5.2 方式二:Promise(推荐)
“Callback 写法在 ArkTS 侧要传函数,嵌套多了很难看。用 Promise 更现代。”
struct PromiseData {
napi_async_work work = nullptr;
napi_deferred deferred = nullptr; // Promise 的"兑现器"
double temperature = 0.0;
int32_t status = 0;
};
static void ExecuteReadPromise(napi_env env, void *data)
{
PromiseData *d = static_cast<PromiseData *>(data);
d->temperature = ReadSensorHardware();
}
static void CompleteReadPromise(napi_env env, napi_status status, void *data)
{
PromiseData *d = static_cast<PromiseData *>(data);
napi_value result = nullptr;
if (d->status == 0) {
napi_create_double(env, d->temperature, &result);
napi_resolve_deferred(env, d->deferred, result); // 成功
} else {
napi_value errCode = nullptr;
napi_create_int32(env, d->status, &errCode);
napi_reject_deferred(env, d->deferred, errCode); // 失败
}
napi_delete_async_work(env, d->work);
delete d;
}
static napi_value ReadTempPromise(napi_env env, napi_callback_info info)
{
auto *d = new PromiseData();
// ① 创建 Promise
napi_value promise = nullptr;
napi_create_promise(env, &d->deferred, &promise);
// ② 创建异步工作
napi_value resourceName = nullptr;
napi_create_string_utf8(env, "ReadTempPromise", NAPI_AUTO_LENGTH, &resourceName);
napi_create_async_work(env, nullptr, resourceName,
ExecuteReadPromise, CompleteReadPromise,
d, &d->work);
napi_queue_async_work(env, d->work);
// ③ 立刻返回 Promise
return promise;
}
“ArkTS 侧就干净多了:”
// index.d.ts
export const readTempAsync: (cb: (code: number, temp: number) => void) => void;
export const readTempPromise: () => Promise<number>;
// 使用
@Entry
@Component
struct Index {
@State temp: number = 0;
@State loading: boolean = false;
async readTemp(): Promise<void> {
this.loading = true;
try {
// Promise 写法 —— 清爽
this.temp = await nativeModule.readTempPromise();
} catch (e) {
hilog.error(0x0000, 'NAPI', '读取失败: %{public}s', JSON.stringify(e));
} finally {
this.loading = false;
}
}
build() {
Column({ space: 16 }) {
if (this.loading) {
LoadingProgress().width(40).height(40)
} else {
Text(`${this.temp.toFixed(1)}°C`).fontSize(48).fontWeight(FontWeight.Bold)
}
Button('读取温度').onClick(() => { this.readTemp(); })
}
}
}
"优先用 Promise。"老陈说,“配合 async/await,代码比回调嵌套清晰十倍。”
六、线程安全:一个真实的坑
“现在讲最容易出事的地方。”
“场景:你的 C 代码里有个后台线程,持续采集传感器数据,采到了要通知 ArkTS 更新界面。”
“直接调回调不行吗?”
"绝对不行。"老陈说,“ArkTS 的引擎不是线程安全的。你从非主线程直接碰 napi_env,轻则数据错乱,重则整个应用崩溃。”
“那怎么办?”
“用 napi_threadsafe_function(线程安全函数)。”
// ============ 线程安全回调:从任意线程通知 ArkTS ============
static napi_threadsafe_function g_tsfn = nullptr;
// ① 在【主线程】创建线程安全函数
static napi_value StartMonitor(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);
napi_value resourceName = nullptr;
napi_create_string_utf8(env, "SensorMonitor", NAPI_AUTO_LENGTH, &resourceName);
// 创建线程安全函数(把 JS 回调包装起来)
napi_create_threadsafe_function(
env,
args[0], // JS 回调函数
nullptr,
resourceName,
0, // 队列上限(0 = 无限)
1, // 初始线程数
nullptr, nullptr, nullptr,
CallJsFromNative, // ← 在 JS 线程执行的回调
&g_tsfn
);
// 启动后台采集线程(C++ std::thread)
StartBackgroundThread();
return nullptr;
}
// ② 运行在【JS 主线程】的回调 —— 这里才能安全碰 napi_env
static void CallJsFromNative(napi_env env, napi_value jsCallback,
void *context, void *data)
{
double *temp = static_cast<double *>(data);
napi_value arg = nullptr;
napi_create_double(env, *temp, &arg);
napi_value undefined = nullptr, ret = nullptr;
napi_get_undefined(env, &undefined);
napi_call_function(env, undefined, jsCallback, 1, &arg, &ret);
delete temp; // 记得释放
}
// ③ 【任意线程】调用:把数据丢进队列
void NotifyFromBackgroundThread(double temperature)
{
if (g_tsfn == nullptr) return;
double *payload = new double(temperature);
// napi_tsfn_nonblocking:队列满了就丢弃,不阻塞采集线程
napi_call_threadsafe_function(g_tsfn, payload, napi_tsfn_nonblocking);
}
"核心规则:"老陈总结。
┌────────────────────────────────────────────┐
│ 主线程(ArkTS 线程) │
│ ✅ 可以直接用 napi_env │
│ ✅ 可以调 JS 函数 │
│ ✅ Complete 回调在这里执行 │
├────────────────────────────────────────────┤
│ 后台线程(Execute 回调 / 你自己开的线程) │
│ ❌ 不能碰 napi_env │
│ ❌ 不能调 JS 函数 │
│ ✅ 只能干纯计算 / 读硬件 │
│ ✅ 要通知 JS → 用 napi_threadsafe_function │
└────────────────────────────────────────────┘
"违反这条规则的后果:随机崩溃,而且很难复现。"他说,“这是 NAPI 最凶险的坑。”
七、完整案例:ArkTS 读开发板的温湿度
"现在把整条线串起来。"老陈在白板上画了全链路:
┌──────────────────────────────────────────────────┐
│ ① 硬件层 SHT30 传感器(I2C 挂在开发板上) │
└───────────────────────┬──────────────────────────┘
│ I2C 时序
┌───────────────────────▼──────────────────────────┐
│ ② 驱动层 HDF 驱动 / IoTI2c 接口 │
│ GpioWrite / IoTI2cRead │
└───────────────────────┬──────────────────────────┘
│ C 函数调用
┌───────────────────────▼──────────────────────────┐
│ ③ 原生层 sensor_napi.cpp │
│ 读硬件 → 打包成 napi_value │
│ 异步:napi_create_async_work │
└───────────────────────┬──────────────────────────┘
│ NAPI 桥接
┌───────────────────────▼──────────────────────────┐
│ ④ 应用层 Index.ets │
│ await nativeModule.readTemp() │
│ @State 更新 → 界面自动刷新 │
└──────────────────────────────────────────────────┘
7.1 原生层
// entry/src/main/cpp/sensor/sensor_napi.cpp
#include "napi/native_api.h"
#include "hilog/log.h"
#include <thread>
#include <chrono>
#undef LOG_TAG
#define LOG_TAG "SENSOR_NAPI"
#define LOGI(...) HILOG_INFO(LOG_CORE, __VA_ARGS__)
#define LOGE(...) HILOG_ERROR(LOG_CORE, __VA_ARGS__)
// ---------- 模拟读硬件(真实项目里换成 IoTI2cRead / HDF 调用) ----------
static double HwReadTemperature()
{
// 真实实现:
// uint8_t raw[6];
// IoTI2cRead(I2C_BUS, (0x44 << 1) | 1, raw, 6);
// uint16_t tRaw = (raw[0] << 8) | raw[1];
// return -45.0 + 175.0 * tRaw / 65535.0;
std::this_thread::sleep_for(std::chrono::milliseconds(200)); // 模拟 I2C 耗时
return 26.5;
}
static double HwReadHumidity()
{
std::this_thread::sleep_for(std::chrono::milliseconds(200));
return 61.0;
}
// ---------- 数据结构 ----------
struct ReadData {
napi_async_work work = nullptr;
napi_deferred deferred = nullptr;
double temp = 0.0;
double humi = 0.0;
int32_t errCode = 0;
};
// ---------- 后台执行 ----------
static void ExecuteRead(napi_env env, void *data)
{
ReadData *d = static_cast<ReadData *>(data);
d->temp = HwReadTemperature();
d->humi = HwReadHumidity();
d->errCode = 0;
}
// ---------- 主线程回调 ----------
static void CompleteRead(napi_env env, napi_status status, void *data)
{
ReadData *d = static_cast<ReadData *>(data);
napi_value result = nullptr;
napi_create_object(env, &result);
napi_value t = nullptr, h = nullptr;
napi_create_double(env, d->temp, &t);
napi_create_double(env, d->humi, &h);
napi_set_named_property(env, result, "temperature", t);
napi_set_named_property(env, result, "humidity", h);
napi_resolve_deferred(env, d->deferred, result);
napi_delete_async_work(env, d->work);
delete d;
}
// ---------- 对外方法 ----------
static napi_value ReadSensor(napi_env env, napi_callback_info info)
{
auto *d = new ReadData();
napi_value promise = nullptr;
napi_create_promise(env, &d->deferred, &promise);
napi_value name = nullptr;
napi_create_string_utf8(env, "ReadSensor", NAPI_AUTO_LENGTH, &name);
napi_create_async_work(env, nullptr, name,
ExecuteRead, CompleteRead, d, &d->work);
napi_queue_async_work(env, d->work);
LOGI("ReadSensor queued");
return promise;
}
// ---------- 导出 ----------
static napi_value SensorInit(napi_env env, napi_value exports)
{
napi_property_descriptor desc[] = {
{ "readSensor", nullptr, ReadSensor, nullptr, nullptr, nullptr, napi_default, nullptr },
};
napi_define_properties(env, exports, sizeof(desc)/sizeof(desc[0]), desc);
return exports;
}
7.2 类型声明
// index.d.ts
export interface SensorData {
temperature: number;
humidity: number;
}
export const readSensor: () => Promise<SensorData>;
7.3 ArkTS 界面
// pages/Index.ets
import nativeSensor from 'libentry.so';
import hilog from '@ohos.hilog';
@Entry
@Component
struct SensorPage {
@State temp: number = 0;
@State humi: number = 0;
@State loading: boolean = false;
@State lastUpdate: string = '--';
async refresh(): Promise<void> {
this.loading = true;
try {
let data: nativeSensor.SensorData = await nativeSensor.readSensor();
this.temp = data.temperature;
this.humi = data.humidity;
this.lastUpdate = new Date().toLocaleTimeString();
} catch (err) {
hilog.error(0x0000, 'SENSOR', '读取失败: %{public}s', JSON.stringify(err));
} finally {
this.loading = false;
}
}
aboutToAppear(): void {
this.refresh();
}
build() {
Column({ space: 20 }) {
Text('客厅 · 传感器').fontSize(20).fontWeight(FontWeight.Bold)
if (this.loading) {
LoadingProgress().width(60).height(60)
Text('读取中...').fontSize(14).fontColor('#999999')
} else {
Text(`${this.temp.toFixed(1)}°C`)
.fontSize(56).fontWeight(FontWeight.Bold)
.fontColor(this.temp > 28 ? '#E74C3C' : '#0F9D58')
Text(`湿度 ${this.humi.toFixed(0)}%`).fontSize(16).fontColor('#666666')
Text(`更新于 ${this.lastUpdate}`).fontSize(12).fontColor('#AAAAAA')
}
Button('刷新').width('60%').onClick(() => { this.refresh(); })
}
.width('100%').height('100%').padding(24).justifyContent(FlexAlign.Center)
}
}
“跑一下。”
我点运行。两百毫秒后,界面上的转圈消失,跳出一个数字:26.5°C。
"这个数字,"老陈说,“是从 C 代码里、从 I2C 时序里,一路穿过 NAPI 的桥,跑到你的 ArkTS 界面上的。”
我看着那个数字,忽然有点感慨。
“从第 04 章点亮那盏灯,到现在……”
"到现在,你已经把南向和北向打通了。"老陈接上我的话。
=================小剧场模式=================
我:老师,我总结了一下 NAPI 的心法,你看对不对。
老陈:说。
我:第一,ArkTS 和 C 是两个世界,NAPI 是桥。
老陈:对。
我:第二,过桥的东西都要"兑换"——napi_get_value_xxx 进来,napi_create_xxx 出去。
老陈:对。
我:第三,主线程才能碰 ArkTS,后台线程只能通过 async_work 或者 threadsafe_function 通信。
老陈:这条最重要。 记住它,你能避开 NAPI 里八成的崩溃。
我:还有一点我想不通——既然 NAPI 这么麻烦,为什么不干脆用 ArkTS 重写所有 C 代码?
老陈:因为生态。 你想用的那些库——OpenCV、FFmpeg、加解密库、各种芯片厂商提供的 SDK——全是 C/C++ 写的,几十年的积累。
我:重写一遍?
老陈:不现实。 光是 FFmpeg 就有上百万行代码。而且重写了性能还更差。
我:所以 NAPI 的价值是——
老陈:让你不用重写。 已有的 C 资产直接用,ArkTS 只负责界面和业务逻辑。
我:各干各的强项。
老陈:对。C 干它擅长的(性能、硬件),ArkTS 干它擅长的(界面、开发效率)。NAPI 把它们接起来。
========================================
八、传大数据:ArrayBuffer 而不是数组
"还有个性能大坑没讲。"老陈说,“你要往 ArkTS 传一千个采样点,怎么做?”
我想了想:“建个数组,一个一个 push?”
“那会慢到你想哭。“老陈说,”**每 push 一个元素都是一次跨语言调用。**一千次调用,几百毫秒。”
“那怎么办?”
“用 ArrayBuffer——一次传一整块内存。”
8.1 ArrayBuffer 方式
// C++ 侧:一次返回一整块采样数据
static napi_value ReadWaveform(napi_env env, napi_callback_info info)
{
// 假设采集了 N 个点
const int N = 1024;
float samples[N];
for (int i = 0; i < N; i++) {
samples[i] = ReadAdcOnce(); // 读 ADC
}
// ① 创建 ArrayBuffer
void *bufferData = nullptr;
napi_value arrayBuffer = nullptr;
napi_create_arraybuffer(env, N * sizeof(float), &bufferData, &arrayBuffer);
// ② 直接往这块内存里写(不需要逐个 napi_create_double!)
memcpy(bufferData, samples, N * sizeof(float));
// ③ 包一层 Float32Array 给 ArkTS(这样它才能按 float 读)
napi_value typedArray = nullptr;
napi_create_typedarray(env, napi_float32_array, N, arrayBuffer, 0, &typedArray);
return typedArray;
}
// ArkTS 侧
import native from 'libentry.so';
let wave: Float32Array = native.readWaveform(); // 一次拿到 1024 个点
console.info(`采样点数: ${wave.length}`);
console.info(`第一个点: ${wave[0]}`);
"关键区别在这——"老陈指着 memcpy 那行:
❌ 慢的方式(逐个转换):
for (i) { napi_create_double(env, samples[i], &v); napi_set_element(env, arr, i, v); }
→ N 次跨语言调用 + N 次对象创建
✅ 快的方式(ArrayBuffer):
napi_create_arraybuffer(...) + memcpy(...)
→ 1 次内存分配 + 1 次内存拷贝
“快多少?”
"1024 个点,能快几十倍。"老陈说,“而且数据量越大差距越明显。”
8.2 ArkTS 侧也能直接写 buffer
“反过来也行——ArkTS 分配 buffer,C 层直接往里写,避免拷贝。”
static napi_value FillBuffer(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);
// 确认是 ArrayBuffer
bool isAB = false;
napi_is_arraybuffer(env, args[0], &isAB);
if (!isAB) {
napi_throw_type_error(env, "ETYPE", "需要 ArrayBuffer");
return nullptr;
}
// 直接拿到 ArkTS 那块内存的地址(零拷贝!)
void *data = nullptr;
size_t length = 0;
napi_get_arraybuffer_info(env, args[0], &data, &length);
// 直接写,不拷贝
int count = length / sizeof(float);
float *out = static_cast<float *>(data);
for (int i = 0; i < count; i++) {
out[i] = ReadAdcOnce();
}
napi_value result = nullptr;
napi_create_int32(env, count, &result);
return result;
}
// ArkTS 侧:自己分配,交给 C 填
let buf = new ArrayBuffer(1024 * 4); // 1024 个 float
let n = native.fillBuffer(buf); // C 直接往里写
let view = new Float32Array(buf);
console.info(`填了 ${n} 个点,第一个是 ${view[0]}`);
“这个叫’零拷贝’——数据只在内存里存在一份,C 和 ArkTS 共享它。”
“有风险吗?”
“有,而且很大。“老陈严肃起来,”**C 层拿到的是裸指针,写越界了不会有任何保护,直接把相邻内存踩烂。**所以——”
零拷贝的三条铁律:
1. 必须检查 length,写之前算清楚能写多少
2. 绝不能把这个指针存起来以后用 —— 函数返回后它随时可能失效
3. 绝不能在后台线程用这个指针(GC 可能已经动过了)
"能用 memcpy 就别用零拷贝。"他总结,“零拷贝是性能优化的最后一步,不是第一步。”
8.3 实战:接入一个现成的 C 库(cJSON)
"说了半天’复用 C 生态’,来真的。"老陈说,“把 cJSON 接进来。”
“cJSON 是什么?”
“**一个几百 KB 的 C 语言 JSON 解析库。全世界的嵌入式项目都在用。**你要是自己用 ArkTS 解析 JSON,当然可以——但如果你要移植一套已有的 C 代码,它里面的 JSON 解析就是调 cJSON。”
步骤:
1. 下载 cJSON 源码(cJSON.c + cJSON.h)
2. 放进 cpp/thirdparty/cJSON/
3. 在 CMakeLists.txt 里加进编译
4. 写个 NAPI wrapper 暴露给 ArkTS
# cpp/CMakeLists.txt
include_directories(
${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/thirdparty/cJSON # ← 加头文件路径
)
add_library(entry SHARED
napi_init.cpp
sensor/sensor_napi.cpp
thirdparty/cJSON/cJSON.c # ← 加源文件
)
target_link_libraries(entry PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
libc++.a
)
// NAPI wrapper:把 cJSON 的能力暴露出去
#include "cJSON.h"
// 解析一个 JSON 字符串,取某个字段
static napi_value ParseJson(napi_env env, napi_callback_info info)
{
size_t argc = 2;
napi_value args[2] = { nullptr };
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// ① 取出 ArkTS 传来的字符串
size_t strLen = 0;
napi_get_value_string_utf8(env, args[0], nullptr, 0, &strLen);
char *jsonStr = new char[strLen + 1];
napi_get_value_string_utf8(env, args[0], jsonStr, strLen + 1, &strLen);
// 取字段名
size_t keyLen = 0;
napi_get_value_string_utf8(env, args[1], nullptr, 0, &keyLen);
char *key = new char[keyLen + 1];
napi_get_value_string_utf8(env, args[1], key, keyLen + 1, &keyLen);
// ② 用 cJSON 解析(这就是"复用 C 生态")
napi_value result = nullptr;
cJSON *root = cJSON_Parse(jsonStr);
if (root == nullptr) {
napi_get_undefined(env, &result);
} else {
cJSON *item = cJSON_GetObjectItem(root, key);
if (cJSON_IsString(item)) {
napi_create_string_utf8(env, item->valuestring, NAPI_AUTO_LENGTH, &result);
} else if (cJSON_IsNumber(item)) {
napi_create_double(env, item->valuedouble, &result);
} else if (cJSON_IsBool(item)) {
napi_get_boolean(env, cJSON_IsTrue(item), &result);
} else {
napi_get_undefined(env, &result);
}
cJSON_Delete(root); // ← 必须释放!
}
// ③ 清理自己 new 的内存
delete[] jsonStr;
delete[] key;
return result;
}
// ArkTS 侧
let v = native.parseJson('{"name":"SHT30","addr":68,"ok":true}', 'name');
console.info(v); // "SHT30"
"这个例子演示了 NAPI 最典型的价值——"老陈说:
┌─────────────────────┐
│ 你的 ArkTS 应用 │
└──────────┬──────────┘
│ NAPI wrapper(你写的,几十行)
┌──────────▼──────────┐
│ cJSON(别人的库) │ ← 一行没改,直接用
│ / FFmpeg / OpenCV │
│ / 芯片厂商 SDK │
└─────────────────────┘
“**你只写了几十行 wrapper,就拥有了一个成熟的 C 库。**这就是 NAPI 最大的意义。”
"注意 cJSON_Delete 和 delete[]。"他敲黑板,“C 层没有垃圾回收,你 new 的、库分配的,全得自己还。”
九、常见坑
| 现象 | 原因 | 解法 |
|---|---|---|
import 找不到 so |
模块名不匹配 | nm_modname = “entry” → libentry.so → import ... from 'libentry.so' |
| 方法调用报 undefined | ① 没在 Init 里注册② 名字拼错 |
检查 napi_property_descriptor 数组 |
| 调用后应用直接崩溃 | ① 参数没检查,C 层空指针 ② 后台线程碰了 napi_env③ 返回了 nullptr 但没抛异常 |
C 层的崩溃不会变成 JS 异常,直接是应用闪退 |
| AsyncWork 回调不执行 | 没调 napi_queue_async_work |
检查队列调用 |
| 内存持续增长 | ① napi_ref 没 delete_reference② async_work 没 delete_async_work③ new 出来的数据没 delete |
每一对 create/delete 都要配对 |
| 日志打不出来 | 用了 printf 而不是 HILOG |
用 HILOG_INFO/ERROR |
| 编译报找不到头文件 | CMakeLists 里 include_directories 缺路径 |
补上 |
"最后一条经验——"老陈说,“C 层代码要写得比 ArkTS 保守十倍。”
“为什么?”
“因为 ArkTS 里抛出异常,最坏是这个页面挂了;C 里一个野指针,是整个应用闪退,连日志都来不及打。”
“所以——”
“参数一律检查,指针一律判空,资源一律配对释放。”
十、本章速查
【核心概念】
napi_env 运行环境上下文
napi_value ArkTS 值的句柄(不是数据本身!)
napi_callback_info 调用信息(取参数用)
napi_status 返回值,napi_ok = 成功
【类型转换】
进来:napi_get_value_int32 / _double / _string_utf8
出去:napi_create_int32 / _double / _string_utf8 / _object / _array
检查:napi_typeof(env, val, &type)
【注册模块】
napi_property_descriptor desc[] = {{"方法名", nullptr, C函数, ...}};
napi_define_properties(env, exports, n, desc);
static napi_module g_module = { .nm_modname = "entry", .nm_register_func = Init };
extern "C" __attribute__((constructor)) void Register() { napi_module_register(&g_module); }
【异步三件套】
napi_create_async_work(env, nullptr, name, Execute, Complete, data, &work);
napi_queue_async_work(env, work); ← 别忘!
Execute:后台线程,只干纯计算/硬件
Complete:主线程,才能碰 JS
【Promise】
napi_create_promise(env, &deferred, &promise);
napi_resolve_deferred(env, deferred, result);
napi_reject_deferred(env, deferred, error);
【线程安全】
主线程:随便用 napi_env
后台线程:只能用 napi_threadsafe_function 通知 JS
【铁律】
C 层崩溃 = 应用闪退,没有异常保护
参数必检查、指针必判空、资源必配对释放
"下一章,"老陈说,“我们要把你做的东西,变成别人能装、能用的产品。”
“上架?”
"上架。"他说,“HAP 包、签名、元服务、应用市场——最后这一公里。”
本章自检
过一遍下面的问题。答不上来的,回去翻对应小节——那些就是你这个章的欠账。
- ☐ ArkTS 为什么不能直接调用 C 函数?(说出三个原因)
- ☐
napi_value是数据本身吗?它是什么? - ☐ 什么情况下必须用异步?同步调用会怎样?
- ☐ 后台线程能碰
napi_env吗? 要通知 JS 该怎么办? - ☐ 传 1024 个采样点,用什么方式?为什么不能用数组?
- ☐ C 层崩溃和 ArkTS 层抛异常,后果有什么不同?
- ☐ 记住三个「必」:参数必___、指针必___、资源必___。
**➡️ 下一站:**第 11 章 · 出师 —— 把它变成别人能装、能用的产品。
=================剧情片段=================
导演:卡。南北打通了。
助理:这一章技术难度不低——ABI、异步模型、线程安全。
导演:但主线很清楚:两个世界,一座桥。 剩下的都是桥上的细节。
助理:主角现在算是"能做东西"了吧?
导演:算是。 从硬件到界面,全链路走通了。
助理:那最后两章要讲什么?
导演:第 11 章:把东西变成产品。 包结构、签名、上架、元服务、生态和就业方向。
助理:第 12 章呢?
导演:大结局。 一个完整的收尾项目,师傅离开,还有——
助理:还有什么?
导演:回扣开篇。 让他想起第一次进那间落灰的实验室时,自己是什么样。
========================================
未完待续。。。


