在 Zig 中用 comptime 按条件禁用代码
原文由 Mitchell Hashimoto 于 发布,订阅该博客
本文是Zig comptime 用例系列文章之一。
Zig 有一个非常强大的特性叫 comptime。comptime 让你可以在编译期运行 Zig 代码。这不是什么特殊的宏语言,也不是对 AST 的操作,它就是会在编译期执行的标准 Zig 代码。唯一的真正限制是 comptime 代码不能产生副作用(不能进行系统调用、IO 等)。
我承认,刚开始接触 Zig 时,我觉得这不过是个噱头。我当时想:“到底有多少事情是真的需要在 comptime 做的呢?”但在用 Zig 开发一个颇具规模的项目两年之后,comptime 已经无处不在,并且成了我最喜欢的 Zig 特性。
comptime 本身是一个很大的话题。所以本文不打算面面俱到,而是想向你展示一个特别实用的模式:用 comptime 按条件禁用代码。
为什么要按条件禁用代码?
按条件禁用代码是软件开发中一种常见的模式。以下是几个你可能想要按条件禁用代码的常见原因:
平台相关代码:同一个函数在 macOS 和 Linux 上可能需要不同的实现。
调试代码:有些代码只在调试时有用,你可能希望在生产构建中将其禁用。
构建配置:你可能希望根据构建时的配置来省略或修改某个功能。
在 Python 或 JavaScript 这类动态语言中,通常可以在运行时用一个简单的 if 语句来选择合适的代码路径。由于动态语言只在运行时才求值代码,这样就能避开那些可能无法运行的分支。
但在编译型语言中,却不能用 if 语句来按条件禁用代码,因为编译器必须编译并链接所有在运行时可能走到的代码路径。
除了让代码能够通过编译,在编译期就省略掉不需要的代码还有一个好处:可以让二进制文件更小,并避免可能带来性能开销的运行时条件判断。
非 Zig 的做法
我们来看看其他编译型语言是如何处理这个问题的。如果你对这部分不感兴趣,可以直接跳到下一节,看看如何用 Zig 的 comptime 来解决。
基于 C 的语言
在基于 C 的语言中,经常会看到用预处理器来按条件禁用代码。例如:
#define FLAG
void my_function() {
#ifdef FLAG
// This code will only be included if FLAG is defined.
#else
// This code will only be included otherwise.
#endif
}这本质上是在编译期对代码做模板化处理。预处理器会先运行,在文本层面转换代码,然后编译器再进行编译。第一个主要的缺点是,预处理器是一门独立的语言,有自己的一套语法和规则。
第二个主要的缺点是预处理器的能力非常有限。C 程序员通常需要依赖强大的构建系统来生成正确的预处理器定义,而构建系统本身往往又使用另一种语言来生成这些定义。
Go
在 Go 中,编译期的代码省略是以文件为单位、通过构建标签来实现的。例如:
// +build mytag
func myFunction() {
// This code will only be included if the file is built with the "mytag"
// build tag.
}对于平台相关的代码,还可以使用文件名后缀的方式。这不是一篇 Go 教程,这里就不展开了,关键在于条件编译是在文件层面进行的。
对于这种做法,大家的看法见仁见智。我认为其中客观上不好的一点是,你仍然需要用某种预处理语言来决定是否包含某个标签,而这通常又会交给构建系统、Makefile 等去处理。
Zig 的 comptime
Zig 的 comptime 让你可以直接用 Zig 来按条件禁用代码。下面是一个关于平台相关代码的最简单示例:
const builtin = @import("builtin");
fn myFunction() void {
if (comptime builtin.os.tag == .macos) {
// This code will only be included if the target OS is macOS.
return;
}
// This code will be included for all other operating systems.
}首先要注意的是,这就是标准的 Zig 代码,没有预处理器语言,也没有特殊语法。其次,Zig 编译器足够智能,能够判断出由于这个 if 语句以退出条件结尾,如果该条件显然为真,就不需要再编译后面的内容。在这个例子中,如果目标操作系统是 macOS,编译器就只会编译第一个代码块。
关于 comptime 关键字的说明:上面的 comptime 关键字其实是不必要的。如果所有可用信息在编译期就已确定,Zig 会自动在编译期求值该条件。由于 builtin 全是编译期常量,comptime 关键字是多余的。我加上它只是为了让示例更直观。
下面是一个来自我正在开发的实际项目的更复杂的例子:
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
adwaita.enabled(&config) and
adwaita.versionAtLeast(1, 4, 0)) {
// This code will only be included if we're building with
// libadwaita available and the version is at least 1.4.0
// at both build and runtime.
}这体现了 comptime 就是标准 Zig 的强大之处。在这个例子里,我们混合了 comptime 条件和非 comptime 条件。第一个条件是 comptime,其余则是运行时条件。
第一个 comptime 条件会检查我们的构建配置是否启用了 libadwaita,以及构建时拥有的版本是否至少为 1.4.0。如果这个条件为假,Zig 编译器就知道其余条件无论如何都不可能让该代码块被包含,因此根本不会编译这个代码块的其余部分。
重要的是,这意味着我可以在运行时条件以及代码块中使用 adwaita 库,即使该库不可用、版本不对,或者我引用了仅在新版本库中才存在的类型,也不会出现编译错误。
为什么要调用两次 adwaita.versionAtLeast(1, 4, 0)?第一次是构建时检查,确保我们拥有 libadwaita 且版本至少为 1.4.0;第二次是运行时检查,确保动态链接的库版本至少为 1.4.0。这样一来,就可以在库版本较新的系统上编译代码,而让它在版本可能更旧的系统上运行,等等各种组合都能兼顾。
为了更清楚地说明问题,我们来想想如果用 C 会怎么做。在 C 中,我们可能会使用 CMake 这类构建系统,创建一个一次性的程序并尝试编译它,以此来决定是否应该定义某个预处理器标志。唉。🤮
comptime 的坑
使用 comptime 按条件禁用代码时,有一些坑需要注意。首先,comptime 不会跨越函数边界,除非使用了 comptime 关键字。例如,如果我们把上面的条件抽成一个函数,就不会生效:
fn myFunction() void {
if (hasFeature()) {
// Feature-specific code.
} else {
// Default code.
}
}
fn hasFeature() bool {
return false;
}在上面的例子中,即便 hasFeature 明显返回 false,编译器仍会编译条件语句的两个分支。要修复这个问题,可以在函数调用前加上 comptime:
fn myFunction() void {
if (comptime hasFeature()) {
// Feature-specific code.
} else {
// Default code.
}
}遗憾的是,如果 hasFeature 混合了 comptime 和非 comptime 条件,这种做法就行不通了。这时,你需要将函数内联:
fn myFunction() void {
if (hasFeature()) {
// Feature-specific code.
} else {
// Default code.
}
}
inline fn hasFeature() bool {
return (comptime comptimeCheck()) and runtimeCheck();
}这样就能按预期工作了:如果 comptime 检查为假,编译器就不会包含特定功能的代码。
这种细微的行为很容易被忽略。每当我写了这样的 comptime 检查,我都会在 CI 中同时测试两个分支,以确保程序在两种环境下都能构建通过。
comptime 太棒了
我爱 comptime。这是一个直到拥有之后才意识到自己多么需要的特性,也是在使用其他语言时会让我无比想念的特性。本文只展示了 comptime 的一个具体用例,但即便 comptime 只有这一个用途,也足以成为杀手级特性。
通过 comptime 进行条件编译,让我能够编写跨平台的代码,同时共享相同的文件和大量相同的代码。我不需要复杂的构建系统或外部工具来管理平台相关代码,只需编写 Zig 代码,剩下的交给编译器处理。
当然,comptime 还能做很多其他事情,也有许多其他强大的特性。例如,comptime 中的类型会与目标系统保持一致(因此指针大小等都是正确的)。还有太多值得探索的地方!我鼓励你去查阅 Zig 文档和其他博文来了解更多,也不妨亲自试试 Zig!
随机一篇博客
评论
登录后参与讨论