仓颉内置编译标记全解析:源码定位、条件编译、@FastNative、@Frozen 与声明管理标记实战指南

原创2026-10-13 19:13:081,362 阅读
文章标签:文档教程

仓颉内置编译标记全解析:源码定位、条件编译、@FastNative、@Frozen 与声明管理标记实战指南

仓颉(Cangjie)语言提供了一组预定义的内置编译标记(built-in compilation tags),开发者可以借助它们直接影响编译器的行为:在编译期获取源码位置、按条件选择性编译代码、优化与 C 语言的互操作调用、为库 API 的长期演进作出稳定性承诺,以及为声明附加自定义属性或标注废弃状态。本文以 builtin_compilation_flags.md 为骨架,逐项讲解 @sourcePackage()/@sourceFile()/@sourceLine()、@When、@FastNative、@Frozen、@Attribute 与 @Deprecated 六类内置编译标记的语法、使用场景与底层语义,并补充 cjc 编译选项、std.ast 宏机制与库演化规范中的实现证据。读完本文,你将能够在仓颉项目中熟练使用这些标记完成源码信息采集、跨平台条件编译、C 互操作性能优化、ABI 稳定性管理和 API 生命周期管理。

一、内置编译标记概述

内置编译标记是仓颉编译器预定义的一类特殊标记,它们不依赖外部宏包,直接由编译器在编译阶段识别和处理。与普通函数调用不同,这些标记不会在运行时求值,而是在编译期被展开或参与编译决策:

标记 编译期行为 主要用途
@sourcePackage() 展开为 String 字面量 获取所在源码的包名
@sourceFile() 展开为 String 字面量 获取所在源码的文件名
@sourceLine() 展开为 Int64 字面量 获取所在源码的代码行号
@When[...] 根据编译条件裁决代码是否参与编译 条件编译(平台适配、功能选择、调试支持、性能优化)
@FastNative 优化 foreign C 函数调用 提升 C 互操作性能
@Frozen 固化函数/属性的实现 库版本演进的 ABI 稳定性承诺
@Attribute[...] 为声明附加属性值 供宏在编译期查询声明元信息
@Deprecated[...] 生成废弃告警/错误 API 生命周期管理

其中 @When 的完整机制在条件编译章节中有专门论述,本文在第三节给出核心要点与补充说明。

二、源码位置标记:@sourcePackage() / @sourceFile() / @sourceLine()

仓颉内置了三个用于在编译时获取源码位置的编译标记,它们展开后分别是不同类型的字面量:

  • @sourcePackage():展开为 String 类型的字面量,内容为该标记所在源码的包名;
  • @sourceFile():展开为 String 类型的字面量,内容为该标记所在源码的文件名;
  • @sourceLine():展开为 Int64 类型的字面量,内容为该标记所在源码的代码行号。

这三个标记可以在任意表达式内部使用,只要符合类型检查规则即可。由于它们在编译期即被展开为字面量,因此常用于日志、断言、默认参数等需要携带源码信息的场景,且不会引入任何运行时开销。

2.1 基本使用示例

func test1() {
    let s: String = @sourceFile() // s 的值为当前源文件名
}

func test2(n!: Int64 = @sourceLine()) { /* 位于第 5 行 */
    // n 的默认值为 test2 定义处的源文件行号
    println(n) // 打印 5
}

要点解读:

  • @sourceFile() 出现在 test1 的函数体内,s 将获得该 .cj 文件自身的文件名;
  • @sourceLine() 作为 test2 的默认参数出现,编译期展开为该函数定义所在的行号(示例中为 5),调用 test2() 而不传参时 println(n) 会打印 5;
  • 三个标记均可以在表达式任意位置使用,例如拼接进字符串、作为函数实参或默认值。

2.2 典型应用场景

从这些标记的语义可以推断,它们适合以下场景:

  • 调试与诊断信息:在日志输出中自动带上当前文件与行号,免去手工维护字符串常量;
  • 断言与错误报告:在抛出异常前记录 @sourceFile()、@sourceLine(),定位问题更直接;
  • 默认参数的"位置敏感"设计:如上例所示,让参数的默认值自动跟随定义位置,保持代码同步。

三、条件编译标记:@When[...]

条件编译通过内置编译标记 @When 实现,其参数为编译条件。它允许开发者在程序代码中根据特定条件选择性地编译不同代码段,作用体现在四个方面:

  • 平台适应:根据当前编译环境选择性地编译代码,实现跨平台兼容;
  • 功能选择:根据不同需求灵活配置编译的代码(功能开关);
  • 调试支持:在调试模式下编译调试信息、日志相关代码,发布版本中将其排除,兼顾性能与安全;
  • 性能优化:根据预定义条件选择性地编译代码,减少发布产物中的无效路径。

@When 仅允许修饰 import 节点和除 package 声明外的其他所有声明类节点。例如:

@When[os == "Linux"]
class MyClass {}

main(): Int64 {
    var a = MyClass()
    return 0
}

在 Linux 系统上可正常编译执行;在非 Linux 系统上则会遇到找不到 MyClass 类定义的编译错误。

3.1 内置编译条件变量

仓颉提供的内置条件变量包括 os、arch、env、backend、cjc_version、debug 和 test,其取值与操作符能力如下:

条件变量 支持的取值示例 支持的操作符 语义
os Windows、Linux、macOS、iOS ==、!= 目标平台操作系统
arch x86_64、aarch64、arm ==、!= 目标平台处理器架构
env ohos、gnu、simulator、android 及空字符串 ==、!= 目标平台 ABI 等额外环境信息
backend cjnative ==、!= 目标平台后端类型
cjc_version 形如 0.18.8 的版本号 ==、!=、>、<、>=、<= 当前编译器版本
debug 布尔 !(逻辑非) 是否开启 -g 调试编译选项
test 布尔 !(逻辑非) 是否开启 --test 单元测试选项

几点细节值得注意:

  • cjc_version 的格式为 xx.xx.xx,每个 xx 支持 1~2 位数字,比较时按**补位(补齐 2 位)**规则进行,例如 0.18.8 < 0.18.11、0.18.8 == 0.18.08;
  • debug 对应 cjc 的 -g 编译选项,@When[debug] 与 @When[!debug] 可分别在调试/发布构建中编译不同代码;
  • test 对应 --test 选项,可用于区分测试代码与普通代码;
  • 交叉编译场景下,目标平台由 os、arch、env 三者共同确定,完整映射关系见条件编译章节末尾的"目标平台和条件编译映射表"。

3.2 自定义编译条件变量

除了内置变量,仓颉允许开发者自定义编译条件变量:

  • 自定义变量必须是合法的标识符,且不允许与内置条件变量同名,其值为字符串字面量,仅支持 == 和 != 两种操作符;
  • 自定义条件需要在编译时通过 --cfg 编译选项或在配置文件 cfg.toml 中定义。

通过 --cfg 以键值对形式直接配置:

$ cjc --cfg "feature = lion, platform = dsp" source.cj

也允许多次使用 --cfg,但不允许多次定义同一个条件变量(重复定义会报错):

$ cjc --cfg "feature = lion" --cfg "platform = dsp" source.cj

若 --cfg 的值不包含 =,则该值被当作配置文件 cfg.toml 的搜索路径:

$ cjc --cfg "./cfg" source.cj

此时编译器会自动获取 ./cfg/cfg.toml 中的自定义编译条件,cfg.toml 采用键值对方式配置,支持全行注释与行末注释:

feature = "lion"
platform = "dsp"
# 全行注释
feature = "meta" # 行末注释

3.3 使用限制与组合条件

  • 不支持编译条件嵌套,以下写法均不允许:

    @When[os == "Windows"]
    @When[os == "Linux"]    // Error, illegal nested when conditional compilation
    import std.ast.*
    @When[os == "Windows"]
    @When[os == "Linux"]    // Error, illegal nested when conditional compilation
    func A(){}
    
  • @When[...] 在导入前处理,由宏展开生成的代码中若含有 @When[...] 会编译报错:

    @Derive[ToString]
    @When[os == "Linux"] // Error, unexpected when conditional compilation directive
    class A {}
    
  • 仓颉允许自由组合多个条件编译选项,支持逻辑运算符(&&、||、!)与括号明确优先级,例如 @When[(test || feature == "lion") && !debug]。

关于条件编译的完整机制、内置变量的逐项示例与交叉编译映射表,请阅读条件编译章节。

四、@FastNative:优化 C 互操作调用

为了提升与 C 语言互操作的性能,仓颉提供 @FastNative 标记用于优化对 C 函数的调用。值得注意的是,@FastNative 只能用于 foreign 声明的函数。

4.1 完整示例:调用 C 动态库

首先编写并编译以下 C 语言程序,得到动态库文件 libcProg.so:

#include <stdio.h>

char* foo()
{
    static char str[] = "this is an example";
    return str;
}

然后在仓颉文件 main.cj 中声明并调用 foo 与 printf:

@FastNative
foreign func foo(): CPointer<Int32>

@FastNative
foreign func printf(fmt: CPointer<Int32>, ...): Int32

main(): Int32 {
    unsafe {
        let str = foo()
        printf(str)
    }
}

编译命令如下(-L . 指定库文件搜索目录为当前目录,-lcProg 指定链接名为 cProg 的库,链接器会查找 libcProg.so):

cjc -L . -lcProg ./main.cj

执行生成的 main 可执行文件,输出:

this is an example

4.2 使用 @FastNative 的约束

开发者在使用 @FastNative 修饰 foreign 函数时,应确保对应的 C 函数满足以下两点要求:

  1. 函数的整体执行时间不宜太长:例如不允许函数内部存在很大的循环;不允许函数内部产生阻塞行为,如调用 sleep、wait 等函数;
  2. 函数内部不能调用仓颉方法。

从实现原理看,这两条约束与 @FastNative 的优化目标一致:它通过减少仓颉运行时介入的开销来提升调用性能,因此要求被调用的 C 函数保持"短小、纯净",避免在快速调用路径中出现阻塞或反向依赖仓颉运行时的情况。

4.3 相关背景与延伸

@FastNative 作用于 foreign 声明的函数,而 foreign 声明本身有一套独立的规则,详见 C 与仓颉互操作:

  • foreign 修饰函数声明,代表该函数为外部函数,只能有声明不能有实现;
  • foreign 函数的参数和返回类型必须符合 C 与仓颉数据类型之间的映射关系;
  • 由于 C 侧函数很可能产生不安全操作,调用 foreign 函数需要被 unsafe 块包裹;
  • foreign 函数不支持命名参数和参数默认值,但允许通过 ... 声明变长参数(上例中的 printf 即为此形态)。

关于 cjc 链接相关的编译选项(如 --library <value>/-l、--library-path <value>/-L、动态库运行时的 LD_LIBRARY_PATH 设置等),可查阅 cjc 编译选项附录。

五、@Frozen:函数与属性的版本演进承诺

@Frozen 标记可用于修饰函数和属性。如果确定某个函数、属性在将来的版本更新中不会修改其内部实现,就可以使用 @Frozen 对其进行标记——该标记代表开发者对该函数/属性在未来版本演进的一种承诺。

被 @Frozen 修饰的函数和属性,在后续的升级版本中签名和函数体都不能发生任何变化。这意味着,前后两个代码版本在相同编译器、相同编译选项的情况下,该函数或属性的生成产物完全一致。

5.1 适用范围

@Frozen 注解仅可用于修饰:

  • 除局部(嵌套)函数外的所有种类的函数定义,包括全局函数、成员函数、构造函数、主构造函数和析构函数;
  • 所有种类的成员属性定义。

5.2 使用示例

@Frozen
public func test(): Unit {}

public class testClass {
    @Frozen
    public func testFunc(): Unit {}

    @Frozen
    public prop testProp: Unit {
        get() {}
    }
}

示例中 test、testFunc 与 testProp 均被 @Frozen 修饰,意味着它们在后续版本中保持实现不变。

5.3 从库演化规范看 @Frozen 的 ABI 语义

仓库中的库演化规范 · 兼容性规则对 @Frozen 的兼容性影响给出了系统性的说明,可作为理解其底层语义的权威证据:

  • 新增 @Frozen 编译标记:API 与 ABI 均兼容;
  • 删除 @Frozen 编译标记:API 兼容,但 ABI 不兼容。规范中解释,这是为了保证版本 A 与 B 兼容、B 与 C 兼容、则 A 与 C 兼容的传递性——若先删除 @Frozen 再修改函数体,会产生实质的 ABI 破坏;
  • 修改标记了 @Frozen 的函数的参数默认值或函数体:API 兼容,但 ABI 不兼容;而修改未标记 @Frozen 的对应部分则两者均兼容;
  • 对于属性,修改未标记 @Frozen 的属性 getter/setter 为兼容,修改标记 @Frozen 的属性 getter/setter 则为 ABI 不兼容。

此外,被 @Frozen 标记的函数还会影响"模块外可见"的判定:某全局变量即使没有 public 修饰,只要被其他模块外可见且标记了 @Frozen 的函数依赖,也会参与下游模块的编译过程(参见规范第 18 行附近及对应代码示例)。

注意:在多平台(common/specific)场景下,仓库文档公共平台与多平台开发明确指出:当前不支持 @Frozen 修饰 common/specific 声明,行为存在异常,应避免在 multi-platform 声明上使用。

因此,@Frozen 的最佳实践是:只对长期稳定、不打算再修改的实现使用,并在发布前确认其与库的版本演进策略一致,否则后续任何内部调整都会造成 ABI 断裂。

六、@Attribute:为声明附加编译期属性

仓颉语言内部提供 @Attribute 标记,开发者通过内置的 @Attribute 来对某个声明设置属性值,从而达到标记声明的目的。属性值可以是 identifier 或者 string。

6.1 基本语法

@Attribute[State]
var cnt = 0 // identifier 类型的属性 State

@Attribute["Binding"]
var bcnt = 0 // string 类型的属性 "Binding"

上例为变量 cnt 添加了 identifier 类型的属性 State,为变量 bcnt 添加了 string 类型的属性 "Binding"。

6.2 与宏机制配合:std.ast 的属性查询接口

@Attribute 的价值主要体现在与宏配合的场景:宏在编译期处理声明时,可以通过标准库 std.ast 包提供的方法查询节点属性:

  • getAttrs():获取节点的属性,返回一组 Tokens;
  • hasAttr(attrs: String):判断当前节点是否具有某个属性。

6.3 完整示例:宏中读取属性

宏定义(位于宏包 define 中):

macro package define

import std.ast.*

public macro Component(input: Tokens): Tokens {
    var varDecl = parseDecl(input)
    if (varDecl.hasAttr("State")) { // 如果该节点被标记了属性且值为 "State",则返回 true,否则返回 false
        var attrs = varDecl.getAttrs() // 返回一组 Tokens
        println(attrs[0].value)
    }
    return input
}

宏调用:

import define.Component

@Component(
    @Attribute[State] var cnt = 0
)

main() {}

执行流程解读:

  1. @Component(...) 触发宏调用,输入的程序片段被包装为 Tokens;
  2. 宏内通过 parseDecl(input) 将 Tokens 解析为声明节点(关于 parseDecl 及语法节点体系,参见语法节点);
  3. varDecl.hasAttr("State") 判断该变量声明是否带有 State 属性;
  4. 命中后通过 getAttrs() 取得属性 Tokens 并读取其值。

这样,宏就能在编译期感知源码中的声明元信息,从而实现按属性驱动的代码生成、代码检查等能力。关于宏包的组织方式(macro package)、Tokens 与 quote 表达式,可参考宏的简介。

七、@Deprecated:API 废弃标记

@Deprecated 表示此 API 已废弃:虽然暂时可用,但未来将被移除或更改,建议其他开发者不要调用此 API。

7.1 基本使用与告警输出

@Deprecated["用boo代替", since: "1.3.4"]
func foo() {}

main() {
    foo()
}

编译器编译时提供如下告警信息:

warning: function 'foo' is deprecated since 1.3.4. 用boo代替
 ==> file.cj:5:5:
  |
5 |     foo()
  |     ^^^ deprecated
  |
  # note: this warning can be suppressed by setting the compiler option `-Woff deprecated`

1 warning generated, 1 warning printed.

告警信息中的 # note 行给出了关闭该告警的方法:通过编译选项 -Woff deprecated 即可抑制本组告警。-Woff(即 --warn-off)的完整语义参见 cjc 编译选项附录:其参数可以为 all 或某个警告组别,且与 --warn-on 组合使用时顺序敏感。

7.2 可应用的声明范围

@Deprecated 自定义宏可以应用于以下声明:

  • 类、接口、结构体、枚举、枚举构造器;
  • 顶级(全局)函数或变量;
  • 静态或非静态的成员函数、成员变量、属性、属性设置器;
  • 运算符函数;
  • 扩展的成员函数、静态函数、属性或属性设置器;
  • foreign 函数或声明在 foreign 块内的函数;
  • 构造函数和主构造函数;
  • 抽象的函数和属性;
  • 类型别名(包括关联类型);
  • 函数具有默认参数的命名参数;
  • const 变量和函数;
  • 宏定义;
  • 注解类。

7.3 @Deprecated 参数

参数 类型 默认值 说明
message String 必填 描述声明为何废弃、如何迁移等
since! ?String 无 废弃版本
strict! Bool false 为 false 时,在被该标记修饰的 API 的调用处触发警告;为 true 时,触发编译错误

strict: true 的示例(将被 @Deprecated 修饰的宏标记为严格废弃,任何调用都会导致编译错误):

@Deprecated["Use Macro2", since: "1990", strict: true]
public macro Macro(input: Tokens): Tokens {
    return input
}

7.4 使用建议

  • 废弃 API 时必须提供 message,说明替代方案或迁移路径,帮助调用方平滑升级;
  • 常规废弃先用 strict: false 以警告形式提醒;确需强制切换时再升级为 strict: true 触发编译错误;
  • since 应填写真实的废弃版本,便于调用方评估升级窗口。

八、使用建议与注意事项小结

  1. 源码位置标记(@sourcePackage()/@sourceFile()/@sourceLine())是纯编译期机制,展开为字面量,无运行时开销,适合日志与诊断场景;
  2. 条件编译(@When[...])用于跨平台与功能开关,注意其不支持嵌套、且宏展开产物中不允许出现 @When[...];自定义条件变量需通过 --cfg 或 cfg.toml 声明;
  3. @FastNative 只能修饰 foreign 函数,被调用的 C 函数必须执行时间短、无阻塞、不回调仓颉方法,否则应使用普通 foreign 声明;
  4. @Frozen 是对未来版本演进的 ABI 承诺,新增标记兼容、删除标记 ABI 不兼容,一旦标记便不能再修改签名与函数体;多平台 common/specific 声明当前不支持该标记;
  5. @Attribute 与宏机制配合使用,通过 std.ast 的 hasAttr/getAttrs 在编译期查询声明属性;
  6. @Deprecated 提供 message/since/strict 三个参数,通过 -Woff deprecated 可抑制对应告警,strict: true 可将废弃升级为编译错误。

内置编译标记覆盖了"获取源码信息—控制编译范围—优化互操作—承诺稳定性—管理元数据与生命周期"的完整编译期能力链路,是仓颉开发者进行工程化与库设计的常用工具。结合条件编译、cjc 编译选项、宏机制与库演化规范等仓库文档,可以进一步掌握这些标记在真实项目中的组合用法。

登录后查看全文
cangjie_docs