0
0
0

鸿蒙造物者之路 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 章呢?

导演:大结局。 一个完整的收尾项目,师傅离开,还有——

助理:还有什么?

导演:回扣开篇。 让他想起第一次进那间落灰的实验室时,自己是什么样。

========================================

未完待续。。。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论

欢迎来到我的博客!