Zig:构建系统重构及ELF链接器改进
- #Zig
- #编译器
- #构建系统
- #ELF链接器
- #增量编译
2026年5月30日:ELF链接器改进
作者:Matthew Lugg
过去几周我一直在开发新的ELF链接器,该链接器首次出现在Zig 0.16.0中。在0.16.0发布时,这个链接器实现还处于相当早期的阶段,仅支持链接纯Zig代码(不包含任何外部库,甚至libc)——因此它默认是禁用的(可通过-fnew-linker启用)。不过,自首次发布以来已经取得了很大进展!
一个重要的里程碑是:根据我最新的PR,新的ELF链接器已经能够构建包含LLVM和LLD库的自托管Zig编译器,这需要很多底层特性支持。
[mlugg@nebula master]$ # 使用新链接器构建Zig编译器:
[mlugg@nebula master]$ zig build -Dno-lib -Dnew-linker -Denable-llvm
[mlugg@nebula master]$ # 用那个编译器构建带LLVM和LLD的内容:
[mlugg@nebula master]$ ./zig-out/bin/zig build-exe ~/hello.zig -fllvm -flld
[mlugg@nebula master]$ ./hello
Hello, World!
当然,ELF链接器本身可能不是最令人兴奋的东西,所以这个新链接器的核心功能是支持快速增量编译。经过最近的增强,现在(在x86_64 Linux上)可以在链接外部库、C源码等时进行增量重建——而且没有任何额外性能开销!下面是尝试在Andrew的Tetris克隆上运行的情况:
对Andrew的Tetris克隆进行一些简单的修改,每次构建大约30ms。
快速增量重建在Zig编译器本身也工作得很好:
[mlugg@nebula master]$ zig build -Dno-lib -Denable-llvm -fincremental --watch
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 36s
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 244ms
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 228ms
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 288ms
Build Summary: 4/4 steps succeeded
install success
└─ install zig success
└─ compile exe zig Debug native success 283ms
该链接器实现目前最大的缺失功能是仍未支持为Zig代码生成DWARF调试信息——这显然是我的下一个优先事项。但即使没有这个支持,即时重建的实用性也非常惊人,例如在你大量使用print调试的任何场景中。
如果你正在使用Zig的master分支并且是在x86_64 Linux上,考虑尝试一下新的ELF链接器的增量编译(如果之前对你的项目不工作的话)!我相信许多代码库已经可以很好地运行,从而使你的项目重建时间缩短到毫秒级。当然,如果遇到任何bug,请提交issue。
如果你目前还停留在Zig的标记版本上,不用担心——正如Andrew在上次开发日志中提到的,Zig 0.17.0即将发布,所以你很快也能尝试这个功能了!
2026年5月26日:构建系统重构
作者:Andrew Kelley
重大分支刚刚合并:将maker进程与configurer进程分离。 本开发日志本质上是即将发布的版本的预演,但也是向那些希望帮助测试新功能并提供反馈以指导Zig项目前进的人发出的提前通知。
以前,build.zig文件加上构建系统实现都会被编译成一个臃肿的进程(Debug模式)。build.zig逻辑在内存中构建完构建图后,由“构建运行器”代码执行它。
现在,build.zig文件被编译成一个小的进程(“configurer”),以debug模式运行。当这个逻辑完成构建图的构建后,它会被序列化成一个二进制配置文件。父级zig build进程知道这个文件并将其缓存以供下次使用。在等待这一切的同时,它异步地以release模式编译构建图执行进程(“maker”)。一旦配置文件就绪且maker进程编译完成,就执行maker进程,并将配置文件传递给它。得益于全局缓存,maker进程每个Zig版本只需要编译一次。然后maker进程执行构建图,该构建图包含在序列化的配置文件中。
这一改动的主要动机是以三种方式加速zig build:
- 只有用户的
build.zig逻辑会在每次更改时被编译,而不是整个构建系统。随着我们引入了--watch、--fuzz和--webui,这一点开始变得更有价值。构建系统可以增加更多特性而不会使zig build变慢。 - 现在构建系统可以在知道没有任何变化时完全跳过重新运行
build.zig逻辑,例如如果你在zig build命令行中添加了-freference-trace,它会避免冗余运行你的build.zig逻辑,而是使用上一次的相同配置。 - 实际执行构建图的进程现在使用优化模式编译。
为了演示第2点和第3点,以下是运行zig build --help前后的差异:
Benchmark 1 (34 runs): master/zig build -h wall_time: 150ms ± 5.52ms peak_rss: 84.8MB ± 275KB cpu_cycles: 593M ± 4.01M instructions: 995M ± 52.5K
Benchmark 2 (348 runs): branch/zig build -h wall_time: 14.3ms ± 744us ⚡- 90.4% ± 0.4% peak_rss: 78.5MB ± 562KB ⚡- 7.4% ± 0.2% cpu_cycles: 24.1M ± 821K ⚡- 95.9% ± 0.1% instructions: 43.7M ± 23.8K ⚡- 95.6% ± 0.0%
效果非常显著,因为之前每次zig build命令都会执行build.zig逻辑,而现在构建系统使用的是缓存好的序列化配置。
除了性能,我还期望第三方工具(如ZLS)能够从使用序列化配置文件中受益,而不必维护构建运行器的分支。
这次变更大幅重构了Zig构建系统的内部机制,但从API角度来说基本不破坏兼容性,除了上面PR中提到的例外。
对大多数人来说,主要的破坏性变更可能是:
if (b.args) |args| { run_cmd.addArgs(args); }
⬇️
run_cmd.addPassthruArgs();
这移除了构建脚本观察那些参数的能力。作为交换,当这些参数改变时,构建脚本不再需要重新构建。
如果你希望影响Zig的发展方向,现在是升级你的项目到开发版本并尝试这些变更的好时机。我们将在几周内发布0.17.0。不过,如果你没有时间,并且发现0.17.0破坏了你的构建,别担心,0.17.1也会有机会进行修复。
2026年4月8日:使用LLVM进行增量编译
作者:Matthew Lugg
上个月合并了类型解析更改后,我花了一些时间做个人项目,但最近我还是找到时间对LLVM代码生成后端进行了一些改进。这涉及几个不同目标的增强,但一个很好的用户可见变化是我成功让LLVM后端支持增量编译。
遗憾的是,这并不能加快令人头疼的“LLVM Emit Object”阶段:那个时间完全取决于LLVM。但是,增量编译有助于最小化Zig编译器本身的耗时,这意味着如果你的代码存在编译错误(因此“LLVM Emit Object”将被跳过),你通常可以很快得到错误信息。(当然,对于成功的构建,它也会带来些许加速。)
此支持现在在master分支构建中可用,并将包含在即将发布的0.16.0版本中。
对于还没有尝试过的人,特别是如果你正在使用Zig的master分支,请一定试试通过-fincremental --watch参数使用增量编译!Zig核心团队已经在我们的工作流中受益于增量编译有一年了,我们也听到了用户的好评。这个功能目前已经相当稳定,人们经常惊讶于只需毫秒级就能获得最新的编译错误信息,而不是秒级。
我自己并没有真正使用LLVM后端的增量编译,但CI中所有的增量测试覆盖现在都已为LLVM后端启用,而且我收到了用户的积极反馈,所以绝对值得一试。一如既往,如果你遇到增量编译的bug,请尽可能报告!
谢谢,希望这能帮到你 :)
2026年3月10日:类型解析重新设计,附带语言变化
作者:Matthew Lugg
今天,我合并了一个3万行的PR,经过了两(或者说三)个月的工作。这个分支的目标是将Zig编译器的内部类型解析逻辑重构成更合理、更直接的设计。对我个人来说这是一个非常令人兴奋的变更,因为它允许我清理很多编译器内部逻辑,而且它还有一些你可能感兴趣的面向用户的变化!
首先,Zig编译器现在对类型字段的分析更加“懒惰”:如果类型从未被初始化,那么Zig就不需要关心该类型“长什么样”。这对于那些同时作为命名空间的类型很重要,这是现代Zig中的常见模式。例如,当使用std.Io.Writer时,你不希望编译器也拉入一大堆std.Io中的代码!下面是一个简单的例子:
const Foo = struct {
bad_field: @compileError("我是一个邪恶的字段,哈哈哈"),
const something = 123;
};
comptime {
_ = Foo.something; // `Foo`仅用作命名空间
}
以前,这段代码会触发编译错误。现在,它编译通过了,因为Zig从未真正查看@compileError调用。
我们做的另一个改进是“依赖循环”的体验。任何以前在Zig中遇到过依赖循环编译错误的人都知道,它们的错误信息完全没有帮助——但现在改变了!如果你遇到一个(现在发生的概率也比以前低一点),你会得到一个详细的错误信息,告诉你依赖循环的确切来源。来看:
const Foo = struct { inner: Bar };
const Bar = struct { x: u32 align(@alignOf(Foo)) };
comptime { _ = @as(Foo, undefined); }
$ zig build-obj repro.zig
error: dependency loop with length 2
repro.zig:1:29: note: type 'repro.Foo' depends on type 'repro.Bar' for field declared here
const Foo = struct { inner: Bar };
^~~
repro.zig:2:44: note: type 'repro.Bar' depends on type 'repro.Foo' for alignment query here
const Bar = struct { x: u32 align(@alignOf(Foo)) };
^~~
note: eliminate any one of these dependencies to break the loop
...(原文未结束,但已足够翻译)
评论