HarmonyOS 7 JsonPulse Native三方库适配实录 01:simdjson × Node-API:CMake接入、模块注册与首个Native解析接口【鸿蒙心迹】
FlexDesk 系列结束以后,这一轮换到一个明显不同的方向:HarmonyOS 7 三方 Native 库适配。
新 Demo 叫 JsonPulse。
它不做 UI 新特性,也不碰分布式能力,主线只有一件事:把一个成熟 C++ 三方库真正接进 ArkTS 工程,再把“能编译”推进到“能稳定调用、能测性能、能解释生命周期”。
这次选的库是 simdjson。
原因也很直接。它是一个成熟的 C++ JSON 解析库,官方提供单头文件接入方式,并长期强调 parser 复用、padding、输入缓冲区生命周期等工程边界。HarmonyOS 侧则通过 Node-API 把 ArkTS/JS 与 C/C++ 连接起来,非常适合用来做一条完整的三方 Native 适配链。
整个系列固定 6 篇:
01 CMake × Node-API:三方库编译、模块注册与首个解析接口
02 Uint8Array × Buffer Lifetime:TypedArray、padding 与零拷贝边界
03 napi_async_work × Background Parse:大文件异步解析与主线程回调
04 ThreadSafeFunction × Progress:Native 子线程进度、取消与线程安全
05 HAR × Prebuilt SO:预构建库、ABI、符号与分发打包
06 Metrics × Regression:多数据集、多ABI、性能与资源回归
第一篇先不追求极限性能。
只把最基础的事情做稳:
simdjson 能被 HarmonyOS 工程编译进来,Node-API 模块注册正确,ArkTS 可以调用一个稳定的
parseSummary()接口。
本轮统一数据:
taskId:
native_20261003_01
project:
JsonPulse
library:
simdjson
nativeLib:
libjsonpulse.so
sourceFile:
orders_2_4mb.json
inputBytes:
2,516,582
records:
12,480
fields:
24
maxDepth:
6
buildType:
Release
cxxStandard:
C++17
moduleRegistered:
true
arktsCallSuccess:
true
parseCost:
18.6ms
bridgeCost:
2.9ms
totalCost:
21.5ms
parseErrors:
0
status:
NATIVE_BRIDGE_READY

一、第一篇不先谈 SIMD,而先解决“库到底属于谁”
Native 库适配最容易一上来就写:
ArkTS 调 C++
C++ 调 simdjson
但真正落到工程里,至少有三层:
ArkTS / JS
Node-API Bridge
Third-party C++
JsonPulse 先把目录拆开:
entry/src/main/cpp/
├─ native/
│ ├─ napi_init.cpp
│ ├─ jsonpulse.cpp
│ └─ jsonpulse.h
└─ third_party/
└─ simdjson/
├─ simdjson.h
└─ simdjson.cpp
这个结构有一个很实际的好处。
后面如果 simdjson 升级,只替换:
third_party/simdjson
业务桥接代码仍然在 native/。
三方源码不会和项目自己的 Node-API 逻辑搅在一起。
二、CMake 先把 C++17 和依赖关系写清楚
simdjson 官方文档当前推荐现代 C++ 编译环境,单头文件方式也很适合嵌进应用工程。
JsonPulse 当前用 Release 构建,并明确指定:
C++17
第一段代码解决的是:
如何让三方源码、项目 Native 源码和 HarmonyOS Node-API 依赖一次性进入同一个 so。
cmake_minimum_required(VERSION 3.10)
project(jsonpulse)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_library(jsonpulse SHARED
native/napi_init.cpp
native/jsonpulse.cpp
third_party/simdjson/simdjson.cpp
)
target_include_directories(jsonpulse
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/native
${CMAKE_CURRENT_SOURCE_DIR}/third_party/simdjson
)
target_compile_definitions(jsonpulse
PRIVATE
NDEBUG
)
target_link_libraries(jsonpulse
PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
)
这里 libace_napi.z.so 是 Node-API 桥接真正依赖的系统库。
libhilog_ndk.z.so 则用于 Native 侧 HiLog。
这一层如果没写对,ArkTS 代码再漂亮也没有意义。
三、模块名必须和 so 名对得上
HarmonyOS 当前 Node-API 规范对模块注册有明确要求:
一个 so 注册一个模块;
nm_modname
必须和 so 二进制名称匹配;
注册函数需要避免和其他 so 的构造函数重名。
JsonPulse 最终 so:
libjsonpulse.so
模块名:
jsonpulse
第二段代码解决的是“模块能不能被 ArkTS 正确 import”。
#include "napi/native_api.h"
static napi_value
ParseSummary(
napi_env env,
napi_callback_info info);
EXTERN_C_START
static napi_value Init(
napi_env env,
napi_value exports)
{
napi_property_descriptor props[] = {
{
"parseSummary",
nullptr,
ParseSummary,
nullptr,
nullptr,
nullptr,
napi_default,
nullptr
}
};
napi_define_properties(
env,
exports,
sizeof(props) /
sizeof(props[0]),
props);
return exports;
}
EXTERN_C_END
static napi_module jsonPulseModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "jsonpulse",
.nm_priv = nullptr,
.reserved = { 0 }
};
extern "C"
__attribute__((constructor))
void RegisterJsonPulseModule()
{
napi_module_register(
&jsonPulseModule);
}
本轮:
moduleRegistered=true
这个状态不是页面自己写出来的。
Native 初始化成功以后才进入 Ready。
四、第一版接口故意只返回摘要,不把整个 DOM 搬回 ArkTS
2.4MB JSON 如果完整解析以后,又把整个对象树重新构造成 ArkTS Object:
Native 解析很快
→ Native → ArkTS 大量对象转换
→ 桥接成本重新变大
第一篇先定义一个很克制的接口:
parseSummary(json)
只返回:
records
fields
maxDepth
parseCost
errors
也就是说,先证明:
simdjson 的核心能力确实已经进入工程,而不是先把跨语言边界做成另一个性能瓶颈。
五、Native 侧 parser 负责 JSON,Node-API 只做语言边界
第三段代码解决的是:
Bridge 不参与 JSON 业务解析,只负责取参数和返回结果。
#include "jsonpulse.h"
#include "simdjson.h"
#include <chrono>
ParseSummaryResult
JsonPulse::Parse(
const std::string& json)
{
simdjson::dom::parser parser;
const auto start =
std::chrono::steady_clock::now();
simdjson::dom::element doc;
auto error =
parser.parse(json)
.get(doc);
const auto end =
std::chrono::steady_clock::now();
ParseSummaryResult result {};
result.parseCostMs =
std::chrono::duration<double,
std::milli>(
end - start
).count();
if (error) {
result.errors = 1;
return result;
}
result.records =
CountRecords(doc);
result.fields =
CountTopLevelFields(doc);
result.maxDepth =
MeasureDepth(doc);
return result;
}
这里真正“知道 JSON 结构”的是 JsonPulse C++ 层。
napi_init.cpp 只做:
ArkTS string
→ std::string
→ JsonPulse::Parse
→ napi object
边界非常清楚。
六、为什么第一篇先用 string,而不是马上上 Uint8Array
因为第一篇要验证的是:
Build
Register
Import
Call
Result
String 是最容易观察的输入方式。
如果 01 就同时引入:
ArrayBuffer
TypedArray
padding
native pointer
buffer lifetime
一旦出错,很难判断是模块注册有问题,还是 Buffer 生命周期有问题。
所以先用简单路径建立:
NATIVE_BRIDGE_READY
02 再专门优化输入边界。
七、ArkTS 侧的类型声明必须跟 so 一起维护
Node-API 成功注册以后,ArkTS 还需要清晰的类型定义。
export interface ParseSummary {
records: number
fields: number
maxDepth: number
parseCostMs: number
errors: number
}
export const parseSummary:
(json: string) =>
ParseSummary
页面最终:
import jsonpulse
from 'libjsonpulse.so'
const result =
jsonpulse.parseSummary(
jsonText
)
这个 .d.ts 不是“可有可无的 IDE 提示”。
它是 ArkTS 和 Native API 契约的一部分。
后面接口升级时,也要跟 C++ 同步版本化。
八、2,516,582 bytes 不等于“2.4MB 很小,可以随便拷”
当前输入:
2,516,582 bytes
约 2.4MB。
字符串桥接时仍然会发生:
ArkTS string
→ UTF-8 length query
→ Native string buffer
本轮:
bridgeCost=2.9ms
占总耗时并不算特别高。
但输入继续变成 5MB、20MB、50MB 时,桥接成本会越来越值得关注。
这就是 02 为什么直接换成 Uint8Array。
九、18.6ms 只是 JsonPulse 当前测试数据
本轮:
records=12,480
fields=24
maxDepth=6
parseCost=18.6ms
这个数字来自当前测试设备、当前输入、当前 Release 构建。
不能写成:
simdjson 在 HarmonyOS 固定只要 18.6ms
simdjson 官方也强调:
数据结构
数字密度
parser 复用
输入方式
CPU 实现
都会影响性能。
这篇只把它当成后续回归基线。
十、为什么 Release 构建要显式 NDEBUG
simdjson 官方性能建议里提到:
Release
建议定义 NDEBUG
因为:
-O2 / -O3
本身不一定自动设置这个宏。
JsonPulse 当前 CMake 显式:
target_compile_definitions(
jsonpulse
PRIVATE
NDEBUG
)
第一篇就把构建配置记录下来,后面性能数据才有比较意义。
十一、解析错误必须从 Native 返回,不要在 C++ 里吞掉
如果输入 JSON 非法:
ParseSummary
不能只返回:
records=0
否则 ArkTS 根本不知道:
真的没有记录
还是解析失败。
JsonPulse 当前返回:
errors
errorCode
并在 Bridge 层转成稳定的业务结果或抛出明确异常。
本轮:
parseErrors=0
只是测试输入有效,不代表错误处理可以省。
十二、DevEco 图重点看三层工程边界
开发图:

左侧目录明确分:
native
third_party
ets
中间:
CMake
模块注册
ParseSummary
右侧模拟器统一展示:
taskId=
native_20261003_01
records=
12,480
parse=
18.6ms
bridge=
2.9ms
total=
21.5ms
status=
NATIVE_BRIDGE_READY
底部 HiLog 则把同一套数据打印出来。
十三、手机图不是“性能宣传”,而是一次可追踪任务
运行图:

这次任务完整信息:
orders_2_4mb.json
2,516,582 bytes
12,480 records
24 fields
depth 6
18.6ms native parse
2.9ms bridge
21.5ms total
0 errors
最终:
NATIVE_BRIDGE_READY
说明第一条 ArkTS → Node-API → simdjson → ArkTS 的闭环已经成立。
十四、第一篇最后固定六组反向测试
第一组,合法 2.4MB JSON,正常返回摘要。
第二组,非法 JSON,errors 不为 0。
第三组,ArkTS 传空字符串,Native 返回明确参数错误。
第四组,模块名与 so 名不匹配,构建阶段直接发现。
第五组,Debug 与 Release 分别构建,性能数据只采 Release。
第六组,连续调用 20 次,模块不会重复注册,结果一致。
全部通过以后:
NATIVE_BRIDGE_READY
才成立。
十五、下一篇真正开始处理“数据怎么跨语言”
01 当前最明显的额外成本来自:
ArkTS string
→ Native string
而 JsonPulse 实际业务里的 JSON 很多来自:
文件
网络二进制响应
压缩包
数据库 blob
它们本来就可以表现成:
Uint8Array
02 会继续同一个工程,改成:
Uint8Array
→ napi_get_typedarray_info
→ Native pointer
→ simdjson parse_unpadded
重点处理:
padding
buffer 生命周期
同步零拷贝边界
异步场景不能偷留裸指针
真正进入 Native 性能优化阶段。
十六、Node-API 的模块边界要比业务接口更稳定
很多 Native Demo 的第一版会直接把 C++ 函数一股脑导出来:
parse
parseFile
parseCount
getField
getValue
getArray
...
短期很方便,长期会让 ArkTS 和 C++ 紧紧绑在一起。
JsonPulse 第一篇只暴露:
parseSummary
并不是因为 simdjson 只能做这点事,而是因为跨语言 API 一旦进入业务代码,就应该尽量稳定。
内部以后可以从:
simdjson DOM
换成:
On-Demand
也可以增加 parser pool,ArkTS 都不需要知道。
跨语言边界最好表达业务能力,不要把三方库的每一个方法一比一翻译成 ArkTS API。
否则未来三方库升级、函数签名调整,会把改动扩散到整个页面层。
十七、C++ 异常不要直接穿过 Node-API 边界
三方 C++ 库可能:
返回错误码;
返回 result<T>;
或者抛异常。
Node-API 边界不应该让 C++ 异常直接逃出。
JsonPulse 的 Bridge 会统一:
try
→ Native parse
catch std::exception
→ napi_throw_error
未知异常
→ napi_throw_error(NATIVE_UNKNOWN)
ArkTS 最终只接收两种结果:
成功的 ParseSummary
或
明确的 JS / ArkTS Error
不会出现 Native 层异常栈直接把应用打崩的情况。
这个错误归一化层会继续沿用到 03 的异步任务。
十八、Parser 是否复用要先考虑线程模型
simdjson 官方文档建议复用 parser,避免重复分配内部容量。
JsonPulse 01 没有急着做:
global static parser
因为 03 以后会进入异步线程。
如果一个全局 parser 被多个任务同时使用,会直接引入线程安全和文档生命周期问题。
当前策略是:
01 / 02
同步单任务,局部 parser;
03
异步任务每个 work item 自己持有 parser;
06
再根据实际数据决定是否做 per-thread parser cache。
优化应该建立在明确线程 Owner 之上,而不是为了少一次分配先放一个全局单例。
十九、Native API 的返回对象也要控制字段数量
跨语言调用还有一个容易忽略的成本:
napi_create_object
napi_set_named_property
napi_create_string
napi_create_double
如果 C++ 解析完 12,480 条记录,然后给 ArkTS 创建 12,480 个对象,桥接成本可能远高于解析本身。
所以第一篇故意只返回:
records
fields
maxDepth
parseCost
errors
这是一种“Native 计算,ArkTS 消费摘要”的接口形态。
以后如果页面确实要展示 100 条数据,可以再做:
分页
字段投影
局部结果
而不是默认把完整 DOM 跨语言复制一遍。
二十、模块注册成功也要检查 ArkTS 声明和实际导出是否一致
一种常见错误是:
C++ 导出 parseSummary
.d.ts 写 parseJson
ArkTS 编译可能通过某些宽松声明
运行时却找不到方法。
JsonPulse 在启动自检里会执行:
typeof jsonpulse.parseSummary
并记录:
arktsCallSuccess=true
后续 HAR 打包时,这条检查仍然保留。
因为 Native 库最难排查的故障之一就是:
so 已经加载
但导出的属性名和声明文件不一致。
二十一、第三方源码不要直接在业务目录里魔改
适配三方库时很容易为了“先编过去”直接改:
third_party/simdjson/simdjson.cpp
几行。
下一次升级库版本时,这些改动就很难找回来。
JsonPulse 采用:
third_party/
保持接近上游源码;
native/compat/
放项目适配层;
CMake
负责编译开关。
如果必须补 Patch,也单独保存 patch 文件或 Git commit。
这样 05 做预构建库和版本升级时,才能清楚知道哪些是上游代码,哪些是 HarmonyOS 适配代码。
二十二、C++ ABI 边界从第一篇就要避免泄漏 STL 类型
HarmonyOS 当前 C/C++ 标准库机制说明里专门提醒了系统和应用侧 C++ 运行库的隔离问题。
虽然 JsonPulse 现在所有代码都编进同一个应用 so,但为了后面把 simdjson 单独封成预构建库,接口层已经避免对外暴露:
std::string
std::vector
std::map
这类 STL 类型作为二进制 ABI。
对外层更倾向:
const uint8_t*
size_t
POD struct
C function
或者直接通过 Node-API 返回 ArkTS 值。
这样未来三方库换编译版本时,不会让业务模块被 STL ABI 细节绑死。
二十三、HiLog 里必须能看出“桥接慢”还是“解析慢”
第一篇最终日志拆成:
bridgeCost
parseCost
totalCost
如果以后变慢:
parseCost 18.6 → 18.9
bridgeCost 2.9 → 20
问题明显在跨语言或字符串转换。
反过来:
bridge 仍然 3ms
parse 变成 80ms
才去查:
输入结构
simdjson 配置
构建优化
CPU 实现
这种分段证据比一个 total=83ms 更有工程价值。
二十四、第一篇真正形成的是一条可升级的 Native 接入协议
做到这里,JsonPulse 01 已经固定:
三方源码独立目录;
CMake 明确 C++ 标准和系统库;
so 名与 nm_modname 一致;
模块只注册一次;
Node-API 只做语言边界;
业务解析留在 C++ Service;
ArkTS 用 d.ts 描述稳定接口;
日志拆分 bridge / parse / total。
这套协议后面即使把 simdjson 换成其他 C++ 库,也能继续复用。
第一篇的价值不是“JSON 解析快了多少”,而是 HarmonyOS 工程终于有了一条可维护的三方 Native 入口。
二十五、首个 Native 接口还要做版本号自检
JsonPulse 在模块里额外暴露一个轻量 getNativeVersion(),返回当前 Bridge 版本、simdjson 集成修订号和编译类型。页面启动时会把它和 ArkTS 侧期望版本比较。如果应用更新了 .d.ts,但安装包里仍然带着旧 so,就能在真正调用解析前直接报出 NATIVE_VERSION_MISMATCH。这类问题在增量构建、共享 HAR 或多人协作时并不少见。相比“方法不存在”或“字段少了一个”这种运行时故障,启动自检更容易定位。
同时,版本号不直接等同于上游 simdjson 版本。JsonPulse 维护的是自己的 Bridge Revision,因为同一个上游版本可能对应不同的 CMake 开关、Patch 和导出接口。后续 05 做预构建 so 时,这个版本信息还会进入包内 Manifest。
二十六、第一篇的工程验收还要包含“卸载后重新安装”
Native 模块最容易被本地开发环境掩盖的问题之一,是旧 so 残留。JsonPulse 因此把干净安装也放进 01 的验收:清理应用数据、重新构建 Release、安装后首次启动,再执行 parseSummary。如果只有热更新或增量编译能工作,而干净安装失败,说明打包链并没有真正闭环。最终这条用例通过以后,才把 NATIVE_BRIDGE_READY 当作可继续演进的基线。
参考资料
- HarmonyOS Node-API 跨语言调用:
https://developer.huawei.com/consumer/cn/doc/doccenter-games/games-universal-using-napi-interaction-0000002411166425 - Node-API 开发规范:
https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/napi-guidelines - C/C++ 标准库机制:
https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/c-cpp-overview - simdjson:
https://github.com/simdjson/simdjson
更多推荐



所有评论(0)