简介

  • 目标:把一个纯 C 的 JSON 解析器集成到仓颉(Cangjie)项目,完成“解析 → 递归转换为仓颉 JsonValue → 示例运行”的端到端流程。
  • 成果:已实现完整适配与演示,能够解析对象与数组并输出结构信息。本文将完整拆解实现细节、踩坑与修复,以及可扩展方向。

你将收获

  • 🧩 如何声明 C FFI:foreign/@C、CString、CPointer、varargs(…)等
  • 🔐 如何安全地调用 C 函数:unsafe、指针判空、释放内存
  • 🔁 如何把 C 解析树递归转换为仓颉的 JsonValue/JsonObject/JsonArray
  • 🧱 如何编译和链接第三方 C 静态库到仓颉包
  • 🛠️ 常见错误与修复手册(语法、类型、链接等)

两种适配方式概览 ⚖️

方式 A:全量重构(纯仓颉实现)🧱

  • 直接用仓颉实现词法/语法解析器与数据结构,完全不依赖 C 代码
  • 优点:跨平台一致、可充分利用仓颉语言特性(泛型、枚举、Option、匹配、单测框架等),维护与分发简单
  • 成本:实现工作量较大,需要设计 tokenizer、parser、AST 与错误恢复;适合长期维护/扩展的场景

方式 B:FFI 适配(调用 C 静态库)🔌

  • 保留既有 C 解析器,通过仓颉 FFI 声明与桥接使用
  • 优点:上线快,复用成熟 C 代码;对计算密集场景可沿用 C 的性能
  • 成本:需要正确声明 foreign/@C、处理指针与内存释放;构建与链接需稳定配置

👉 本文主要演示方式 B(FFI 适配),同时给出方式 A 的实现思路与对比结论,便于评估选型。

项目结构概览 🗂

  • 根目录包含 C 解析器源码:json_parser.c、json_parser.h
  • 仓颉包 json_parser4cj:
    • src/ffi_json_parser.cj:C FFI 声明(类型与函数)
    • src/ffi_bridge.cj:桥接与递归转换、顶层解析函数 parseWithC
    • src/json_core.cj:仓颉侧 JsonValue/JsonObject/JsonArray 类型与基础方法
    • src/main.cj:示例演示,调用 parseWithC 并打印结构
    • cjpm.toml:配置链接静态库

方式 A:全量重构(纯仓颉实现)🧱

  • 基础类型与数据结构:
    • JsonValue(Null/Boolean/Num/Str/Obj/Arr)
    • JsonObject(键值对、有序或哈希结构)、JsonArray(顺序表)
    • 提供 find/insert/remove/get/size 等 API,与当前 json_core.cj 的接口保持一致
  • 解析器(建议分层):
    • tokenizer:将输入 String 切分为 token(字符串、数字、true/false/null、符号如 { } [ ] , :)
    • parser:递归下降或基于栈实现,按 JSON 语法生成 JsonValue 树
    • 错误处理:返回 Option.None 或携带错误信息的 Result(视项目需求)
  • 单元测试:
    • 基于 @Test/@TestCase/@Assert,覆盖字符串、数字、布尔、空、对象、数组、非法输入等用例
    • 可复用当前测试结构,替换顶层 parseWithC 为纯仓颉 parse(text)

方式 B:FFI 适配(调用 C 静态库)🔌

第 1 步:声明 C FFI 函数与类型 🔌

  • 在 src/ffi_json_parser.cj 中使用 @C struct 和 foreign 声明:
    • C 类型:json_value_t/json_object_t/json_array_t
    • 解析与销毁:
      • foreign func json_value_parse(text: CString): CPointer<json_value_t>
      • foreign func json_value_destroy(val: CPointer<json_value_t>): Unit
    • 类型与访问:
      • json_value_typejson_value_stringjson_value_numberjson_value_objectjson_value_array
    • 对象迭代:
      • json_object_next_namejson_object_next_valuejson_object_value_namejson_object_size
    • 数组迭代:
      • json_array_next_value、json_array_size
  • 注意常量类型:把 JSON_VALUE_* 枚举常量在仓颉侧定义为 Int32,确保与 json_value_type 返回值比较无类型冲突。
    • 例如:let JSON_VALUE_OBJECT: Int32 = 3

第 2 步:递归转换桥接(C → 仓颉)🔁

  • 在 src/ffi_bridge.cj 中实现两件事:
    • CString → String:
      • 采用插值法生成 String:func cstringToString(cs: CString): String { "${cs}" }
      • 说明:目前插值方式可靠易用;若未来标准库提供 String.fromCString 或 fromUtf8,建议替换为官方 API。
    • 递归转换函数 convertCValueToCJ:
      • 根据 json_value_type 判断类型并构造仓颉 JsonValue
      • Object 的遍历:使用 json_object_next_value 按值迭代,并用 json_object_value_name 从值获得对应键名
      • Array 的遍历:使用 json_array_next_value 按元素迭代
      • 关键点:
        • 每个 foreign 调用必须放在 unsafe 块中
        • CPointer 的判空(结束条件)通过 vptr.isNull() 完成

第 3 步:顶层解析函数 parseWithC 🧩

  • 在 src/ffi_bridge.cj 中提供:
    • public func parseWithC(text: String): Option
    • 实现要点:
      • StringCString:unsafe { LibC.mallocCString(text) }
      • 调用 C 解析:unsafe { json_value_parse(ctext) }
      • 释放输入缓冲:unsafe { LibC.free(ctext) }
      • 判空返回 None
      • 转换为仓颉 JsonValue 后,释放 C 解析树:unsafe { json_value_destroy(root) }

第 4 步:编译并链接 C 静态库 🧱

  • 在仓库根目录生成 C 静态库(推荐以下任一方式,产出 libjsonparser.a 放在仓库根目录):

    1. 命令行一次性生成:

      cc -std=c99 -Wall -O2 -c json_parser.c -o json_parser.o && ar rcs libjsonparser.a json_parser.o
      
    2. 使用 Makefile 目标:

      make clean && make libjsonparser
      
    3. 使用 CMake(已设置 OUTPUT_NAME=jsonparser):

      cmake -S . -B build && cmake --build build --target json-parser && cp build/libjsonparser.a .
      
  • 在 json_parser4cj/cjpm.toml 中配置链接选项(指向仓库根目录,避免被 cjpm clean 清理):

    link-option = "-L /Users/…/json-parser -ljsonparser"
    
  • 构建仓颉包:

    cd json_parser4cj && cjpm build
    

第 5 步:示例运行与验证 ✅

  • 在 src/main.cj 中调用 parseWithC 并打印成员与数组类型:

    • 构建与运行:

      cjpm build && ./target/release/bin/json_parser4cj
      
  • 你将看到类似输出:

    - parseWithC succeeded, root kind=Obj
    - root object size: 3
    - member 'user' => kind=Obj
    - member 'nums' => kind=Arr
    - member 'ok' => kind=Boolean
    - nums size: 3
    - nums[0] kind=Num
    - nums[1] kind=Num
    - nums[2] kind=Num
    

对比结论 📊

  • 若你追求“快速集成 + 复用既有 C 代码”,选择方式 B(FFI 适配)
  • 若你追求“纯仓颉生态 + 维护简化 + 进一步扩展”,选择方式 A(全量重构)

运行示例与测试模块 📦

  • 示例运行(main.cj):

    cd json_parser4cj
    cjpm build && ./target/release/bin/json_parser4cj
    

    (或 main,可根据包名/输出名变化)

  • 单元测试(unittest):

    • 测试文件:src/json_parser_unittest_test.cj

    • 测试框架:@Test / @TestCase / @Assert

    • Option 助手:src/test_helpers.cj 中提供 optJVIsSome/unwrapJVOrNulloptJOIsSome/unwrapJOOrEmptyoptJAIsSome/unwrapJAOrEmpt,避免 match 语法歧义与类型冲突

    • 执行:

      • cd json_parser4cj
      cjpm clean && cjpm test -V
      
    • 覆盖用例:

      • parse_string / parse_number / parse_boolean_true / parse_boolean_false / parse_null
      • object_user_fields / object_ok_boolean
      • array_nums_elements
      • invalid_json_returns_none
      • file_parse_root_and_fields / file_parse_nums_array

方式 A 最小骨架✨

  • 纯仓颉解析器骨架:src/pure_cj_parser.cj(当前支持 true/false/null)
  • 对应单测:src/pure_cj_parser_unittest_test.cj(4 个用例,最小可跑)
  • 示例输出(main.cj 已加入 parseCJ 演示):
    • parseCJ('true') => kind=Boolean, value=true
    • parseCJ('false') => kind=Boolean, value=false
    • parseCJ('null') => kind=Null
  • 迭代建议:按“字符串 → 数字 → 数组 → 对象”的顺序逐步补齐 parseCJ 能力,并为每步添加对应用例。

安全与内存管理要点 🔒

  • unsafe 必须:
    • 调用 foreign 函数(包括 LibC.mallocCString/LibC.free)
    • 指针读写与迭代
  • 指针判空:
    • 通过 CPointer
      .isNull() 判断,避免非法解引用
    • 资源释放:
      • 输入 CString:parse 后立即释放
    • C 解析树:递归转换完成后释放 root(json_value_destroy)

💡 提示:确保任何 CString 都在不再使用后释放;建议统一封装到 parse/convert 的生命周期中,避免遗漏。

踩坑与修复 🛠

  • 链接错误(undefined symbol …):
    • 原因:未生成/未找到 libjsonparser.a,或链接路径错误,或静态库被 cjpm clean 清理
    • 修复:按上文第 4 步生成并将 libjsonparser.a 放在仓库根目录;在 cjpm.toml 使用绝对路径 link-option 指向根目录;避免将库放在 target/release 目录(可能被清理)
  • 类型比较错误(Int32 vs Int64):
    • 原因:json_value_type 返回 Int32,而你用的是默认 Int64 常量
    • 修复:把 JSON_VALUE_* 在仓颉侧定义为 Int32
  • match 语法错误(expected ‘=>’):
    • 原因:match 的 case 右侧必须是表达式;不能直接写代码块
    • 修复:把多语句逻辑提取到函数,用函数调用作为表达式返回 Unit(例如 dumpObject(o))
    • 另一个实践:在测试中优先使用 Option 助手函数(见 test_helpers.cj),避免写复杂嵌套 match 造成解析歧义
  • LibC 调用必须 unsafe:
    • 原因:标准库的 C 接口属于 native/unsafe
    • 修复:把 LibC.mallocCString/LibC.free 放到 unsafe 块中

可扩展方向 🌱

  • 绑定 C 的构建/编辑接口(varargs):
    • json_value_create、json_value_copy
    • json_object_append/insert_after/insert_before/remove
    • json_array_append/insert_after/insert_before/remove
    • 注意:varargs 的传参需要严格匹配 C API(例如 JSON_VALUE_NUMBER 传 double,STRING 传 CString),并确保 CString 生命周期管理正确。
  • 增加从文件读取并解析的辅助函数:
    • 在仓颉侧读取文件内容为 String → parseWithC → 打印结构
  • 更稳健的 CString → String:
    • 若未来提供 String.fromCString 或 fromUtf8 API,建议替换当前插值实现,获得官方行为保证与潜在性能优化。
  • 测试用例:
    • 为 parseWithC 增加非法 JSON、深度嵌套、空输入等边界用例,验证返回 None 与资源释放路径。

完整实践清单 ✅

  • FFI 声明齐全(读取相关)并将 JSON_VALUE_* 设置为 Int32
  • 桥接递归转换 convertCValueToCJ 正确处理 7 种 JSON 类型
  • parseWithC 顶层函数处理分配与释放
  • 编译并链接 C 静态库到仓颉包
  • 示例运行成功输出对象与数组信息

附:两种方式的选择建议 📝

  • 面向短期交付或需要复用成熟 C 代码:选 FFI 适配(本文路径)
  • 面向长期维护、希望纯仓颉生态与可扩展:选全量重构(按“方式 A”规划实现)

Quick Start ⚡️

  1. 构建原生静态库(推荐 Makefile):
make clean && make libjsonparser
  1. 检查库位置:确保在仓库根目录存在 libjsonparser.a。

  2. 构建并运行仓颉包示例:

cd json_parser4cj
cjpm build && ./target/release/bin/json_parser4cj
  1. 运行单元测试(含 FFI + 纯仓颉骨架用例):
cjpm clean && cjpm test -V

❗ 若提示“library not found for -ljsonparser”,请确认 cjpm.toml 的 link-option 指向仓库根目录,并且 libjsonparser.a 已成功生成。

可视化演示 🖼️

cjpm run dev
在这里插入图片描述
cjpm test
在这里插入图片描述

对比要点一览 📊

  • 交付速度:
    • 🔌 FFI 适配 → 上线快,直接复用 C 解析器
    • 🧱 全量重构 → 需要设计 tokenizer/parser,初期投入较大
  • 维护成本:
    • 🔌 FFI 适配 → 需维护链接与内存管理、C API 变更时需同步
    • 🧱 全量重构 → 纯仓颉代码,维护与分发更简单
  • 可扩展性:
    • 🔌 FFI 适配 → 可继续绑定更多 C 接口(varargs、编辑操作)
    • 🧱 全量重构 → 更易引入仓颉语言特性(泛型、枚举、Option 等)
  • 性能:
    • 🔌 FFI 适配 → 复用 C 实现,性能稳定
    • 🧱 全量重构 → 视实现质量而定,可逐步优化
  • 风险:
    • 🔌 FFI 适配 → 需严格管理 CString 生命周期与指针判空
    • 🧱 全量重构 → 解析器实现复杂度高,错误恢复与边界用例需要充分测试

Troubleshooting Checklist 🧯

  • ⛓️ 链接失败:
    • 检查 libjsonparser.a 是否位于仓库根目录
    • cjpm.toml 的 link-option 是否使用绝对路径指向根目录
    • 是否误将库文件放在 target/release(可能被 cjpm clean 清理)
  • 🧠 类型与常量:
    • JSON_VALUE_* 常量是否为 Int32
    • 与 json_value_type 的返回值比较是否一致
  • 🔒 unsafe 块:
    • foreign / LibC 调用是否位于 unsafe
    • 是否正确释放 mallocCString 与解析树(json_value_destroy)
  • 🧰 测试写法:
    • 优先使用 Option 助手(optJVIsSome / unwrapJVOrNull 等),避免嵌套 match 造成语法歧义

FAQ ❓

  • Q:库文件能放在 json_parser4cj/target 目录吗?
    • A:不建议,cjpm clean 可能会清理该目录,导致链接失败。推荐放在仓库根目录,并在 cjpm.toml 使用绝对路径。
  • Q:CString 如何转换为 String?
    • A:当前采用插值 “${cs}”。若未来提供官方 API(如 String.fromCString),建议替换为官方实现。
  • Q:能否只用纯仓颉实现?
    • A:可以,已有 parseCJ 的最小骨架(true/false/null)。可按路线图逐步实现完整 JSON 支持。
  • Q:如何从文件解析?
    • A:使用 parseFileWithC(path);内部采用 LibC 的 fopen/fread/fclose 并进行内存管理。

许可与致谢 📜

  • 许可证:请参考仓库中的 LICENSE 文件
  • 致谢:感谢 C 解析器原作者与仓颉社区的文档与示例

参考文件与路径 🔗

  • C 解析器:json_parser.c / json_parser.h
  • 静态库:libjsonparser.a(仓库根目录)
  • 仓颉包:json_parser4cj/
    • FFI 声明:src/ffi_json_parser.cj
    • 桥接转换:src/ffi_bridge.cj
    • 核心类型:src/json_core.cj
    • 示例入口:src/main.cj
    • 测试助手:src/test_helpers.cj
    • FFI 测试:src/json_parser_unittest_test.cj
    • 纯仓颉最小骨架:src/pure_cj_parser.cj
    • 纯仓颉测试:src/pure_cj_parser_unittest_test.cj
    • 测试数据:testdata/example.json

总结 🎯

本实战示例完整演示了“用仓颉适配第三方 C 库”的关键路径:正确声明 FFI、实现安全调用与递归转换、完成构建与链接、并通过示例验证。

Logo

新一代开源开发者平台 GitCode,通过集成代码托管服务、代码仓库以及可信赖的开源组件库,让开发者可以在云端进行代码托管和开发。旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐