第 5 篇:搭起仓颉工程,翻出第一段能跑的代码
本系列第 5 篇。前面理清了活动规则、装好了环境、读懂了库、规划了路线。这一篇终于动手:用
cjpm按活动规范把plist4cj工程搭起来,并翻译第一个模块,跑通cjpm build和cjpm test。
一、先明确这次要交付什么
结合群公告,我锁定了目标:
- 路线:纯仓颉(后续要兼容 LTS 1.0.5 + STS 1.1.3 双版本,并发布中心仓);
- 目录结构要符合公告的新版规范;
- 功能要与上游全量一致,接口名不能改。
所以第一步不是急着翻代码,而是把符合规范的工程骨架搭对。
二、用 cjpm 生成工程骨架
仓颉的工程用官方包管理器 cjpm 管理。因为我们做的是「库」(不是可执行程序),用 --type=static 生成静态库工程:
cd plist4cj
cjpm init --type=static --name plist4cj它会生成一个 cjpm.toml 和一个空的 src/ 目录。
💡
--name就是模块名,同时也是根包名。我们的库叫plist4cj,所以根包就是plist4cj,子包会是plist4cj.xxx。
三、按活动规范调整目录
活动要求的结构是这样(实现和测试都在 src/ 里面):
plist4cj/
├── cjpm.toml # 工程配置(必须)
├── README.md # 说明文档
├── README.OpenSource # 上游库信息,JSON 格式(必须)
└── src/
├── plist4cj.cj # 根包文件(见下方“坑 1”)
├── plist/ # 功能实现,package plist4cj.plist(必须)
├── example/ # 使用示例,package plist4cj.example(建议)
└── test/ # 测试用例,package plist4cj.test(必须)仓颉的「包 = 目录」规则
仓颉有一条硬规则:包名要反映源文件相对于 src 的路径,且文件夹名必须和包名一致。
src/plist/*.cj→ 包声明写package plist4cj.plistsrc/test/*.cj→ 包声明写package plist4cj.test- 直接放在
src/下的文件 → 就是根包plist4cj
四、cjpm.toml 长这样
[package]
cjc-version = "1.0.5" # 最低版本要求,写 1.0.5 是为了兼容 LTS
name = "plist4cj"
description = "A Cangjie port of dd-plist ..."
version = "1.0.0"
output-type = "static" # 静态库为什么
cjc-version写1.0.5而不是我本机的1.1.3?因为这个字段是最低版本要求。纯仓颉路线最终要能在 LTS 1.0.5 上编译,写 1.0.5 表示「1.0.5 及以上都能用」,我本机的 1.1.3 自然也满足。
五、翻译第一个模块:异常类
按第 4 篇的规划,从没有依赖、最简单的模块开始。我选了异常体系里的两个类:
LocationInformation(抽象基类,46 行)PropertyListFormatException(异常类,88 行,依赖上面那个)
翻译 LocationInformation:抽象类怎么写
Java 原版是个抽象类,有一个抽象方法 getDescription(),还重写了 toString()。
仓颉里:
- 抽象类用
abstract class,抽象方法不写函数体; - Java 的
toString()在仓颉里没有「万能父类方法」可重写,而是要实现ToString接口(<: ToString),这样才能被println、字符串插值识别。
public abstract class LocationInformation <: ToString {
public func getDescription(): String // 抽象方法,无函数体
public func toString(): String {
this.getDescription()
}
}翻译 PropertyListFormatException:异常与 cause
仓颉的自定义异常继承内置的 Exception,用 super(message) 传消息,用 getClassName() 返回类名。
这里遇到第一个**「上游有、仓颉没有直接对应」**的情况:Java 的异常可以带一个 cause(原始异常),但仓颉内置 Exception 的构造不支持传 cause。
按群公告「功能要全量一致」的要求,不能因为不方便就把这个能力砍掉。我的处理是:用一个可选字段 Option<Exception> 把 cause 存下来,并提供 getCause() 读取,保证对外功能不缺失。
public open class PropertyListFormatException <: Exception {
private var locationInfo: Option<LocationInformation> = None
private var causeException: Option<Exception> = None
public init(message: String) { super(message) }
public init(message: String, cause: Exception) {
super(message); this.causeException = Some(cause)
}
// ... 其余构造函数一一对应 Java 版
public func getCause(): Option<Exception> { this.causeException }
public open override func getClassName(): String {
"PropertyListFormatException"
}
}这就是「一致性」的实操:接口名、参数、功能一个都不能少,仓颉没有的能力就想办法用等价方式补上(这里是可选字段;更复杂的场景后面会用 FFI)。
六、踩到的两个坑
坑 1:src 根目录必须有 .cj 文件,否则子目录不被编译
我一开始只在 src/plist/ 放了代码,src/ 根目录是空的。cjpm build 直接警告:
Warning: there is no '.cj' file in directory 'src',
and its subdirectories will not be scanned as source code意思是:根包没有任何源文件时,cjpm 不会去扫描子目录。解决办法是在 src/ 根下放一个根包文件 src/plist4cj.cj:
package plist4cj
public let VERSION: String = "1.0.0"加上之后,cjpm check 就能正确识别编译顺序了:
The valid serial compilation order is:
plist4cj -> plist4cj.plist坑 2:暂时用不到的函数会告警
setLocationInformation 这个包内函数目前没人调用,编译器会报 unused function 警告。这个不用管——它对应上游的包私有 setter,等后面解析器实现了就会用到,保留它才能保证功能一致。
七、验证:build + test 全绿
写好测试(放在 src/test/,用仓颉自带的单元测试框架 std.unittest)后,跑:
cjpm build # 编译静态库
cjpm test # 运行单元测试结果:
[ PASSED ] CASE: testMessageAndClassName
[ PASSED ] CASE: testLocationInformation
[ PASSED ] CASE: testNoLocationByDefault
Summary: TOTAL: 3, PASSED: 3, FAILED: 0
cjpm test success第一段能跑、能测的仓颉代码就诞生了 🎉。
八、这一步的收获
- 先搭对骨架,再写代码:目录结构、
cjpm.toml、包名规则,是后面所有工作的地基。 - 仓颉「包 = 目录」:包名必须和目录路径对应,文件夹名必须和包名一致。
- 一致性优先:上游有的能力(比如异常 cause),仓颉没直接对应也要用等价方式补上,不能砍。
- 小步验证:翻一个最简单的模块就立刻
build+test跑通,把工程链路先打通,比闷头翻一大堆再一起编译要稳得多。
下一篇(待更新):翻译核心数据类型 NSObject 骨架与 NSString/NSNumber,让库开始真正「装得下 plist 里的数据」。