仓颉内置编译标记全解析:源码定位、条件编译、@FastNative、@Frozen 与声明管理标记实战指南
仓颉内置编译标记全解析:源码定位、条件编译、@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 函数满足以下两点要求:
- 函数的整体执行时间不宜太长:例如不允许函数内部存在很大的循环;不允许函数内部产生阻塞行为,如调用
sleep、wait等函数; - 函数内部不能调用仓颉方法。
从实现原理看,这两条约束与 @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() {}
执行流程解读:
@Component(...)触发宏调用,输入的程序片段被包装为Tokens;- 宏内通过
parseDecl(input)将Tokens解析为声明节点(关于parseDecl及语法节点体系,参见语法节点); varDecl.hasAttr("State")判断该变量声明是否带有State属性;- 命中后通过
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应填写真实的废弃版本,便于调用方评估升级窗口。
八、使用建议与注意事项小结
- 源码位置标记(
@sourcePackage()/@sourceFile()/@sourceLine())是纯编译期机制,展开为字面量,无运行时开销,适合日志与诊断场景; - 条件编译(
@When[...])用于跨平台与功能开关,注意其不支持嵌套、且宏展开产物中不允许出现@When[...];自定义条件变量需通过--cfg或cfg.toml声明; @FastNative只能修饰foreign函数,被调用的 C 函数必须执行时间短、无阻塞、不回调仓颉方法,否则应使用普通foreign声明;@Frozen是对未来版本演进的 ABI 承诺,新增标记兼容、删除标记 ABI 不兼容,一旦标记便不能再修改签名与函数体;多平台common/specific声明当前不支持该标记;@Attribute与宏机制配合使用,通过std.ast的hasAttr/getAttrs在编译期查询声明属性;@Deprecated提供message/since/strict三个参数,通过-Woff deprecated可抑制对应告警,strict: true可将废弃升级为编译错误。
内置编译标记覆盖了"获取源码信息—控制编译范围—优化互操作—承诺稳定性—管理元数据与生命周期"的完整编译期能力链路,是仓颉开发者进行工程化与库设计的常用工具。结合条件编译、cjc 编译选项、宏机制与库演化规范等仓库文档,可以进一步掌握这些标记在真实项目中的组合用法。