在Zig中使用Comptime条件禁用代码
摘要
Mitchell Hashimoto解释了如何利用Zig的comptime特性在编译时条件禁用代码,并与C和Go中的实现方式进行了比较。
暂无内容
查看缓存全文
缓存时间: 2026/05/16 03:40
# 使用 Zig 的 Comptime 有条件地禁用代码
来源:https://mitchellh.com/writing/zig-comptime-conditional-disable
Zig 有一个非常强大的特性,叫做 `comptime`(https://ziglang.org/documentation/0.13.0/#comptime)。Comptime 让你可以在编译时运行 Zig 代码。这不是什么特殊的宏语言或 AST 操作,它就是在编译时运行的标准 Zig 代码。唯一的限制是 comptime 代码不能有副作用(没有系统调用、没有 IO 等)。
我承认,刚开始接触 Zig 时,我觉得这玩意就是噱头。“编译时能真正做多少事?”我当时想。但在非玩具项目(https://mitchellh.com/ghostty)中使用 Zig 两年后,comptime 被广泛应用,并且成了我最喜欢的 Zig 特性。
Comptime 本身是个大话题。所以我不打算深入讲解所有内容,而是给你展示一个特别有用的模式:用 comptime 有条件地禁用代码。
---
## 为什么要有条件地禁用代码?
在软件开发中,有条件地禁用代码是一种常见的模式。以下是几个常见的原因:
1. **平台特定代码**:某个函数在 macOS 和 Linux 上可能有不同的实现。
2. **调试代码**:某些代码只在调试时有用,生产构建中需要禁用。
3. **构建配置**:根据构建时的配置,你可能想省略或修改某个功能。
在 Python 或 JavaScript 这类动态语言中,通常可以在运行时用一个基本的 `if` 语句来选择正确的代码路径。因为动态语言只在运行时求值,所以可以避免执行那些可能不工作的代码路径。
然而,在编译型语言中,你不能用 `if` 语句来有条件地禁用代码,因为编译器必须编译并链接所有可能在运行时被执行的代码路径。
除了让代码能编译通过,在编译时省略代码还能让二进制文件更小,并避免可能带来性能开销的运行时条件判断。
---
## 非 Zig 的实现方式
我们先来看看其他编译型语言是如何处理这个问题的。如果你不感兴趣,可以跳过本节,直接看下一节如何用 Zig 的 comptime 解决。
## C 系语言
在 C 系语言中,经常使用预处理器来有条件地禁用代码。例如:
```
#define FLAG
void my_function() {
#ifdef FLAG
// 只有定义了 FLAG 时才会包含这段代码。
#else
// 否则包含这段代码。
#endif
}
```
这实际上是在编译时进行代码模板化。预处理器会先运行,在文本层面转换代码,然后编译器再编译代码。第一个主要缺点是预处理器是独立的语言,有自己的语法和规则。
第二个主要缺点是预处理器的能力极其有限。C 程序员通常依赖一个功能强大的构建系统来生成正确的预处理器定义,而构建系统又使用自己的语言来生成这些定义。
## Go
在 Go 中,编译时代码省略是通过带构建标签的单个文件实现的。例如:
```
// +build mytag
func myFunction() {
// 只有使用 "mytag" 构建标签编译这个文件时,才会包含这段代码。
}
```
对于平台特定代码,还可以使用文件名后缀。这不是 Go 教程,所以不深入细节,关键是条件编译是在文件级别完成的。
对于这种实现方式,有各种主观看法。我认为一个客观的缺点是:你仍然需要某种预处理器语言来选择是否包含某个标签,而这通常要交给构建系统、Makefile 等来处理。
---
## Zig 的 Comptime
Zig 的 comptime 让你能用 Zig 代码本身来有条件地禁用代码。以下是最简单的平台特定代码示例:
```zig
const builtin = @import("builtin");
fn myFunction() void {
if (comptime builtin.os.tag == .macos) {
// 只有目标操作系统是 macOS 时才会包含这段代码。
return;
}
// 这段代码会在所有其他操作系统下被包含。
}
```
首先要注意的是,代码是标准的 Zig 代码,没有预处理器语言或特殊语法。其次,Zig 编译器非常聪明,它知道如果 `if` 语句以退出条件结尾,那么当条件为真时,就不再需要编译其他任何东西。在这个例子中,如果目标 OS 是 macOS,编译器就只会编译第一个代码块。
**关于 `comptime` 关键字的说明:** 上面的 `comptime` 关键字其实不是必须的。如果所有可用信息在编译时都已知,Zig 会自动在编译时评估条件。由于 `builtin` 完全是编译时常量,`comptime` 关键字其实是多余的。我写上是想让示例更明确。
下面是一个更复杂的例子,来自我实际正在做的项目(https://mitchellh.com/ghostty):
```zig
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
adwaita.enabled(&config) and
adwaita.versionAtLeast(1, 4, 0)) {
// 只有在构建时启用了 libadwaita 并且版本至少为 1.4.0 时,
// 并且运行时动态链接库也至少为 1.4.0 时,才会包含这段代码。
}
```
这展示了 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` 关键字。例如,如果我们把上面的条件提取到一个函数中,就不会生效:
```zig
fn myFunction() void {
if (hasFeature()) {
// 功能特定代码。
} else {
// 默认代码。
}
}
fn hasFeature() bool {
return false;
}
```
在上面的例子中,尽管 `hasFeature` 明显是 false,编译器仍会编译条件分支两边的代码。要解决这个问题,可以在函数调用前加上 `comptime`:
```zig
fn myFunction() void {
if (comptime hasFeature()) {
// 功能特定代码。
} else {
// 默认代码。
}
}
```
不幸的是,如果 `hasFeature` 混合了 comptime 和非 comptime 条件,这样做就不行了。这种情况下,你需要内联函数:
```zig
fn myFunction() void {
if (hasFeature()) {
// 功能特定代码。
} else {
// 默认代码。
}
}
inline fn hasFeature() bool {
return (comptime comptimeCheck()) and runtimeCheck();
}
```
现在就能按预期工作:如果 comptime 检查为 false,编译器就不会包含功能特定代码。
这种微妙的行为很容易被忽略。每当我做这样的 comptime 检查时,我都会在 CI 中添加两个分支,以确保程序能在两种环境中构建。
---
## Comptime 太棒了
我非常喜欢 comptime。这是一个在我拥有之前都不知道自己需要的特性,而且当我在其他语言中工作时,我会非常想念它。这篇博文只展示了 comptime 的一个具体用例,但如果 comptime 只擅长做这一件事,它也仍然是一个杀手锏特性。
有了 comptime 的条件编译,我可以在共享相同文件和大量相同代码的同时,编写跨平台的代码。我不需要复杂的构建系统或外部工具来管理平台特定代码。我只需要写 Zig 代码,编译器会处理剩下的事情。
再说一次,comptime 还有很多其他用途和强大的特性。例如,comptime 中的类型会匹配目标系统(所以指针大小等是正确的)。还有很多很多!我鼓励你去查看 Zig 文档和其他博文来了解更多,并且试试 Zig 吧!
相似文章
在Zig中利用Comptime实现标签联合子集
Mitchell Hashimoto 展示了如何利用 Zig 的 comptime 创建标签联合的子集类型,无需穷举处理即可实现编译时安全。
@lukeed05: 为庆祝 @rolldown_rs 1.0 发布,隆重推出 `comptime`——一个受 Zig 启发的构建时评估原语,作为 Vite 和 Rolldown 插件提供……
Luke Edwards 发布了 `comptime`,这是一个受 Zig 启发的构建时评估原语,适用于 Vite 和 Rolldown,允许在构建过程中执行代码,将调用位置替换为评估后的值。
Zig 示例教程
通过带注释的示例,对 Zig 编程语言进行实践性介绍,涵盖从基础到高级的主题。灵感来源于 Go by Example。
Zig 构建速度正在提升
Zig 0.15 相比 0.14 在编译时性能有显著提升,构建脚本编译时间从约 7 秒降至约 1.7 秒,完整构建时间从 41 秒降至 32 秒,且仍使用 LLVM。本文重点介绍了自托管后端和增量编译方面的进展。
用 Zig 写一个 C 编译器
一位开发者记录了用 Zig 语言、按照 Nora Sandler 的教程系列构建名为 paella 的 C 编译器的全过程。