OpenHarmony NAPI 移植第三方 SO 库 cJSON|WSL2 交叉编译 + 真机调通
环境清单:
宿主机:Windows
虚拟机:WSL2 Ubuntu‑22.04
目标设备:OpenHarmony 真机(arm64‑v8a)
第三方库:cJSON v1.7.19
工具链:OpenHarmony SDK clang 交叉编译工具
一、WSL2 虚拟机环境准备
本人已提前配置好完整 OpenHarmony 源码编译环境(HPBUILD),可以直接编译适配鸿蒙系统的第三方组件。
进入 OpenHarmony 源码编译工作目录,切换到 lycium 编译工程目录,该目录用来存放第三方组件源码与 BUILD.gn 编译脚本。
重点:不能使用 WSL 本机 gcc 编译器,必须使用鸿蒙 HB 框架自带 clang 交叉工具链,否则生成 x86 架构库,鸿蒙开发板无法加载运行。
二、导入 cJSON 源码并配置编译脚本
把 cJSON 源码放置到 lycium 工程内,编写对应的BUILD.gn编译脚本,让鸿蒙 hb 编译工具识别源码,完成交叉编译配置。 脚本配置指定编译输出动态库libcjson.so,导出头文件,关闭测试程序与示例代码,只保留核心库代码。
三、执行 HPBUILD 交叉编译

在 WSL 终端执行 hb 构建命令,读取 BUILD.gn 配置,触发 cJSON 的交叉编译。
编译完成后输出路径:lycium/usr/cJSON/arm64‑v8a/

目录下产物:
include/cjson/cJSON.h:库头文件lib/libcjson.so:核心动态库lib/libcjson.so.*:一系列软链接文件,Windows 无法识别,工程中不需要使用
四、WSL 文件导出到 Windows(供 DevEco 使用)
我使用cp命令将编译产出从 WSL 内部目录复制到 Windows 磁盘映射目录 /mnt/d/
# 将头文件复制到Windows D盘的输出文件夹
cp include/cjson/cJSON.h /mnt/d/ohos_cjson_out/
# 将真实so实体库复制过去,不复制软链接
cp lib/libcjson.so /mnt/d/ohos_cjson_out/
只复制两份核心文件,所有软链接文件全部舍弃不要拷贝:
include/cjson/cJSON.h 头文件
lib/libcjson.so 动态库
复制出来的头文件和 so 库,后续拷贝到 DevEco Studio NAPI 工程目录下使用。
五、DevEco Studio 工程适配
5.1 创建 NAPI 原生工程
1. 打开 DevEco Studio → File → New → Create Project
2. 模板选择 Native C++
3. 新建完成后工程自带两个关键文件,它们就是 NAPI 的骨架:
- entry/src/main/cpp/napi_init.cpp — NAPI 入口(模板自带一个 add 示例函数)
- entry/src/main/cpp/CMakeLists.txt — CMake 构建脚本
5.2 导入 cJSON 头文件与动态库
把上篇编译产出的两个文件拷贝到工程对应位置(注意目录层级,见踩坑问题 3):
MyApplication/
└── entry/
├── libs/
│ └── arm64-v8a/ ← 原生库目录(按 ABI 分目录)
│ ├── libcjson.so ← 从 WSL 复制来的动态库
│ └── libcjson.so.1 ← ⚠️ 必须再复制一份!见踩坑问题 1
└── src/main/cpp/
└── include/
└── cjson/
└── cJSON.h ← 头文件(保留 cjson 二级目录,不要压平)
> ⚠️ libcjson.so.1 从哪来:直接把 libcjson.so 复制一份改名为 libcjson.so.1 即可(两个文件内容相同)。
5.3 修改 CMakeLists.txt 链接三方库
entry/src/main/cpp/CMakeLists.txt 完整内容:
# the minimum version of CMake.
cmake_minimum_required(VERSION 3.5.0)
project(MyApplication)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
if(DEFINED PACKAGE_FIND_FILE)
include(${PACKAGE_FIND_FILE})
endif()
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include)
# cJSON 三方库:预编译产物位于 entry/libs/${OHOS_ARCH}/libcjson.so
set(CJSON_LIB_PATH ${NATIVERENDER_ROOT_PATH}/../../../libs/${OHOS_ARCH})
add_library(entry SHARED napi_init.cpp)
target_link_libraries(entry PUBLIC libace_napi.z.so)
target_link_directories(entry PUBLIC ${CJSON_LIB_PATH})
target_link_libraries(entry PUBLIC cjson)
说明:
- include_directories 让 #include "cjson/cJSON.h" 能找到头文件;
- ${OHOS_ARCH} 是鸿蒙 CMake 工具链提供的变量(arm64-v8a / x86_64),DevEco 构建时自动注入;
- target_link_directories + target_link_libraries(... cjson) 链接 libcjson.so;
- 相对路径层级:${NATIVERENDER_ROOT_PATH} 是 entry/src/main/cpp,到 entry/libs 需要 ../../../libs(往上三级),少一级就会链接报错 unable to find library -lcjson(见踩坑问题 2)。
5.4 编写 NAPI 封装代码(napi_init.cpp)
设计思路——高层便捷封装:不在 ArkTS 侧暴露 cJSON 句柄,而是把 cJSON 树与 JS 对象做双向递归翻译,ArkTS 用起来和原生 JSON 一样自然,内存由 C++ 侧统一管理(cJSON_Delete / cJSON_free)。
对外只暴露 3 个函数:
| 接口 | 说明
|---|---|
| version(): string | 返回 cJSON 版本号 |
| parse(json: string): object \| null | 解析 JSON 字符串为 JS 对象,失败返回 null |
| stringify(value): string | 把 JS 对象序列化为 JSON 字符串,含不可序列化类型时抛TypeError
entry/src/main/cpp/napi_init.cpp 完整内容:
#include "napi/native_api.h"
#include "cjson/cJSON.h"
#include <vector>
// ---------- cJSON 节点 -> JS 值(递归) ----------
static napi_value CJsonToJsValue(napi_env env, const cJSON *item)
{
if (item == nullptr) {
napi_value nullVal;
napi_get_null(env, &nullVal);
return nullVal;
}
napi_value result = nullptr;
if (cJSON_IsTrue(item)) {
napi_get_boolean(env, true, &result);
} else if (cJSON_IsFalse(item)) {
napi_get_boolean(env, false, &result);
} else if (cJSON_IsNull(item)) {
napi_get_null(env, &result);
} else if (cJSON_IsNumber(item)) {
napi_create_double(env, cJSON_GetNumberValue(item), &result);
} else if (cJSON_IsString(item)) {
napi_create_string_utf8(env, cJSON_GetStringValue(item), NAPI_AUTO_LENGTH, &result);
} else if (cJSON_IsArray(item)) {
int size = cJSON_GetArraySize(item);
napi_create_array_with_length(env, size, &result);
int index = 0;
cJSON *child = nullptr;
cJSON_ArrayForEach(child, item) {
napi_value elem = CJsonToJsValue(env, child);
napi_set_element(env, result, index++, elem);
}
} else if (cJSON_IsObject(item)) {
napi_create_object(env, &result);
cJSON *child = nullptr;
cJSON_ArrayForEach(child, item) {
napi_value elem = CJsonToJsValue(env, child);
napi_set_named_property(env, result, child->string, elem);
}
} else {
// cJSON_Raw / Invalid 等兜底为 null
napi_get_null(env, &result);
}
return result;
}
// ---------- JS 值 -> cJSON 节点(递归) ----------
static cJSON *JsValueToCJson(napi_env env, napi_value value)
{
napi_valuetype type;
napi_typeof(env, value, &type);
switch (type) {
case napi_undefined:
case napi_null: {
return cJSON_CreateNull();
}
case napi_boolean: {
bool b = false;
napi_get_value_bool(env, value, &b);
return cJSON_CreateBool(b ? 1 : 0);
}
case napi_number: {
double d = 0;
napi_get_value_double(env, value, &d);
return cJSON_CreateNumber(d);
}
case napi_string: {
size_t len = 0;
napi_get_value_string_utf8(env, value, nullptr, 0, &len);
std::vector<char> buf(len + 1);
napi_get_value_string_utf8(env, value, buf.data(), len + 1, &len);
return cJSON_CreateString(buf.data());
}
case napi_object: {
bool isArray = false;
napi_is_array(env, value, &isArray);
if (isArray) {
uint32_t length = 0;
napi_get_array_length(env, value, &length);
cJSON *arr = cJSON_CreateArray();
for (uint32_t i = 0; i < length; i++) {
napi_value item = nullptr;
napi_get_element(env, value, i, &item);
cJSON *citem = JsValueToCJson(env, item);
if (citem == nullptr || !cJSON_AddItemToArray(arr, citem)) {
cJSON_Delete(citem);
cJSON_Delete(arr);
return nullptr;
}
}
return arr;
}
napi_value names = nullptr;
napi_get_property_names(env, value, &names);
uint32_t count = 0;
napi_get_array_length(env, names, &count);
cJSON *obj = cJSON_CreateObject();
for (uint32_t i = 0; i < count; i++) {
napi_value name = nullptr;
napi_value prop = nullptr;
napi_get_element(env, names, i, &name);
napi_get_property(env, value, name, &prop);
size_t nlen = 0;
napi_get_value_string_utf8(env, name, nullptr, 0, &nlen);
std::vector<char> nbuf(nlen + 1);
napi_get_value_string_utf8(env, name, nbuf.data(), nlen + 1, &nlen);
cJSON *cprop = JsValueToCJson(env, prop);
if (cprop == nullptr || !cJSON_AddItemToObject(obj, nbuf.data(), cprop)) {
cJSON_Delete(cprop);
cJSON_Delete(obj);
return nullptr;
}
}
return obj;
}
default: {
// function / symbol / external 等不可序列化类型
return nullptr;
}
}
}
// ---------- NAPI 导出函数 ----------
// version(): string
static napi_value Version(napi_env env, napi_callback_info info)
{
napi_value result;
napi_create_string_utf8(env, cJSON_Version(), NAPI_AUTO_LENGTH, &result);
return result;
}
// parse(json: string): object | null —— 解析失败返回 null
static napi_value Parse(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 nullVal = nullptr;
napi_get_null(env, &nullVal);
if (argc < 1) {
napi_throw_type_error(env, nullptr, "parse: expected 1 argument (string)");
return nullptr;
}
size_t len = 0;
napi_get_value_string_utf8(env, args[0], nullptr, 0, &len);
if (len == 0) {
napi_throw_type_error(env, nullptr, "parse: argument must be a non-empty string");
return nullptr;
}
std::vector<char> buf(len + 1);
napi_get_value_string_utf8(env, args[0], buf.data(), len + 1, &len);
cJSON *root = cJSON_Parse(buf.data());
if (root == nullptr) {
return nullVal;
}
napi_value result = CJsonToJsValue(env, root);
cJSON_Delete(root);
return result;
}
// stringify(value: object | string | number | boolean | null | Array): string
static napi_value Stringify(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);
if (argc < 1) {
napi_throw_type_error(env, nullptr, "stringify: expected 1 argument");
return nullptr;
}
cJSON *root = JsValueToCJson(env, args[0]);
if (root == nullptr) {
napi_throw_type_error(env, nullptr, "stringify: value contains unsupported type");
return nullptr;
}
char *text = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (text == nullptr) {
napi_throw_error(env, nullptr, "stringify: cJSON_Print failed");
return nullptr;
}
napi_value result;
napi_create_string_utf8(env, text, NAPI_AUTO_LENGTH, &result);
cJSON_free(text);
return result;
}
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports)
{
napi_property_descriptor desc[] = {
{ "version", nullptr, Version, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "parse", nullptr, Parse, nullptr, nullptr, nullptr, napi_default, nullptr },
{ "stringify", nullptr, Stringify, 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 demoModule = {
.nm_version = 1,
.nm_flags = 0,
.nm_filename = nullptr,
.nm_register_func = Init,
.nm_modname = "entry",
.nm_priv = ((void*)0),
.reserved = { 0 },
};
extern "C" __attribute__((constructor)) void RegisterEntryModule(void)
{
napi_module_register(&demoModule);
}
> 注意:cJSON 的 cJSON_ArrayForEach`宏在 C++ 下必须先声明迭代变量(cJSON *child = nullptr;),否则编译报 use of undeclared identifier 'child'。
5.5 更新类型声明 Index.d.ts
entry/src/main/cpp/types/libentry/Index.d.ts 完整内容:
export const version: () => string;
export const parse: (json: string) => object | null;
export const stringify: (value: object | string | number | boolean | null | undefined) => string;
5.6 ArkTS 页面调用演示
entry/src/main/ets/pages/Index.ets 完整内容(启动即演示,无需点击):
import { hilog } from '@kit.PerformanceAnalysisKit';
import testNapi from 'libentry.so';
const DOMAIN = 0x0000;
@Entry
@Component
struct Index {
@State message: string = 'cJSON 初始化中...';
aboutToAppear(): void {
this.runCjsonDemo();
}
runCjsonDemo(): void {
try {
// 步骤 1:version
this.message = '步骤 1/3: 调用 version()...';
const v: string = testNapi.version();
hilog.info(DOMAIN, 'testTag', 'step1 version = %{public}s', v);
// 步骤 2:parse
this.message = '步骤 2/3: 调用 parse()...';
const jsonStr: string = '{"name":"HarmonyOS","version":"5.0","list":[1,2,3],"enabled":true,"note":null}';
const obj = testNapi.parse(jsonStr);
const name = (obj as Record<string, Object>)['name'];
hilog.info(DOMAIN, 'testTag', 'step2 parse ok, name = %{public}s', String(name));
// 步骤 3:stringify
this.message = '步骤 3/3: 调用 stringify()...';
const backToJson: string = testNapi.stringify(obj);
hilog.info(DOMAIN, 'testTag', 'step3 stringify = %{public}s', backToJson);
this.message = 'cJSON v' + v + ' name=' + name + '\n\n' + backToJson;
} catch (e) {
const errMsg = ((e as Error).message !== undefined) ? (e as Error).message : JSON.stringify(e);
this.message = '调用失败于: ' + this.message + '\n\n' + errMsg;
hilog.error(DOMAIN, 'testTag', 'cjson demo failed, errMsg = %{public}s', errMsg);
}
}
build() {
Row() {
Column() {
Text(this.message)
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.onClick(() => {
// 点击可重新执行演示
this.runCjsonDemo();
})
}
.width('100%')
}
.height('100%')
}
}
六、构建与运行验证
6.1 IDE 构建
DevEco 菜单:Build → Build Hap(s)/App(s),或直接点工具栏绿色 ▶ 运行到真机。底部 Build 面板出现 `BUILD SUCCESSFUL` 即构建成功。
6.2 真机运行验证
连接 OpenHarmony 真机后运行,手机屏幕显示:
cJSON v1.7.19 name=HarmonyOS
{"name":"HarmonyOS","version":"5.0","list":[1,2,3],"enabled":true,"note":null}


看到这行即代表:version() 调用成功、parse()解析正确、stringify() 序列化往返一致——三方库适配打通。
七、自动化测试(ohosTest)
7.1 编写测试用例
entry/src/ohosTest/ets/test/Ability.test.ets 中追加 cjsonNapiTest 套件:
import { hilog } from '@kit.PerformanceAnalysisKit';
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
import testNapi from 'libentry.so';
const DOMAIN = 0x0000;
export default function abilityTest() {
describe('ActsAbilityTest', () => {
// 模板自带用例(省略骨架,保留即可)
it('assertContain', 0, () => {
let a = 'abc';
let b = 'b';
expect(a).assertContain(b);
expect(a).assertEqual(a);
})
})
describe('cjsonNapiTest', () => {
// cJSON 三方库 NAPI 高层封装自动化验证(真机 ohosTest)
it('version_returns_1_7_19', 0, () => {
const v: string = testNapi.version();
expect(v).assertEqual('1.7.19');
})
it('parse_returns_object_fields', 0, () => {
const obj = testNapi.parse('{"name":"HarmonyOS","version":"5.0","list":[1,2,3],"enabled":true,"note":null}');
expect(obj).assertInstanceOf('Object');
const rec = obj as Record<string, Object>;
expect(rec['name']).assertEqual('HarmonyOS');
expect(rec['version']).assertEqual('5.0');
expect(rec['enabled']).assertEqual(true);
expect(rec['note']).assertNull();
const list = rec['list'] as Array<number>;
expect(list.length).assertEqual(3);
expect(list[0]).assertEqual(1);
expect(list[2]).assertEqual(3);
})
it('parse_invalid_json_returns_null', 0, () => {
const obj = testNapi.parse('{invalid json');
expect(obj).assertNull();
})
it('stringify_roundtrip_preserves_data', 0, () => {
const src: string = '{"name":"HarmonyOS","enabled":true,"list":[1,2,3]}';
const obj = testNapi.parse(src);
const out: string = testNapi.stringify(obj);
const obj2 = testNapi.parse(out) as Record<string, Object>;
expect(obj2['name']).assertEqual('HarmonyOS');
expect(obj2['enabled']).assertEqual(true);
const list = obj2['list'] as Array<number>;
expect(list.length).assertEqual(3);
expect(list[1]).assertEqual(2);
})
it('stringify_primitive_values', 0, () => {
expect(testNapi.stringify('hello')).assertEqual('"hello"');
expect(testNapi.stringify(3.14)).assertEqual('3.14');
expect(testNapi.stringify(true)).assertEqual('true');
expect(testNapi.stringify(null)).assertEqual('null');
})
})
}
8.3 mock 文件(ohosTest 默认走 mock,需与真实行为对齐)
entry/src/mock/Libentry.mock.ets完整内容:
const NativeMock: Record<string, Object | null> = {
'version': (): string => '1.7.19',
'parse': (json: string): Object | null => {
try {
return JSON.parse(json);
} catch (e) {
// 与真实 cJSON 行为对齐:解析失败返回 null 而非抛异常
return null;
}
},
'stringify': (value: Object): string => {
return JSON.stringify(value);
},
};
export default NativeMock;
> DevEco 模板自带 @ohos/hamock + mock-config.json5,ohosTest 构建默认启用 mock。mock 的 parse必须和真实 cJSON 一样"失败返回 null"(原生 JSON.parse`是抛异常),否则用例失败。这是测试验证接口契约,真机页面演示验证的是真实 NAPI。
7.3 运行测试
打开 Ability.test.ets → 点击 describe('cjsonNapiTest', ...)`行号旁的绿色 ▶ → Run 'Ability.test'。底部 Run 窗口出现测试树,6 个用例全绿即通过。

八、编译过程踩坑记录
问题 1:架构错误,设备运行提示找不到库
原因:误用 WSL 本机 gcc 编译,生成 x86 架构库,ARM 鸿蒙设备不识别。 解决:必须使用 OpenHarmony HB 编译框架做交叉编译。
问题 2:拷贝软链接文件到 Windows 出现异常
原因:编译生成.so.1、.so.1.7.19软链接,Windows 不支持 Linux 软链接。 解决:仅复制实体文件libcjson.so,全部软链接丢弃。
问题 3:工程编译提示找不到 cJSON.h 头文件
原因:cJSON 头文件自带cjson二级目录,拷贝时目录层级被破坏。 解决:保留原始层级,include/cjson/cJSON.h不要直接把 h 文件直接丢到根目录。
九、DevEco 侧踩坑记录(真实问题)
问题 1:真机报 cannot read property version of undefined,页面调用失败
原因:libcjson.so 的 SONAME 是 libcjson.so.1(Linux 发行版版本化命名)。链接时编译器把 SONAME 写进 libentry.so 的 DT_NEEDED,真机加载 libentry.so 时按 libcjson.so.1 查找依赖,而包里只有 libcjson.so,导致整个原生模块 dlopen 失败。用 llvm-readobj --dynamic-table libentry.so 可以看到 NEEDED libcjson.so.1,而 HAP 里只有 libcjson.so。
解决:把 libcjson.so 复制一份命名为 libcjson.so.1 放入 entry/libs/arm64-v8a/,重新构建。
问题 2:构建报 ld.lld: error: unable to find library -lcjson
原因:CMake 相对路径层级算错。${NATIVERENDER_ROOT_PATH} 是 entry/src/main/cpp,到 entry/libs 需要往上三级,写成两级 ../../libs 会解析成不存在的 entry/src/libs 目录。
解决:改为 ${NATIVERENDER_ROOT_PATH}/../../../libs/${OHOS_ARCH},即 set(CJSON_LIB_PATH ${NATIVERENDER_ROOT_PATH}/../../../libs/${OHOS_ARCH})。
问题 3:ArkTS 严格模式编译报错
原因:ArkTS 严格模式不允许箭头函数省略返回类型(arkts-no-implicit-return-types),也不允许使用 any/unknown(arkts-no-any-unknown)。mock 里 'version': () => '1.7.19' 没写返回类型;返回 null 时写 null as Object 被拒,改 null as unknown as Object 也被拒。
解决:箭头函数显式写返回类型,如 'version': (): string => '1.7.19';需要返回 null 时把函数返回类型声明为 Object | null,直接 return null,不使用 any/unknown 断言。
问题 4:ohosTest 测试里 parse_invalid_json_returns_null 用例失败
原因:ohosTest 构建默认启用 mock(模板自带 @ohos/hamock 和 mock-config.json5),测试实际调用的是 mock 的 JSON.parse,而 JSON.parse 对非法 JSON 抛异常,真实 cJSON 的 parse 返回 null,行为不一致。且 mock 文件只在 ohosTest 构建时编译,default 构建不报错,容易被忽略。
解决:mock 的 parse 加 try/catch,解析失败返回 null,与真实 cJSON 契约对齐。
至此,cJSON 三方库完成了「WSL2 交叉编译 → DevEco NAPI 封装 → 真机运行 → 自动化测试」的完整适配闭环。核心经验:so 的 SONAME 必须与包内文件名一致、CMake 相对路径要算准层级、ArkTS 严格模式要显式写类型。后续如需扩展(格式化输出、错误定位、其他 ABI),在这个骨架上加函数即可。
更多推荐


所有评论(0)