环境清单:

宿主机: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),在这个骨架上加函数即可。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐