Skip to content

第 5 篇:搭起仓颉工程,翻出第一段能跑的代码

本系列第 5 篇。前面理清了活动规则、装好了环境、读懂了库、规划了路线。这一篇终于动手:用 cjpm 按活动规范把 plist4cj 工程搭起来,并翻译第一个模块,跑通 cjpm buildcjpm test

一、先明确这次要交付什么

结合群公告,我锁定了目标:

  • 路线:纯仓颉(后续要兼容 LTS 1.0.5 + STS 1.1.3 双版本,并发布中心仓);
  • 目录结构要符合公告的新版规范;
  • 功能要与上游全量一致,接口名不能改。

所以第一步不是急着翻代码,而是把符合规范的工程骨架搭对

二、用 cjpm 生成工程骨架

仓颉的工程用官方包管理器 cjpm 管理。因为我们做的是「库」(不是可执行程序),用 --type=static 生成静态库工程:

bash
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.plist
  • src/test/*.cj → 包声明写 package plist4cj.test
  • 直接放在 src/ 下的文件 → 就是根包 plist4cj

四、cjpm.toml 长这样

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-version1.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、字符串插值识别。
cangjie
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() 读取,保证对外功能不缺失。

cangjie
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

cangjie
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)后,跑:

bash
cjpm build   # 编译静态库
cjpm test    # 运行单元测试

结果:

[ PASSED ] CASE: testMessageAndClassName
[ PASSED ] CASE: testLocationInformation
[ PASSED ] CASE: testNoLocationByDefault
Summary: TOTAL: 3, PASSED: 3, FAILED: 0
cjpm test success

第一段能跑、能测的仓颉代码就诞生了 🎉。

八、这一步的收获

  1. 先搭对骨架,再写代码:目录结构、cjpm.toml、包名规则,是后面所有工作的地基。
  2. 仓颉「包 = 目录」:包名必须和目录路径对应,文件夹名必须和包名一致。
  3. 一致性优先:上游有的能力(比如异常 cause),仓颉没直接对应也要用等价方式补上,不能砍。
  4. 小步验证:翻一个最简单的模块就立刻 build + test 跑通,把工程链路先打通,比闷头翻一大堆再一起编译要稳得多。

下一篇(待更新):翻译核心数据类型 NSObject 骨架与 NSString/NSNumber,让库开始真正「装得下 plist 里的数据」。