本文记录 msgpackr 2.0.4 适配仓颉(Cangjie)的完整过程。适配目标是得到一个可以被仓颉项目直接依赖的跨端 MessagePack 三方库,并在本地终端中通过真实二进制编解码验证标量、容器、扩展值、Float32、连续对象流、迭代器和结构化克隆能力。
项目地址: AtomGit/nutpi_cj/msgpackr
上游仓库: kriszyp/msgpackr
开发工具: 华为云码道
一、背景
1.1 为什么适配 msgpackr
msgpackr 是 JavaScript 生态中的高性能 MessagePack 编解码库,提供 pack、unpack、Packr、Decoder、扩展值、连续对象和结构化克隆等能力。它的 JavaScript 版本可以接收动态对象,按照值的运行时类型选择 MessagePack marker,并在 Node.js、浏览器和 Bun 等环境中提供不同的入口。
仓颉强调静态类型、显式集合和清晰的运行时边界,因此适配的难点不在逐行翻译 JavaScript 语法,而在于重新确定值模型、二进制缓冲区和解码结果的类型。当前实现使用 MsgPackValue 显式表达 MessagePack 值,使用 Array<UInt8> 表达线上的字节,让编解码结果可以直接被仓颉调用方检查和复用。
本项目保留 pack、unpack、encode、decode、Packr、Encoder、Unpackr 和 Decoder 等熟悉名称,并把实际 wire format 字节打印到终端。这样熟悉 msgpackr 的开发者可以沿用原库的工作方式,同时在编译期看到仓颉值的形状。
1.2 跨端三方库的适配边界
当前项目提供的是仓颉跨端库,不绑定某一个 UI 框架或操作系统。适配边界可以概括为:
| 边界 | 仓颉实现 |
|---|---|
| 包管理 | cjpm.toml、cjpm.lock、cjpm build 和静态库输出 |
| 值模型 | MsgPackValue、ArrayList<MsgPackValue> 和键值对映射 |
| 编码格式 | MessagePack nil、bool、整数、Float32/Float64、str、bin、array、map 和 extension |
| 解码入口 | 单值 unpack、连续对象 unpackMultiple 和分块辅助接口 |
| 可复用对象 | Packr、Encoder、Unpackr 和 Decoder |
| 辅助能力 | structuredClone、clone、display、hex 和 valuesEqual |
| 交付形态 | 仓颉静态库 libmsgpackr_cangjie.a 和接口文件 msgpackr_cangjie.cjo |
| 验证方式 | test/main.cj 编译测试和 example/example.cj 终端示例 |
这里没有 HAP 构建、ArkTS 桥接和真机系统能力验收。只要目标平台提供兼容版本的仓颉 SDK,就可以按同一套源码和 cjpm 配置构建并消费这个库。原始 JavaScript、TypeScript、Node.js stream 和 npm 构建文件继续保留在项目中,不参与仓颉包的源码扫描。
1.3 适配后的能力
当前仓颉实现围绕 msgpackr 的可移植核心提供以下能力:
- 值构造:
nilValue、boolean、integer、unsigned、number、text、binary、array、object、map和extensionValue; - 单值编解码:
pack、encode、unpack和decode; - 连续与分块处理:
unpackMultiple、encodeIter和decodeIter; - 可复用对象:
Packr、Encoder、Unpackr和Decoder; - MessagePack 类型:固定长度和扩展长度的整数、字符串、二进制、数组、映射,以及固定长度和可变长度扩展值;
- 浮点选项:默认 Float64 和
EncodeOptions(useFloat32: true); - 结果检查:深拷贝、十六进制字节展示、递归值比较和人类可读输出。
这些入口覆盖本次仓颉静态值模型中真正实现的编解码语义。JavaScript 专属的动态对象、Node.js 流、记录代码生成和可选原生加速没有被伪装成已经支持的仓颉 API,具体边界在第七节说明。
二、实现路线图
text
第 1 阶段:盘点 API ── 对照 msgpackr 的编解码生命周期,确定仓颉值模型和字节边界
第 2 阶段:建立 cjpm 工程 ── 固定包名、版本、源码目录和静态库输出
第 3 阶段:类型映射 ── 用 enum、struct、Array 和 ArrayList 表达 MessagePack 值
第 4 阶段:实现编解码 ── 完成 marker、长度、端序、扩展值和流式对象解析
第 5 阶段:测试与核对 ── 编译测试入口,执行真实字节往返和边界值检查
第 6 阶段:终端演示 ── 构建 example,直接输出真实编码字节、解码值和效果图
每个阶段都有可检查的产物:工程阶段得到 cjpm.toml,实现阶段得到 .cjpm-target/msgpackr_cangjie,验证阶段得到 20 项真实演示结果,最终示例则输出来自静态库调用的 MessagePack 十六进制字节和解码结构。
三、逐步实现过程
第 1 阶段:盘点原库 API 和仓颉工程边界
1.1 识别动态 API
msgpackr 的 JavaScript 入口可以接收任意 JavaScript 值,并通过运行时类型判断决定编码方式。Map、Set、Date、BigInt、函数和自定义 class 等值还会受到 Node.js 或浏览器运行时的影响。仓颉没有与 JavaScript any 完全等价的动态对象,因此本适配先筛选可以由静态枚举稳定表达的 MessagePack 类型,再为扩展值保留原始 type 和 payload。
MapValue 使用 ArrayList<(MsgPackValue, MsgPackValue)>,而不是只使用字符串键的 HashMap。这样整数键、布尔键和字符串键都可以在一次编码和解码后保持顺序与值形状。常见的字符串键对象则通过 object 辅助函数构造。
1.2 固定仓颉项目结构
以下目录树按照当前 msgpackr 项目的仓颉适配文件整理,只展示仓颉相关文件;原始 msgpackr JavaScript、TypeScript、测试和构建目录保持原位置:
text
msgpackr/
├── cjpm.toml
├── cjpm.lock
├── README.Cangjie.md
├── README.Cangjie_CN.md
├── src/
│ └── msgpackr.cj
├── test/
│ └── main.cj
└── example/
├── cjpm.toml
├── cjpm.lock
└── example.cj
所有仓颉实现集中在 src/msgpackr.cj,测试入口位于 test/main.cj,示例工程位于 example/。示例使用 src-dir = ".",但 target-dir 固定为 ../.cjpm-target/msgpackr_example,构建输出不在源码扫描范围内,因此 cjpm run 不会把生成目录当作仓颉源码目录。
第 2 阶段:创建 cjpm 工程
根目录 cjpm.toml 使用静态库输出类型,并将源码目录和构建目录固定下来:
toml
[package]
name = "msgpackr_cangjie"
version = "2.0.4"
cjc-version = "1.1.3"
description = "Native Cangjie MessagePack codec inspired by msgpackr"
src-dir = "src"
target-dir = "../.cjpm-target/msgpackr_cangjie"
output-type = "static"
[dependencies]
示例工程使用本地路径依赖,并把构建目录放到 example/ 外部:
toml
[package]
name = "msgpackr_example"
version = "2.0.4"
cjc-version = "1.1.3"
description = "Executable demonstration for msgpackr_cangjie"
src-dir = "."
target-dir = "../.cjpm-target/msgpackr_example"
output-type = "executable"
[dependencies]
msgpackr_cangjie = { path = ".." }
根包版本沿用当前 msgpackr 版本,包名使用 msgpackr_cangjie。构建成功后,主要产物是:
text
../.cjpm-target/msgpackr_cangjie/release/msgpackr_cangjie/libmsgpackr_cangjie.a
../.cjpm-target/msgpackr_cangjie/release/msgpackr_cangjie/msgpackr_cangjie.cjo
其中 .a 用于链接,.cjo 提供编译器需要的接口信息。示例的所有输出都写入 ../.cjpm-target/msgpackr_example,验证结束后清理这些目录,不提交构建产物。
第 3 阶段:把 JavaScript 动态值映射为仓颉类型
3.1 用 MsgPackValue 表达 MessagePack 值
JavaScript 可以把任意值交给 pack,仓颉版本需要在调用前构造显式值。核心枚举覆盖 MessagePack 的常见类型:
cangjie
public enum MsgPackValue {
| NilValue
| BooleanValue(Bool)
| IntegerValue(Int64)
| UnsignedValue(UInt64)
| FloatValue(Float64)
| StringValue(String)
| BinaryValue(Array<UInt8>)
| ArrayValue(ArrayList<MsgPackValue>)
| MapValue(ArrayList<(MsgPackValue, MsgPackValue)>)
| ExtensionValue(Int8, Array<UInt8>)
}
数组使用 ArrayList<MsgPackValue>,映射使用键值对列表,二进制使用 Array<UInt8>。binary、array、object 和 map 辅助函数会复制传入容器,调用方可以清楚地知道哪些数据会被编码。
3.2 用显式字节数组表达编码结果
仓颉版本的 pack 返回 Array<UInt8>,而不是 Node.js Buffer。这让结果可以直接交给仓颉网络、文件或自定义传输层,也方便 hex 在终端中展示真实 wire format:
cangjie
let input = object([
("name", text("msgpackr")),
("payload", binary([0, 16, 32, 255]))
])
let bytes = pack(input)
println(hex(bytes))
let output = unpack(bytes)
println(display(output))
编码器按照 MessagePack 规范选择 fixint、uint8/16/32/64、str8/16/32、bin8/16/32、array16/32、map16/32 和 extension marker,并使用大端顺序写入长度和数值字节。
3.3 用扩展值和 Float32 选项保留语义
自定义扩展值使用显式的 type 和 payload,不经过字符串化:
cangjie
let extension = extensionValue(7, [9, 8, 7, 6])
let bytes = pack(extension)
let decoded = unpack(bytes)
默认浮点编码为 Float64;需要与 Float32 生产者互操作时,可以使用:
cangjie
let bytes = pack(number(1.5), options: EncodeOptions(useFloat32: true))
// ca3fc00000
解码器会保留扩展值 type 和数据,并根据 marker 读取 Float32 或 Float64,避免把二进制能力退化为仅可读的日志文本。
第 4 阶段:分层实现源码
4.1 核心文件 src/msgpackr.cj
核心文件按编解码生命周期组织。Writer 负责根据值类型写入 marker、长度和 payload;Reader 维护当前位置并从字节数组读取一个完整值;顶层函数负责单值、连续值和分块入口。
整数编码按照 MessagePack 的最短表示选择固定正负整数、无符号整数或有符号 8/16/32/64 位整数。字符串先按 UTF-8 字节长度选择 str marker,数组和映射先写数量再递归写入每个元素,扩展值则按照 1、2、4、8、16 字节固定长度或可变长度 marker 编码。
4.2 连续对象和迭代器辅助接口
unpack 会拒绝一个值之后的尾部字节,unpackMultiple 则从同一个 Reader 中连续读取对象,适合记录边界明确的 MessagePack 流:
cangjie
import std.collection.*
let first = pack(text("one"))
let second = pack(integer(2))
let stream = ArrayList<UInt8>()
stream.add(all: first)
stream.add(all: second)
let values = unpackMultiple(stream.toArray())
仓颉数组拼接在不同 SDK 版本中可以使用 ArrayList<UInt8> 完成,示例中的 encodeIter 返回每个值的独立字节块,decodeIter 再把这些块合并为连续对象读取结果。它提供了与原库迭代器入口相同的使用方向,同时保持仓颉参数类型明确。
4.3 可复用对象和结构化克隆
Packr 和 Encoder 持有 EncodeOptions,Unpackr 和 Decoder 提供可复用的解码入口。structuredClone 递归复制数组、映射、二进制和扩展数据,valuesEqual 用于检查克隆结果与输入的值语义一致:
cangjie
let packr = Packr(options: EncodeOptions(structuredClone: true))
let decoder = Decoder()
let result = decoder.decode(packr.pack(text("reusable encoder")))
这个对象层保留了 msgpackr 的调用习惯,同时不需要在仓颉侧模拟 JavaScript 的可变 prototype 或 Node.js Buffer 类型。
第 5 阶段:建立自动化测试
test/main.cj 是一个直接链接仓颉静态库的可执行测试程序,覆盖以下行为:
| 测试组 | 示例 |
|---|---|
| 基础值 | nil、真假布尔、有符号/无符号整数、Float64 |
| 文本与二进制 | UTF-8 中文、二进制 payload 和十六进制字节 |
| 容器 | 嵌套数组、字符串键映射和非字符串键映射 |
| 扩展值 | 固定长度 extension 的 type 和 payload 往返 |
| 浮点选项 | Float32 marker ca 和解码值 |
| 连续对象 | unpackMultiple 读取两个拼接对象 |
| 迭代器 | encodeIter / decodeIter 三个对象 |
| 复制与对象 | structuredClone、Encoder 和 Decoder |
| 边界整数 | 负数、32 位边界和 Int64 最小值 |
每一项都会打印编码字节和解码结果,随后通过 valuesEqual 校验;失败会抛出异常并以非零状态退出。测试末尾输出:
text
All 20 MessagePack demonstrations produced and verified real results.
示例工程负责可视化展示,测试程序负责回归断言。两者使用同一个 msgpackr_cangjie 静态库,因此效果图中的字节和解码文本都来自真实库调用,而不是预先写死的通过标记。
第 6 阶段:核对 msgpackr 适配 API
源码公开入口按值构造、编解码、连续流、可复用对象、克隆和展示辅助函数分组。文档列出的入口都可以在 src/msgpackr.cj 的 public 声明中找到,且 example 实际调用了每一组核心入口:
text
Value construction ── MsgPackValue and value helpers
Single value codec ── pack / encode / unpack / decode
Multiple value codec ── unpackMultiple / encodeIter / decodeIter
Reusable objects ── Packr / Encoder / Unpackr / Decoder
Clone and inspection ── structuredClone / display / hex / valuesEqual
这项核对说明仓颉文档和源码的适配边界一致,不代表已经覆盖 JavaScript msgpackr 的所有动态值和 Node.js 专属 API。无法静态表达的能力在第七节明确列出。
四、完整验证步骤
以下命令均以仓库根目录 msgpackr/ 为当前目录。先确认本机已经安装仓颉 SDK:
bash
echo "$CANGJIE_HOME"
cjc --version
cjpm --version
然后构建静态库:
bash
cjpm build
编译并运行测试入口:
bash
cjc test/main.cj \
--import-path ../.cjpm-target/msgpackr_cangjie/release/msgpackr_cangjie \
-L ../.cjpm-target/msgpackr_cangjie/release/msgpackr_cangjie \
-l msgpackr_cangjie --set-runtime-rpath \
-o ../.cjpm-target/msgpackr_tests
../.cjpm-target/msgpackr_tests
再构建并运行独立的终端示例工程:
bash
cd example
cjpm check
cjpm run
示例会调用本地路径依赖中的仓颉静态库,并输出真实结果:
text
Scalars
MessagePack bytes: 86a36e696cc0...
decoded value: {"nil": nil, "bool": true, ...}
Binary and nested containers
decoded value: {"payload": bin[4]:001020ff, ...}
Extension value
decoded value: ext(type=7, data=09080706)
Float32 option
bytes: ca3fc00000
Concatenated stream
decoded values: ["one", 2, false]
Demo complete: all codec features above produced visible, verified results.
示例使用 target-dir = "../.cjpm-target/msgpackr_example",因此 src-dir = "." 时不会把构建输出目录纳入源码扫描,也不会出现"there is no '.cj' file ... target"警告。验证结束后清理 .cjpm-target、target 等构建目录,并确认它们未加入 Git。
五、运行效果
下面的效果图来自 example/example.cj 的实际仓颉终端运行,包含静态库构建后的 MessagePack 编码字节、解码结构、扩展值、Float32、连续对象、迭代器、结构化克隆以及 Packr/Decoder 的结果。

这张图可以作为文章或项目说明中的验收证据:终端同时展示了不同 MessagePack 类型的 wire bytes 和解码结果,并在最后输出 Demo complete: all codec features above produced visible, verified results.。这些内容由仓颉示例在库调用完成后生成,不是单独拼接的模拟文本。
六、关键决策说明
6.1 为什么使用显式值模型和字节数组
仓颉适配的价值在于让调用方在编译期知道传入值和返回值的数据形状。若所有输入都退化成无类型字符串,虽然表面上接近 JavaScript,却会失去二进制、无符号整数、扩展 type 和嵌套结构的检查。因此项目使用 MsgPackValue、Array<UInt8> 和键值对列表,让编码结果和解码结果都保持可检查。
6.2 为什么保留 msgpackr 类型和方法名称
保留 pack、unpack、encode、decode、Packr、Decoder 等名称可以降低迁移成本。熟悉 msgpackr 的开发者只需要适应仓颉的显式值构造、参数标签和 Array<UInt8>,不必重新记忆一套完全不同的编解码 API。对于暂时无法表达的 JavaScript 动态能力,适配通过静态值模型和 README 边界说明行为,而不是伪造兼容结果。
6.3 为什么把测试和示例分开
测试程序负责逐项比较输入和输出,适合自动化执行;示例工程负责展示每个真实场景的字节和解码值,适合在终端中观察和截图。两者都使用同一个 msgpackr_cangjie 仓颉静态库,因此效果图来自实际库调用,测试又不会因为展示文本而失去失败定位能力。
七、兼容边界和遗留问题
JavaScript msgpackr 与仓颉静态值模型以及 Node.js 运行时不同,以下限制需要在接入时提前了解:
- 当前仓颉 API 要求调用方先构造
MsgPackValue;不会把任意 JavaScript 对象、函数、class 实例或undefined自动映射为仓颉值。 - 当前实现支持 MessagePack 基本类型和原始 extension payload,但没有实现 JavaScript
Date、BigInt、Map/Set自动识别、record 共享结构和自定义 class 编解码。 encodeIter和decodeIter是仓颉数组分块辅助接口,不是 Node.jsReadable、异步迭代器或背压流;网络流和文件流由宿主项目自行接入。EncodeOptions提供 Float32 和结构化克隆选项;JavaScript 版本中与运行时对象、原生加速、共享结构和 Buffer 池相关的内部优化没有在仓颉侧伪造。- 映射使用键值对列表保留任意键类型和插入顺序,不提供 JavaScript 对象原型、属性枚举和
mapsAsObjects的动态切换。 - 输入值应当是无环的;
structuredClone和递归编解码针对值树工作,不把循环引用转换成隐式引用协议。 isNativeAccelerationEnabled固定为仓颉实现的能力标记,不代表已经加载 msgpackr-extract 或其他 Node.js 原生扩展。
这些不是构建错误。只要 cjpm build 成功、独立测试输出 20 项结果、示例输出真实字节和最终完成标记,就说明当前仓颉静态值范围已经完成本地编译和行为验收。未实现的 JavaScript 专属能力没有被计入示例覆盖数。
八、接入方式
在另一个仓颉项目中,可以将本仓库作为本地依赖使用。最直接的本地验证方式是先在本仓库根目录生成 .cjpm-target/msgpackr_cangjie,再让调用方通过 cjpm 解析本地路径依赖:
toml
[dependencies]
msgpackr_cangjie = { path = "/path/to/msgpackr" }
调用代码需要显式构造值:
cangjie
import msgpackr_cangjie.*
main(): Int64 {
let input = object([
("name", text("Cangjie")),
("values", array([integer(1), integer(2)]))
])
let bytes = pack(input)
let result = unpack(bytes)
println(display(result))
return 0
}
如果使用 Packr,可以为 Float32 或结构化克隆设置 EncodeOptions;如果需要处理连续对象,则把独立编码块拼接后交给 unpackMultiple。跨端项目可以复用相同的值构造、编解码和错误处理思路,具体的网络传输、文件写入和异步调度由宿主平台负责。
九、总结
本次适配完成了 msgpackr MessagePack 核心编解码模型到仓颉静态类型模型的迁移:值使用 MsgPackValue 表达,字节使用 Array<UInt8> 表达,编码和解码使用真实 MessagePack marker,扩展值保留 type 与 payload,连续对象与 Float32 通过独立入口验证,Packr、Decoder 和结构化克隆保持可复用调用方式。
验证链路包括 cjpm build、测试入口编译、20 项真实字节往返、独立示例工程运行和效果图核对。最终效果图来自真实的仓颉可执行程序,能够直观看到标量、二进制、容器、非字符串键、扩展值、浮点选项、连续对象、迭代器和克隆结果。对于无法一比一映射 JavaScript 动态对象、Node.js 流和原生扩展的部分,项目通过静态签名和文档边界保持行为可解释,方便后续继续扩展。
适配过程 FAQ
以下问题来自仓颉工程创建、
cjpm依赖、与 HarmonyOS 设备联调的实际流程。
Q1:cjpm 依赖解析失败或拉取超时?
- 检查
cjpm.toml中的依赖坐标与版本号; - 配置可用的仓颉包仓库镜像或代理;
- 删除
target、cjpm.lock后重新执行cjpm update/cjpm build。
Q2:编译报找不到仓颉 SDK 或 cjc 版本不匹配?
- 设置
CANGJIE_HOME指向仓颉 SDK 根目录; - 确认
cjc --version、cjpm --version与工程要求的版本一致; - 清理构建缓存后重新生成。
Q3:与 HarmonyOS 设备联调时安装失败?
- 用
hdc list targets确认设备在线; - 确认 HAP 已签名,
bundleName与签名 profile 一致; - 设备需加入调试 profile 的
deviceIds。
Q4:动态库或 FFI 符号找不到?
- 检查导出符号名称、ABI 与打包路径;
- 确认
.so已随 HAP 一起打包到libs/<abi>/; - 用
llvm-readelf -d检查依赖库是否齐全。
Q5:测试用例在设备上失败,但本机通过?
- 对比设备与主机的架构、SDK 版本和环境变量;
- 用
hdc hilog抓取运行日志; - 优先复现最小用例,确认是否为平台差异或时序问题。