Zig 2026上半年:构建系统分离、@bitCast 新语义、SPIR-V 后端复苏、增量编译提速 10 倍
本文是 Zig 语言 2026 年上半年的开发日志合集,由 Andrew Kelley、Matthew Lugg、Ali Cheraghi 等核心贡献者撰写,涵盖编译器基础架构的多个重大变更。关键内容包括:(1) 将包管理功能从编译器进程移至构建系统的 maker 进程,使 zig build --help 从 150ms 降至 14ms(-90%),二进制体积缩小 4%;(2) @bitCast 重定义为基于逻辑位布局的端序无关语义,并允许对枚举进行 bitcast;(3) SPIR-V 后端修复位腐烂,新增 @SpirvType 内建函数、执行模式调用约定、多线程代码生成和 .spv 对象文件链接;(4) LLVM 后端整数降级优化(非 ABI 宽度整数用零/符号扩展存入内存)使 Zig 编译器自身性能提升约 5%,并首次支持 LLVM 后端的增量编译;(5) 新 ELF 链接器(-fnew-linker)已能编译带 LLVM/LLD 的 Zig 编译器自身,且支持快速增量重链接(~244ms);(6) io_uring 和 Grand Central Dispatch 的 std.Io.Evented 实现落地,I/O 实现可零改动切换;(7) 包管理将依赖缓存到项目本地的 zig-pkg 目录,新增 --fork 标志用于临时替换依赖树的任意包。文章适合对 Zig 编译器内部机制、系统编程语言演进及编译优化感兴趣的读者。
Now that there is a separate process for users’ build.zig scripts and the build system itself, it makes sense for that to be the place that package management logic lives.
I moved these subcommands to the maker process:
zig build
zig fetch
zig init
zig libc
This means that large parts of what used to be included in the compiler executable are now shipped in source form instead, including:
package fetching logic
HTTP client and networking
TLS (Transport Layer Security) and associated crypto
Git protocol
xz, gzip, zstd, flate, zip
parsing, validation, and otherwise dealing with build.zig.zon files
Consequently, this functionality can now be patched without rebuilding the compiler, making it easier for users and contributors to tinker.
Furthermore, it means that package management in zig now has safety checks enabled when doing networking, since the maker executable is compiled in ReleaseSafe mode. Plus, all the crypto used for networking and file hashing can now take advantage of special CPU instructions available on the host, even the ones that are too rare to normally depend on when distributing software. We can have AOT cake and eat JIT, too!
既然用户的 build.zig 脚本和构建系统本身现在处于不同的进程中,将包管理逻辑放在那里就顺理成章了。
我将以下子命令移到了 maker 进程中:
zig build zig fetch
zig init
zig libc
这意味着,以前包含在编译器可执行文件中的大量代码,现在以源码形式发布,包括:
包获取逻辑
HTTP 客户端和网络栈
TLS 及其相关密码学
Git 协议
xz、gzip、zstd、flate、zip 压缩/解压
build.zig.zon 文件的解析、验证和处理
因此,现在无需重新编译编译器即可修补这些功能,方便用户和贡献者进行试验。
此外,Zig 的包管理在联网时会启用安全检测,因为 maker 可执行文件是以 ReleaseSafe 模式编译的。同时,用于网络和文件哈希的所有密码学操作,现在都能利用宿主 CPU 提供的特殊指令,即使这些指令在分发软件时通常因过于罕见而无法依赖。AOT 的蛋糕我们想吃就吃,JIT 的蛋糕也一样!
My original motivation for doing this was in relation to exposing a build server protocol in order to unblock ZLS after maker/configurer process separation made breaking changes to the --build-runner override flag.
Originally, the process tree looked like this:
zig build (the zig compiler + package manager) └─ builder (the user's build.zig logic + build system implementation)
The process separation changeset made it look like this instead:
zig build (the zig compiler + package manager) ├─ configurer (the user's build.zig logic) └─ maker (build system)
At this point, consider a long-running zig build --watch process, watching files and rebuilding on source code changes. If any changes to build.zig are detected, or any files observed during execution of that logic, it means configurer needs to be rerun, meaning that maker process must exit to give zig build a chance to repeat the package management logic.
Now, after the changes described in this devlog entry, it looks like this:
zig build (the zig compiler) └─ maker (build system + package manager) └─ configurer (the user's build.zig logic)
Thus, when configuration needs to be rerun, maker process can continue to live because it is the parent process rather than a sibling. In terms of the upcoming build server, it means avoiding an awkward situation where the server has to exit and the client has to reconnect, rather than simply informing the client of a configuration change.
我最初这么做的动机,与暴露构建服务器协议有关,目的是在 maker/configurer 进程分离对 --build-runner 覆盖标志做出破坏性变更后,解除对 ZLS 的阻塞。
最初,进程树是这样的:
zig build(Zig 编译器 + 包管理器) └─ builder(用户的 build.zig 逻辑 + 构建系统实现)
进程分离变更集之后,变成了这样:
zig build(Zig 编译器 + 包管理器) ├─ configurer(用户的 build.zig 逻辑) └─ maker(构建系统)
现在考虑一个长期运行的 zig build --watch 进程,它监控文件并在源码变更时重新构建。如果检测到 build.zig 有任何变动,或执行该逻辑时观察到的任何文件发生了变化,就意味着需要重新运行 configurer,而 maker 进程必须退出,以便让 zig build 有机会重新执行包管理逻辑。
经过本次 devlog 中描述的变更后,进程树现在变成了这样:
zig build(Zig 编译器) └─ maker(构建系统 + 包管理器) └─ configurer(用户的 build.zig 逻辑)
这样一来,当需要重新运行配置时,maker 进程可以继续存活,因为它现在是父进程而非兄弟进程。对于即将到来的构建服务器而言,这意味着可以避免那种服务器必须退出、客户端必须重新连接的尴尬局面,而只需通知客户端配置已变更即可。
This is almost entirely a non-breaking change, but there are some observable differences:
Zig executable binary size: shrinks 4% from 14.1 to 13.5 MiB (no LLVM, ReleaseSmall)
--maker-opt flag is replaced by ZIG_DEBUG_MAKER environment variable
--zig-lib-dir flag is replaced by ZIG_LIB_DIR environment variable
The follow-up issues to this changeset are the main blockers until we tag Zig 0.17.0:
build server protocol MVP (needed to unblock ZLS)
introduce the concept of adding path dependencies of the build script itself
make zig build --watch detect modifications to the build script and rerun itself
different cwd causes build script cache miss
I have two conferences coming up in July and I need to work on my talks, so being realistic, I don’t think I will have time to wrap these up until early August. Contributions welcome, of course.
Big thanks to Techatrix from the ZLS team for reaching out and working with me on the build server protocol! They are seeking sponsorship, by the way.
这几乎是一个完全非破坏性的变更,但仍有一些可观察到的差异:
Zig 可执行文件体积:缩小 4%,从 14.1 MiB 降至 13.5 MiB(无 LLVM,ReleaseSmall 模式)
--maker-opt 标志被环境变量 ZIG_DEBUG_MAKER 取代
--zig-lib-dir 标志被环境变量 ZIG_LIB_DIR 取代
此变更集的后续问题是我们标记 Zig 0.17.0 之前的首要阻塞项:
构建服务器协议 MVP(需用于解除 ZLS 阻塞)
引入构建脚本自身添加路径依赖的概念
使 zig build --watch 能检测构建脚本的修改并自动重新运行
不同的 cwd 导致构建脚本缓存未命中
我七月份有两场会议,需要准备演讲,所以说实话,我认为要到八月初才有时间完成这些工作。当然,欢迎贡献。
非常感谢 ZLS 团队的 Techatrix 主动联系并合作开发构建服务器协议!顺便提一下,他们正在寻求赞助。
There’s quite a bit to cover. The SPIR-V backend had bitrotted in a number of places after the recent compiler changes, so I spent the past several weeks dragging it into a better state.
@SpirvType
SPIR-V has a handful of types that couldn’t be expressed in Zig’s type system. The new @SpirvType builtin has been introduced to address the longest-standing blocker for writing shaders. See #20550, #23326 and #35461 to trace the background.
const Sampler = @SpirvType(.sampler); const Image = @SpirvType(.{ .image = .{ .usage = .{ .sampled = u32 }, .format = .unknown, .dim = .@"2d", .depth = .unknown, .arrayed = false, .multisampled = false, .access = .unknown, } }); const SampledImage = @SpirvType(.{ .sampled_image = Image }); const RuntimeArray = @SpirvType(.{ .runtime_array = u32 }); const sampled_image = @extern(*addrspace(.constant) const SampledImage, .{ .name = "sampled_image", .decoration = .{ .descriptor = .{ .set = 0, .binding = 1 } }, });
内容不少。SPIR-V 后端在最近的编译器变更后,多处代码发生了腐化,因此我花了数周时间将其拖回良好状态。
@SpirvType
SPIR-V 有少数类型无法在 Zig 的类型系统中表达。新的 @SpirvType 内置函数旨在解决编写着色器时最长期的阻塞点。有关背景,请参阅 #20550、#23326 和 #35461。
const Sampler = @SpirvType(.sampler);
const Image = @SpirvType(.{ .image = .{
.usage = .{ .sampled = u32 },
.format = .unknown,
.dim = .@"2d",
.depth = .unknown,
.arrayed = false,
.multisampled = false,
.access = .unknown,
} });
const SampledImage = @SpirvType(.{ .sampled_image = Image });
const RuntimeArray = @SpirvType(.{ .runtime_array = u32 });
const sampled_image = @extern(*addrspace(.constant) const SampledImage, .{
.name = "sampled_image",
.decoration = .{ .descriptor = .{ .set = 0, .binding = 1 } },
});
Execution Mode on the Calling Convention
Execution mode info (workgroup size, fragment origin, etc.) is now carried by the calling convention instead of being emitted via inline assembly OpExecutionMode. The old std.gpu.executionMode() helper is gone, and the SPIR-V assembler now rejects manual OpExecutionMode instructions. Two new calling conventions, spirv_task and spirv_mesh, were also added for mesh shading pipelines.
export fn vert() callconv(.spirv_vertex) void {} export fn frag() callconv(.{ .spirv_fragment = .{ .depth_assumption = .greater } }) void {} export fn comp() callconv(.{ .spirv_kernel = .{ .x = 8, .y = 8, .z = 1 } }) void {} export fn task() callconv(.{ .spirv_task = .{ .x = 1, .y = 1, .z = 1 } }) void {} export fn mesh() callconv(.{ .spirv_mesh = .{ .stage_output = .output_lines, .max_primitives = 1, .max_vertices = 2 } }) void {}
Capabilities and Extensions from CPU Features
Capabilities and extensions used to be emitted ad hoc by codegen or via inline assembly. They’re now driven entirely by the CPU feature set like other targets, with dependency chains extracted from SPIRV-Headers (excluding external vendors for now), and the assembler now rejects any attempt to emit OpCapability or OpExtension directly.
Multi-Threaded Codegen
From day one, the SPIR-V backend ran codegen single-threaded inside the linker thread. Each codegen job now produces an Mir value just like every other self-hosted backend, and gets scheduled on the compiler’s thread pool.
The same change brought back two ISel passes that had been removed during earlier refactors: dedup_types (which merges equivalent type instructions) and prune_unused (which strips dead code from the final module). These had originally been deleted back when codegen was single-threaded.
Object File Linking
.spv files are now recognised as object files. You can compile multiple .zig files (or external .spv objects) and have the SPIR-V linker stitch them into a single module.
Tens of bugs have also been fixed along the way with a nearly 10% increase in total passing behavior tests (49% now) on the spirv64-vulkan target, std.gpu was renamed to std.spirv and the SPIR-V backend is meaningfully more useful than it was a month ago, but there’s still a long way to go. Plenty of behavior tests remain skipped on SPIR-V. That said, if you’ve been on the fence about trying Zig for shaders or compute kernels, this is a good time to give it a shot. Bug reports are very welcome on Codeberg. Happy hacking!
调用约定中的执行模式
执行模式信息(工作组大小、片段原点等)现在通过调用约定传递,而不是通过内联汇编的 OpExecutionMode 发出。旧的 std.gpu.executionMode() 辅助函数已移除,SPIR-V 汇编器现在拒绝手动的 OpExecutionMode 指令。还新增了两个调用约定 spirv_task 和 spirv_mesh,用于网格着色管线。
export fn vert() callconv(.spirv_vertex) void {}
export fn frag() callconv(.{ .spirv_fragment = .{ .depth_assumption = .greater } }) void {}
export fn comp() callconv(.{ .spirv_kernel = .{ .x = 8, .y = 8, .z = 1 } }) void {}
export fn task() callconv(.{ .spirv_task = .{ .x = 1, .y = 1, .z = 1 } }) void {}
export fn mesh() callconv(.{ .spirv_mesh = .{ .stage_output = .output_lines, .max_primitives = 1, .max_vertices = 2 } }) void {}
基于 CPU 特性的能力和扩展
能力和扩展以前是由代码生成临时发出,或通过内联汇编实现。现在它们完全像其他目标一样由 CPU 特性集合驱动,依赖链从 SPIRV-Headers 中提取(暂时排除外部供应商),汇编器现在拒绝任何直接发出 OpCapability 或 OpExtension 的尝试。
多线程代码生成
从一开始,SPIR-V 后端的代码生成就在链接器线程内单线程运行。现在,每个代码生成作业都会像其他自托管后端一样产生一个 Mir 值,并被调度到编译器的线程池上。
同一变更还带回了两个在早期重构中被移除的指令选择阶段:dedup_types(合并等效的类型指令)和 prune_unused(从最终模块中剥离死代码)。这些阶段最初在代码生成单线程时被删除。
目标文件链接
.spv 文件现在被识别为目标文件。您可以编译多个 .zig 文件(或外部的 .spv 目标文件),并让 SPIR-V 链接器将它们缝合为单个模块。
在此过程中还修复了数十个 bug,在 spirv64-vulkan 目标上,通过的行为测试总数增加了近 10%(目前为 49%)。std.gpu 已重命名为 std.spirv,SPIR-V 后端比一个月前更加实用了,但仍有很长的路要走。大量行为测试在 SPIR-V 上仍被跳过。不过,如果您还在犹豫是否尝试使用 Zig 编写着色器或计算内核,现在是个好时机。欢迎在 Codeberg 上提交 bug 报告。Happy hacking!
A few weeks ago, I began working on a branch implementing an improvement to the LLVM backend which had been planned for a long time. This ended up snowballing into a bigger change which implemented a few language proposals you might be interested to hear about.
LLVM Backend Integer Lowering
Zig has always lowered arbitrary bit-width integer types (e.g. u4, i13, u40) directly to LLVM IR’s bit-int types (i4, i13, i40). However, we’ve known for a long time that this lowering is not optimal, because LLVM’s documented semantics for representing these types in memory are unnecessarily restrictive to the optimizer. Perhaps more importantly, because Clang never emits LLVM IR like this, these code paths in LLVM have never been properly tested, and so are poorly supported in practice—over the past few years, we have observed many instances of trivial optimizations being missed and even straight-up miscompilations.
So, the original goal of the PR was to only use these bit-int types when manipulating values in SSA form, and to zero- or sign-extend them to ABI-sized types (i8, i16, i32, etc) when storing them in memory. This should be well-supported, not least because it matches how Clang lowers C’s _BitInt(N)!
That change was actually fairly straightforward, but I hit one issue which led me down a bit of a rabbit-hole.
几周前,我开始在一个分支上工作,旨在实现一项长期计划的 LLVM 后端改进。这最终像滚雪球一样发展成一个更大的变更,实现了一些您可能感兴趣的语言提案。
LLVM 后端整数降级
Zig 一直将任意位宽的整数类型(如 u4、i13、u40)直接降级为 LLVM IR 的 bit-int 类型(i4、i13、i40)。然而,我们长期以来就知道这种降级并非最优,因为 LLVM 关于在内存中表示这些类型的文档化语义,对优化器施加了不必要的限制。或许更重要的是,由于 Clang 从未发出过这样的 LLVM IR,LLVM 中的这些代码路径从未得到充分测试,因此在实际中支持不佳——过去几年中,我们观察到许多错失简单优化甚至直接出现错误编译的情况。
因此,该 PR 的原始目标是仅在 SSA 形式中操作值时使用这些 bit-int 类型,而在存储到内存时则将其零扩展或符号扩展到 ABI 大小的类型(i8、i16、i32 等)。这应该能得到良好支持,尤其是因为它与 Clang 降级 C 的 _BitInt(N) 的方式一致!
这个变更实际上相当直接,但我遇到了一个问题,这让我陷入了一个研究难题。
The Problem with @bitCast
@bitCast is an interesting builtin. In the past, it was defined as being equivalent to the following sequence of operations:
Take a pointer to the operand value
Cast it to a pointer to the destination type
Load from that pointer
In other words, it was essentially syntax sugar for reinterpreting bytes of memory. However, over time, we diverged from this definition—for instance, it became allowed to use @bitCast to reinterpret a [3]u8 as a u24, even though on most targets @sizeOf(u24) is greater than @sizeOf([3]u8) so the above definition would invoke Illegal Behavior.
Up to now, the LLVM backend had implemented these underspecified semantics for the @bitCast builtin. However, because that definition involved reinterpreting memory, changing how we store integer types in memory ended up impacting the implementation of @bitCast, and introducing Illegal Behavior which led to crashes in the compiler test suite.
The easiest solution to this would probably have been to implement logic in the LLVM backend to approximately match the old behavior. I instead opted for a better solution—implement a new definition of @bitCast.
Redefining @bitCast
In 2024, Jacob Young wrote up language proposal #19755 which aimed to solve the problems with @bitCast by precisely specifying a new set of semantics for it. This proposal was accepted shortly after it was submitted, and in fact, the semantics it details are already implemented by the self-hosted x86_64 backend! So to solve the LLVM backend’s problems, I didn’t necessarily need to match the old @bitCast semantics—instead, this seemed like a good time to finally get the new semantics implemented everywhere.
As an aside, another advantage to doing this is that we could take advantage of the compiler’s Legalize pass, which takes difficult-to-lower operations and rewrites them in terms of simpler operations, so that compiler backends only need to support those simple operations. Legalize already had functionality, used by the self-hosted x86_64 backend, which converted complex @bitCast operations into simpler ones, and it could be easily adapted to aid the other compiler backends too (mainly the LLVM and C backends)—but only if they implemented the new semantics.
Regardless, the point is, I set out on a side quest (which ended up being harder than the original quest) to implement these new semantics throughout the compiler. This includes not only the LLVM and C backends, but also comptime execution—after all, Zig allows you to do almost any operation at comptime, @bitCast included! Because the new semantics are meaningfully different from the old (more on this later), I also had to audit a lot of uses of @bitCast across the standard library, compiler, and supporting libraries (e.g. compiler_rt). But after a few mostly-painless fixes for CI failures, I was able to finally get my PR green, and landed it in master yesterday (closing a good few issues in the process!).
@bitCast 的问题
@bitCast 是一个有趣的内置函数。过去,它被定义为等效于以下操作序列:
获取操作数值的指针
将其转换为目标类型的指针
从该指针加载
换句话说,它本质上是重新解释内存字节的语法糖。然而,随着时间的推移,我们偏离了这个定义——例如,允许使用 @bitCast 将 [3]u8 重新解释为 u24,尽管在大多数目标上 @sizeOf(u24) 大于 @sizeOf([3]u8),因此上述定义会触发非法行为。
到目前为止,LLVM 后端为 @bitCast 内置函数实现了这些规定不完整的语义。但是,由于该定义涉及重新解释内存,更改我们在内存中存储整数类型的方式最终影响了 @bitCast 的实现,并引入了导致编译器测试套件崩溃的非法行为。
最简单的解决方案可能是在 LLVM 后端实现逻辑以近似匹配旧行为。我选择了更好的方案——实现一个新的 @bitCast 定义。
重新定义 @bitCast
2024 年,Jacob Young 撰写了语言提案 #19755,旨在通过为 @bitCast 精确指定一套新语义来解决这些问题。该提案提交后不久即被接受,事实上,其详述的语义已经由自托管的 x86_64 后端实现!因此,为了解决 LLVM 后端的问题,我不必匹配旧的 @bitCast 语义——相反,这似乎是最终在所有地方实现新语义的好时机。
顺便提一下,这样做还有一个好处,我们可以利用编译器的 Legalize 阶段,该阶段将难以降级的操作重写为更简单的操作,这样编译器后端只需要支持这些简单操作。Legalize 已经具备了自托管 x86_64 后端所使用的功能,可以将复杂的 @bitCast 操作转换为更简单的操作,并且可以轻松适应以协助其他编译器后端(主要是 LLVM 和 C 后端)——但前提是它们实现了新语义。
无论如何,关键在于,我踏上了一项支线任务(最终比主线任务更难),即在编译器中全面实现这些新语义。这不仅包括 LLVM 和 C 后端,还包括编译期执行——毕竟,Zig 允许您在编译时执行几乎所有操作,包括 @bitCast!由于新语义与旧语义有显著不同(稍后详述),我还必须审计标准库、编译器和支持库(如 compiler_rt)中大量使用 @bitCast 的地方。但在处理了 CI 失败的一些基本无痛修复后,我终于使 PR 变绿,并于昨天将其合并到 master(过程中关闭了不少问题!)。
The New @bitCast Semantics
Now that we’ve gotten through all of the background, it’s finally time for me to actually explain new @bitCast behavior. Instead of being based on reinterpreting bytes in memory like before, the builtin is now defined in terms of the bits which logically represent a type.
Every type which supports @bitCast has a “logical bit layout”—a representation of that type as an ordered sequence of bits. For instance, u5 is composed of 5 logical bits, which we order from least-significant to most-significant. [2]u5 is composed of 10 logical bits—the 5 from the first element, followed by the 5 from the second element. The new definition of @bitCast is that it reinterprets the logical bits of one type as the logical bits of a different type.
The simplest example is to take an unsigned integer, say a u8, and convert it to a signed integer of the same size, in this case i8. This operation does exactly what you’d expect—the bits are unchanged, and we just reinterpret the most-significant bit as a sign bit. Also unchanged are the semantics of @bitCast between an integer type and a packed struct/packed union type.
The place where the new semantics differ from the old is when you get aggregate types (arrays and vectors) involved.
Consider, for instance, bitcasting a [2]u8 to a u16. Under the old semantics, the result of this operation depends on the target endian: on big-endian targets, the first array element became the 8 most significant bits, whereas on little-endian targets, the first array element became the 8 least significant bits. Under the new semantics, because we only care about logical bit representation (which is endian-agnostic), the operation behaves identically on every target: the first array element becomes the 8 least significant bits. As a general rule, the new semantics tend to match the behavior of the old semantics on little-endian targets.
This definition also allows for some weirder operations, such as converting [2]u3 to @Vector(3, u2):
test "bitcast [2]u3 to @Vector(3, u2)" { const arr: [2]u3 = .{ 0b001, 0b011 }; const vec: @Vector(3, u2) = @bitCast(arr);
// Concatenate all bits of arr starting with the least-significant bit of arr[0] to find the
// logical bit sequence, then read off 2-bit chunks from it to get the elements of the resulting
// vector value vec.
//
// arr[0] arr[1]
// 0b001 0b011
// ------------- -------------
// 1 0 0 1 1 0
// -------- -------- --------
// 0b01 0b10 0b01
// vec[0] vec[1] vec[2]
try expect(vec[0] == 0b01); try expect(vec[1] == 0b10); try expect(vec[2] == 0b01); } const expect = @import("std").testing.expect;
This kind of operation isn’t very useful most of the time, but it’s there if you need it! For instance, perhaps you want to deconstruct an integer into a vector of individual bits to operate on—that can now be done by a @bitCast to @Vector(n, u1).
While doing all of this stuff, I also implemented a couple of smaller accepted proposals—I won’t detail them here, but you can take a look at the issues if you’re interested:
Disallow @bitCast to/from vectors of pointers (#18936)
Allow @bitCast on enums (part of #35602)
Of course, all of these changed semantics will be explained in the 0.17.0 release notes (hopefully a bit more concisely than what I managed here!), and suggested migration steps outlined.
LLVM Backend Performance
On a final note, I just wanted to mention that the original motivation for this branch—changing how the LLVM backend lowers non-ABI integer types—was demonstrably successful at restoring missed optimizations. In fact, the Zig compiler itself—despite not making heavy use of arbitrary bit width integers internally!—saw around 5% performance improvements from the better optimization. This means you might have some minor runtime performance gains to look forward to in 0.17.0!
Thanks for reading, I hope this was interesting to some of you. Happy hacking!
新的 @bitCast 语义
在经历了所有这些背景介绍后,终于到了解释新 @bitCast 行为的时候了。该内置函数不再像以前那样基于重新解释内存中的字节,而是依据逻辑表示类型的位来定义。
每个支持 @bitCast 的类型都有一个“逻辑位布局”(logical bit layout)——将该类型表示为一个有序的位序列。例如,u5 由 5 个逻辑位组成,我们按从最低有效位到最高有效位的顺序排列。[2]u5 由 10 个逻辑位组成——第一个元素的 5 位,后跟第二个元素的 5 位。@bitCast 的新定义是将一个类型的逻辑位重新解释为另一个类型的逻辑位。
最简单的例子是将一个无符号整数,比如 u8,转换为相同大小的有符号整数,即 i8。这个操作完全符合预期——位不变,我们只是将最高有效位重新解释为符号位。整数类型与打包结构体/打包联合体之间的 @bitCast 语义也保持不变。
新语义与旧语义的区别在于涉及聚合类型(数组和向量)时。
例如,将 [2]u8 位转换为 u16。在旧语义下,此操作的结果取决于目标端的字节序(endian):在大端目标上,第一个数组元素成为 8 个最高有效位;而在小端目标上,第一个数组元素成为 8 个最低有效位。在新语义下,由于我们只关心逻辑位表示(与字节序无关),该操作在每个目标上的行为完全一致:第一个数组元素成为 8 个最低有效位。作为一般规则,新语义倾向于匹配旧语义在小端目标上的行为。
这个定义还允许一些更奇特的操作,例如将 [2]u3 转换为 @Vector(3, u2):
test "bitcast [2]u3 to @Vector(3, u2)" {
const arr: [2]u3 = .{ 0b001, 0b011 };
const vec: @Vector(3, u2) = @bitCast(arr);
// 将 `arr` 的所有位从 `arr[0]` 的最低有效位开始拼接,得到逻辑位序列,
// 然后从中读取 2 位的数据块,得到结果向量值 `vec` 的元素。
//
// arr[0] arr[1]
// 0b001 0b011
// ------------- -------------
// 1 0 0 1 1 0
// -------- -------- --------
// 0b01 0b10 0b01
// vec[0] vec[1] vec[2]
try expect(vec[0] == 0b01);
try expect(vec[1] == 0b10);
try expect(vec[2] == 0b01);
}
const expect = @import("std").testing.expect;
这种操作在大多数时候并不常用,但在需要时它就在那里!例如,您可能希望将一个整数解构为单个位的向量以便进行操作——现在可以通过将 @bitCast 应用于 @Vector(n, u1) 来实现。
在完成所有这些工作的同时,我还实现了一些较小的已接受提案——这里不详细说明,但如果您感兴趣,可以查看相关 issue:
禁止 @bitCast 与指针向量之间的转换(#18936)
允许对枚举使用 @bitCast(#35602 的一部分)
当然,所有这些变更的语义都将在 0.17.0 的发布说明中解释(希望能比我在这里写的更简洁!),并概述建议的迁移步骤。
LLVM 后端性能
最后,我想提一下,这个分支的原始动机——改变 LLVM 后端降级非 ABI 整数类型的方式——在恢复错失的优化方面取得了显著成功。事实上,Zig 编译器本身——尽管内部没有大量使用任意位宽整数!——通过更好的优化获得了约 5% 的性能提升。这意味着您可能在 0.17.0 中会看到一些轻微的运行时性能提升!
感谢阅读,希望对您有所启发。Happy hacking!
I’ve spent the past few weeks working on our new ELF linker which debuted in Zig 0.16.0. At the time of the 0.16.0 release, this linker implementation was in its fairly early stages, and only really supported linking Zig-only code without any external libraries (even libc)—hence why it was (and still is) disabled by default (it can be enabled with -fnew-linker). However, quite a lot of progress has been made since that initial release!
Here’s a nice milestone—as of my latest PR, the new ELF linker is capable of building the self-hosted Zig compiler with LLVM and LLD libraries enabled, a task which requires quite a few features under the hood.
[mlugg@nebula master]$ # Build the Zig compiler using the new linker: [mlugg@nebula master]$ zig build -Dno-lib -Dnew-linker -Denable-llvm [mlugg@nebula master]$ # Use that compiler to build something with LLVM and LLD: [mlugg@nebula master]$ ./zig-out/bin/zig build-exe ~/hello.zig -fllvm -flld [mlugg@nebula master]$ ./hello Hello, World! [mlugg@nebula master]$
Of course, an ELF linker isn’t necessarily the most exciting thing in the world, which is why the headline feature of this new linker is its support for fast incremental compilation. After the recent enhancements, it is now possible (on x86_64 Linux) to perform incremental rebuilds while linking external libraries, C sources, etc—without any additional performance overhead! Here’s a clip of me trying it out on Andrew’s Tetris clone:
A few silly changes to Andrew’s Tetris clone being built in around 30ms each.
过去几周,我一直在开发新的 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!
[mlugg@nebula master]$
当然,ELF 链接器本身并非世界上令人兴奋的事物,因此这个新链接器的旗舰特性是对快速增量编译的支持。经过最近的增强,现在(在 x86_64 Linux 上)可以在链接外部库、C 源码等时执行增量重建——且没有任何额外的性能开销!下面是我在 Andrew 的 Tetris 克隆上试验的片段:
对 Andrew 的 Tetris 克隆进行一些琐碎的修改,每次构建大约需要 30 毫秒。
Oh, and fast incremental rebuilds also work nicely on the Zig compiler itself:
[mlugg@nebula master]$ zig build -Dno-lib -Denable-llvm -fincremental --watch Build Summary: 4/4 steps succeeded install success └─ compile exe zig Debug native success 36s
Build Summary: 4/4 steps succeeded install success └─ compile exe zig Debug native success 244ms
Build Summary: 4/4 steps succeeded install success └─ compile exe zig Debug native success 228ms
Build Summary: 4/4 steps succeeded install success └─ compile exe zig Debug native success 288ms
Build Summary: 4/4 steps succeeded install success └─ compile exe zig Debug native success 283ms
The biggest missing feature of this linker implementation right now is that it still does not yet support generating DWARF debug information for Zig code—that’s definitely my next priority. But even without that support, it’s amazing just how useful instant rebuilds can be, for example in any situation where you’re doing a lot of print debugging.
If you’re using the master branch of Zig and you’re on x86_64 Linux, consider trying out incremental compilation with the new ELF linker if it previously wasn’t working with your project! I expect many codebases to already work great with it, unlocking the ability to rebuild your project in milliseconds. Of course, if you come across any bugs, please do open an issue.
And if you’re currently sticking to tagged releases of Zig, don’t worry—as Andrew mentioned in his last devlog, Zig 0.17.0 is just around the corner, so it won’t be long before you can try this too!
哦,快速增量重建在 Zig 编译器本身上也运行良好:
[mlugg@nebula master]$ zig build -Dno-lib -Denable-llvm -fincremental --watch
Build Summary: 4/4 steps succeeded
install success
└─ compile exe zig Debug native success 36s
Build Summary: 4/4 steps succeeded
install success
└─ compile exe zig Debug native success 244ms
Build Summary: 4/4 steps succeeded
install success
└─ compile exe zig Debug native success 228ms
Build Summary: 4/4 steps succeeded
install success
└─ compile exe zig Debug native success 288ms
Build Summary: 4/4 steps succeeded
install 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 即将到来,所以您很快也能尝试这个功能了!
Big branch just landed: separate the maker process from the configurer process
This devlog entry is essentially a preview of the upcoming release notes, but serves as an advanced notice to those who want to help test out the new features and provide feedback that will guide the Zig project moving forward.
Before, build.zig files plus the build system implementation were all compiled into one bloated process, in Debug mode. After build.zig logic finished constructing a build graph in memory, the “build runner” code executed it.
Now, build.zig files are compiled into a small process (the “configurer”) in debug mode. After this logic finishes constructing a build graph in memory, it is serialized to a binary configuration file. The parent zig build process is aware of this file and caches it for next time. While waiting for all that, it asynchronously compiles the build graph execution process (the “maker”) in release mode. Once the configuration file is available and the maker process is finished compiling, the maker process is executed, passing it the configuration file. The maker process only needs to be compiled once per zig version thanks to the global cache. The maker process then executes the build graph, which is contained within the serialized configuration file.
大分支刚刚合并:将 maker 进程与 configurer 进程分离。
这篇开发日志条目本质上是即将发布的版本说明的预览,但对于那些希望帮助测试新功能并提供反馈以指导 Zig 项目向前发展的人来说,这是一个提前通知。
以前,build.zig 文件以及构建系统实现都被编译到一个臃肿的进程中,在 Debug 模式下运行。在 build.zig 逻辑完成内存中构建图的构造后,“构建运行器”(build runner)代码执行它。
现在,build.zig 文件被编译成一个小的进程(称为“configurer”),在 Debug 模式下运行。在该逻辑完成内存中构建图的构造后,它被序列化为一个二进制配置文件。父进程 zig build 知道这个文件并缓存它以供下次使用。在等待这一切的同时,它异步地以 Release 模式编译构建图执行进程(称为“maker”)。一旦配置文件就绪且 maker 进程编译完成,就会执行 maker 进程,并将配置文件传递给它。得益于全局缓存,maker 进程每个 Zig 版本只需编译一次。然后 maker 进程执行包含在序列化配置文件中的构建图。
The primary motivation of this change was to make zig build faster, in three ways:
Only the user’s build.zig logic will be compiled with each change, rather than the entire build system along with it. This is starting to become more valuable now that we have introduced --watch, --fuzz and --webui. The build system can grow more features without making zig build take longer.
Now the build system can skip rerunning the build.zig logic entirely when it knows nothing will change, for example if you add -freference-trace to your zig build command line, it now avoids re-running your build.zig logic redundantly, using the same configuration as last time.
Now the process that actually executes the build graph is compiled with optimizations enabled.
To demonstrate points 2 and 3, here is the difference between running zig build --help before and after:
Benchmark 1 (34 runs): master/zig build -h measurement mean ± σ min … max outliers delta wall_time 150ms ± 5.52ms 145ms … 165ms 4 (12%) 0% peak_rss 84.8MB ± 275KB 84.2MB … 85.1MB 0 ( 0%) 0% cpu_cycles 593M ± 4.01M 588M … 608M 2 ( 6%) 0% instructions 995M ± 52.5K 995M … 995M 0 ( 0%) 0% cache_references 25.8M ± 165K 25.4M … 26.1M 0 ( 0%) 0% cache_misses 651K ± 20.1K 619K … 697K 0 ( 0%) 0% branch_misses 918K ± 7.44K 906K … 935K 0 ( 0%) 0% Benchmark 2 (348 runs): branch/zig build -h measurement mean ± σ min … max outliers delta wall_time 14.3ms ± 744us 13.2ms … 23.3ms 8 ( 2%) ⚡- 90.4% ± 0.4% peak_rss 78.5MB ± 562KB 77.1MB … 81.4MB 7 ( 2%) ⚡- 7.4% ± 0.2% cpu_cycles 24.1M ± 821K 22.8M … 27.1M 3 ( 1%) ⚡- 95.9% ± 0.1% instructions 43.7M ± 23.8K 43.7M … 43.8M 56 (16%) ⚡- 95.6% ± 0.0% cache_references 1.46M ± 14.6K 1.40M … 1.50M 19 ( 5%) ⚡- 94.3% ± 0.1% cache_misses 142K ± 4.87K 127K … 157K 2 ( 1%) ⚡- 78.1% ± 0.4% branch_misses 126K ± 1.37K 120K … 129K 12 ( 3%) ⚡- 86.3% ± 0.1%
It’s dramatic because before, build.zig logic was being executed with each zig build command, but now, the build system uses the cached, serialized configuration instead.
Aside from performance, I expect third-party tooling such as ZLS to benefit from consuming the serialized configuration file rather than maintaining a fork of the build runner.
此项变更的主要动机是使 zig build 更快,体现在三个方面:
每次变更时,只需重新编译用户的 build.zig 逻辑,而不是连同整个构建系统一起重新编译。随着我们引入 --watch、--fuzz 和 --webui,这一点变得越来越有价值。构建系统可以增加更多功能,而不会使 zig build 耗时更长。
现在,当构建系统知道不会发生任何变化时,可以完全跳过重新运行 build.zig 逻辑。例如,如果您在 zig build 命令行中添加了 -freference-trace,它将避免冗余的重新运行,而直接使用上次相同的配置。
现在,实际执行构建图的进程在启用优化的情况下编译。
为了演示第 2 点和第 3 点,以下是运行 zig build --help 之前和之后的差异:
Benchmark 1 (34 runs): master/zig build -h
wall_time 150ms ± 5.52ms 145ms … 165ms 4 (12%) 0%
peak_rss 84.8MB ± 275KB 84.2MB … 85.1MB 0 ( 0%) 0%
cpu_cycles 593M ± 4.01M 588M … 608M 2 ( 6%) 0%
instructions 995M ± 52.5K 995M … 995M 0 ( 0%) 0%
cache_references 25.8M ± 165K 25.4M … 26.1M 0 ( 0%) 0%
cache_misses 651K ± 20.1K 619K … 697K 0 ( 0%) 0%
branch_misses 918K ± 7.44K 906K … 935K 0 ( 0%) 0%
Benchmark 2 (348 runs): branch/zig build -h
wall_time 14.3ms ± 744us 13.2ms … 23.3ms 8 ( 2%) ⚡- 90.4% ± 0.4%
peak_rss 78.5MB ± 562KB 77.1MB … 81.4MB 7 ( 2%) ⚡- 7.4% ± 0.2%
cpu_cycles 24.1M ± 821K 22.8M … 27.1M 3 ( 1%) ⚡- 95.9% ± 0.1%
instructions 43.7M ± 23.8K 43.7M … 43.8M 56 (16%) ⚡- 95.6% ± 0.0%
cache_references 1.46M ± 14.6K 1.40M … 1.50M 19 ( 5%) ⚡- 94.3% ± 0.1%
cache_misses 142K ± 4.87K 127K … 157K 2 ( 1%) ⚡- 78.1% ± 0.4%
branch_misses 126K ± 1.37K 120K … 129K 12 ( 3%) ⚡- 86.3% ± 0.1%
效果显著,因为以前每个 zig build 命令都会执行 build.zig 逻辑,但现在构建系统使用缓存的序列化配置代替。
除了性能之外,我预计像 ZLS 这样的第三方工具也可以从消费序列化配置文件中受益,而无需维护构建运行器的分支。
This changeset heavily reworks the internal mechanism of the zig build system, however, it is mostly non-breaking from an API perspective, with the exceptions noted in the PR linked above.
For most people I’m guessing this is the main breaking change they’ll hit:
if (b.args) |args| { run_cmd.addArgs(args); }
⬇️
run_cmd.addPassthruArgs();
This removes a capability from build scripts since they can no longer observe those arguments. In exchange, it means that when changing those arguments, build scripts no longer must be rebuilt from source.
If you’re someone who wants to influence the direction of Zig, this is a good time to upgrade your projects to the development version and try out these changes. We’ll be releasing 0.17.0 within a couple weeks from now. However, if you don’t have time, and you find out that 0.17.0 broke your build, don’t worry, there will be plenty of opportunity to get fixes in for the 0.17.1 tag as well.
此变更集大量重写了 zig 构建系统的内部机制,但从 API 角度来看,它基本上是非破坏性的,例外情况已在上面链接的 PR 中注明。
对于大多数人来说,我猜他们遇到的主要破坏性变更会是:
if (b.args) |args| {
run_cmd.addArgs(args);
}
⬇️
run_cmd.addPassthruArgs();
这移除了构建脚本的一项能力,因为脚本现在无法再观察这些参数。作为交换,当这些参数变更时,构建脚本不再需要从源码重新构建。
如果您希望影响 Zig 的发展方向,现在是升级项目到开发版本并尝试这些变更的好时机。我们将在未来几周内发布 0.17.0。不过,如果您没有时间,并且发现 0.17.0 破坏了您的构建,也别担心,我们还有充足的机会在 0.17.1 标签中修复问题。
I’ve been spending a bit of time working on personal projects after merging my type resolution changes last month, but I did find the time recently to make some improvements to the LLVM codegen backend. This involved a few different enhancements with various goals, but one nice user-facing change was that I managed to get incremental compilation working with the LLVM backend.
Sadly this can’t do anything to speed up the dreaded LLVM Emit Object: that time is entirely down to LLVM. However, what incremental compilation does help with is minimizing the time spent in the actual Zig compiler code, which means that if your code has compile errors (so “LLVM Emit Object” will be skipped), you’ll usually get those errors very quickly. (Of course, it does still give you a slight speed-up in successful builds too.)
This support is available in master branch builds right now, and will be in the 0.16.0 release (which we’ll be tagging very soon).
For anyone who still hasn’t tried it, especially if you’re using Zig’s master branch, please do try out incremental compilation by passing -fincremental --watch to zig build! The Zig core team have benefited from incremental compilation in our workflows for a good year now, and we’re also hearing good things from users. The feature is relatively stable at this point, and people are often surprised how much time they can save just by getting up-to-date compile errors in milliseconds rather than seconds.
I haven’t really personally used incremental compilation with the LLVM backend, but all of the incremental test coverage in CI is now enabled for the LLVM backend, and I’ve had positive feedback from users, so it’s definitely worth giving a shot. As always, if you encounter bugs in incremental compilation, please report them if you can!
Thank you, and I hope you find this useful :)
上个月合并了类型解析变更后,我花了一些时间在个人项目上,但最近确实抽出时间对 LLVM 代码生成后端进行了一些改进。这涉及到几项不同目标的增强,但一个很好的用户可见变化是,我成功让 LLVM 后端支持了增量编译。
遗憾的是,这无法加速令人畏惧的“LLVM Emit Object”阶段:该时间完全取决于 LLVM。不过,增量编译有助于最小化在实际 Zig 编译器代码中花费的时间,这意味着如果您的代码有编译错误(因此会跳过“LLVM Emit Object”),您通常能非常快速地得到这些错误。(当然,在成功构建时,它也会带来轻微加速。)
此支持现已存在于 master 分支构建中,并将包含在 0.16.0 版本中(我们很快会打标签)。
对于尚未尝试过的人,特别是如果您使用 Zig 的 master 分支,请务必通过向 zig build 传递 -fincremental --watch 来体验增量编译!Zig 核心团队在我们的工作流程中受益于增量编译已有一年之久,我们也听到了用户的积极反馈。该功能目前比较稳定,人们常惊讶于只需几毫秒而不是几秒就能获得最新的编译错误信息,从而节省大量时间。
我个人尚未真正在 LLVM 后端上使用增量编译,但 CI 中所有增量测试覆盖现在都已为 LLVM 后端启用,并且我收到了用户的积极反馈,所以绝对值得一试。一如既往,如果您在增量编译中遇到 bug,请尽可能报告它们!
谢谢,希望这对您有用 :)
Today, I merged a 30,000 line PR after two (arguably three) months of work. The goal of this branch was to rework the Zig compiler’s internal type resolution logic to a more logical and straightforward design. It’s a quite exciting change for me personally, because it allowed me to clean up a bunch of the compiler guts, but it also has some nice user-facing changes which you might be interested in!
For one thing, the Zig compiler is now lazier about analyzing the fields of types: if the type is never initialized, then there’s no need for Zig to care what that type “looks like”. This is important when you have a type which doubles as a namespace, a common pattern in modern Zig. For instance, when using std.Io.Writer, you don’t want the compiler to also pull in a bunch of code in std.Io! Here’s a straightforward example:
const Foo = struct {
bad_field: @compileError("i am an evil field, muahaha"),
const something = 123;
};
comptime {
_ = Foo.something; // Foo only used as a namespace
}
Previously, this code emitted a compile error. Now, it compiles just fine, because Zig never actually looks at the @compileError call.
今天,经过两个(或者可以说是三个)月的工作,我合并了一个 30,000 行的 PR。这个分支的目标是重新设计 Zig 编译器内部的类型解析逻辑,使其更符合逻辑、更直接。对我个人来说,这是一个相当令人兴奋的变更,因为它让我清理了许多编译器内部结构,但也带来了一些您可能会感兴趣的用户可见变化!
首先,Zig 编译器现在在分析类型的字段时变得更“懒惰”:如果类型从未被初始化,那么 Zig 就无需关心该类型“长什么样”。这在类型同时充当命名空间时非常重要,这是现代 Zig 中的常见模式。例如,当使用 std.Io.Writer 时,您不希望编译器也拉入 std.Io 中的一堆代码!这里有一个简单的例子:
const Foo = struct {
bad_field: @compileError("i am an evil field, muahaha"),
const something = 123;
};
comptime {
_ = Foo.something; // `Foo` only used as a namespace
}
以前,这段代码会引发编译错误。现在,它编译顺利,因为 Zig 实际上从未查看 @compileError 调用。
Another improvement we’ve made is in the “dependency loop” experience. Anyone who has encountered a dependency loop compile error in Zig before knows that the error messages for them are entirely unhelpful—but that’s now changed! If you encounter one (which is also a bit less likely now than it used to be), you’ll get a detailed error message telling you exactly where the dependency loop comes from. Check it out:
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
Of course, dependency loops can get much more complicated than this, but in every case I’ve tested, the error message has had enough information to easily see what’s going on.
Additionally, this PR made big improvements to the Zig compiler’s “incremental compilation” feature. The short version is that it fixed a huge amount of known bugs, but in particular, “over-analysis” problems (where an incremental update did more work than should be necessary, sometimes by a big margin) should finally be all but eliminated—making incremental compilation significantly faster in many cases! If you’ve not already, consider trying out incremental compilation: it really is a lovely development experience. This is for sure the improvement which excites me the most, and a large part of what motivated this change to begin with.
There are a bunch more changes that come with this PR—dozens of bugfixes, some small language changes (mostly fairly niche), and compiler performance improvements. It’s far too much to list here, but if you’re interested in reading more about it, you can take a look at the PR on Codeberg—and of course, if you encounter any bugs, please do open an issue. Happy hacking!
我们做出的另一项改进是关于“依赖循环”的体验。任何曾经在 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
当然,依赖循环可能比这复杂得多,但在每个我测试过的案例中,错误信息都提供了足够的信息来轻松了解情况。
此外,这个 PR 还对 Zig 编译器的“增量编译”功能做出了巨大改进。简而言之,它修复了大量已知 bug,但特别是“过度分析”(over-analysis)问题——增量更新做了比必要更多的工作,有时甚至是大幅超出——应该几乎完全消除了,这使得增量编译在许多情况下显著更快!如果您还没有尝试过,请考虑试用增量编译:它确实是一种令人愉悦的开发体验。这无疑是最令我兴奋的改进,也是最初推动此项变更的重要原因。
这个 PR 还带来了许多其他变更——数十个 bug 修复、一些小的语言变化(大多相当小众)以及编译器性能改进。这里无法一一列举,但如果您有兴趣了解更多,可以查看 Codeberg 上的 PR——当然,如果您遇到任何 bug,请提交 issue。Happy hacking!
As we approach the end of the 0.16.0 release cycle, Jacob has been hard at work, bringing std.Io.Evented up to speed with all the latest API changes:
io_uring implementation
Grand Central Dispatch implementation
Both of these are based on userspace stack switching, sometimes called “fibers”, “stackful coroutines”, or “green threads”.
They are now available to tinker with, by constructing one’s application using std.Io.Evented. They should be considered experimental because there is important followup work to be done before they can be used reliably and robustly:
better error handling
remove the logging
diagnose the unexpected performance degradation when using IoMode.evented for the compiler
a couple functions still unimplemented
more test coverage is needed
builtin function to tell you the maximum stack size of a given function to make these implementations practical to use when overcommit is off.
With those caveats in mind, it seems we are indeed reaching the Promised Land, where Zig code can have Io implementations effortlessly swapped out:
const std = @import("std");
pub fn main(init: std.process.Init.Minimal) !void { var debug_allocator: std.heap.DebugAllocator(.{}) = .init; const gpa = debug_allocator.allocator();
var threaded: std.Io.Threaded = .init(gpa, .{ .argv0 = .init(init.args), .environ = init.environ, }); defer threaded.deinit(); const io = threaded.io();
return app(io); }
fn app(io: std.Io) !void { try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n"); }
$ strace ./hello_threaded execve("./hello_threaded", ["./hello_threaded"], 0x7ffc1da88b20 /* 98 vars */) = 0 ...
Swapping out only the I/O implementation:
const std = @import("std");
pub fn main(init: std.process.Init.Minimal) !void { var debug_allocator: std.heap.DebugAllocator(.{}) = .init; const gpa = debug_allocator.allocator();
var evented: std.Io.Evented = undefined; try evented.init(gpa, .{ .argv0 = .init(init.args), .environ = init.environ, .backing_allocator_needs_mutex = false, }); defer evented.deinit(); const io = evented.io();
return app(io); }
fn app(io: std.Io) !void { try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n"); }
Key point here being that the app function is identical between those two snippets.
Moving beyond Hello World, the Zig compiler itself works fine using std.Io.Evented, both with io_uring and with GCD, but as mentioned above, there is a not-yet-diagnosed performance degradation when doing so.
Happy hacking,
Andrew
随着 0.16.0 发布周期接近尾声,Jacob 一直在努力工作,使 std.Io.Evented 跟上所有最新的 API 变更:
io_uring 实现
Grand Central Dispatch 实现
这两者都基于用户态栈切换,有时称为“纤程”(fibers)、“有栈协程”(stackful coroutines)或“绿色线程”(green threads)。
它们现在可以通过使用 std.Io.Evented 构建应用来进行试验。它们应被视为实验性的,因为在能够可靠稳健地使用之前,还有一些重要的后续工作需要完成:
更好的错误处理
移除日志记录
诊断使用 IoMode.evented 编译时出现的意外性能下降
有几个函数尚未实现
需要更多测试覆盖
需要一个内置函数来告知给定函数的最大栈大小,以便在关闭 overcommit 时使这些实现变得实用。
考虑到这些注意事项,我们似乎确实正在接近“应许之地”,Zig 代码可以轻松地切换 I/O 实现:
const std = @import("std");
pub fn main(init: std.process.Init.Minimal) !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
const gpa = debug_allocator.allocator();
var threaded: std.Io.Threaded = .init(gpa, .{
.argv0 = .init(init.args),
.environ = init.environ,
});
defer threaded.deinit();
const io = threaded.io();
return app(io);
}
fn app(io: std.Io) !void {
try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n");
}
只需切换 I/O 实现:
const std = @import("std");
pub fn main(init: std.process.Init.Minimal) !void {
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
const gpa = debug_allocator.allocator();
var evented: std.Io.Evented = undefined;
try evented.init(gpa, .{
.argv0 = .init(init.args),
.environ = init.environ,
.backing_allocator_needs_mutex = false,
});
defer evented.deinit();
const io = evented.io();
return app(io);
}
fn app(io: std.Io) !void {
try std.Io.File.stdout().writeStreamingAll(io, "Hello, World!\n");
}
关键点在于,app 函数在这两个片段中是相同的。
超越“Hello World”,Zig 编译器本身在使用 std.Io.Evented(无论是 io_uring 还是 GCD)时也能正常工作,但如上所述,这样做时存在尚未诊断的性能下降。
Happy hacking,
Andrew
If you have a Zig project with dependencies, two big changes just landed which I think you will be interested to learn about.
Fetched packages are now stored locally in the zig-pkg directory of the project root (next to your build.zig file).
For example here are a few results from awebo after running zig build:
$ du -sh zig-pkg/* 13M freetype-2.14.1-alzUkTyBqgBwke4Jsot997WYSpl207Ij9oO-2QOvGrOi 20K opus-0.0.2-vuF-cMAkAADVsm707MYCtPmqmRs0gzg84Sz0qGbb5E3w 4.3M pulseaudio-16.1.1-9-mk_62MZkNwBaFwiZ7ZVrYRIf_3dTqqJR5PbMRCJzSuLw 5.2M uucode-0.1.0-ZZjBPvtWUACf5dqD_f9I37VGFsN24436CuceC5pTJ25n 728K vaxis-0.5.1-BWNV_AxECQCj3p4Hcv4U3Yo1WMUJ7Z2FUj0UkpuJGxQQ
It is highly recommended to add this directory to the project-local source control ignore file (e.g. .gitignore). However, by being outside of .zig-cache, it provides the possibility of distributing self-contained source tarballs, which contain all dependencies and therefore can be used to build offline, or for archival purposes.
Meanwhile, an additional copy of the dependency is cached globally. After filtering out all the unused files based on the paths filter, the contents are recompressed:
$ du -sh ~/.cache/zig/p/* 2.4M freetype-2.14.1-alzUkTyBqgBwke4Jsot997WYSpl207Ij9oO-2QOvGrOi.tar.gz 4.0K opus-0.0.2-vuF-cMAkAADVsm707MYCtPmqmRs0gzg84Sz0qGbb5E3w.tar.gz 636K pulseaudio-16.1.1-9-mk_62MZkNwBaFwiZ7ZVrYRIf_3dTqqJR5PbMRCJzSuLw.tar.gz 880K uucode-0.1.0-ZZjBPvtWUACf5dqD_f9I37VGFsN24436CuceC5pTJ25n.tar.gz 120K vaxis-0.5.1-BWNV_BFECQBbXeTeFd48uTJRjD5a-KD6kPuKanzzVB01.tar.gz
The motivation for this change is to make it easier to tinker. Go ahead and edit those files, see what happens. Swap out your package directory with a git clone. Grep your dependencies all together. Configure your IDE to auto-complete based on the zig-pkg directory. Run baobab on your dependency tree. Furthermore, by having the global cache have compressed files instead makes it easier to share that cached data between computers. In the future, it is planned to support peer-to-peer torrenting of dependency trees. By recompressing packages into a canonical form, this will allow peers to share Zig packages with minimal bandwidth. I love this idea because it simultaneously provides resilience to network outages, as well as a popularity contest. Find out which open source packages are popular based on number of seeders!
如果您的 Zig 项目有依赖项,刚刚合并了两项重大变更,我想您会感兴趣。
获取的包现在存储在项目根目录下的 zig-pkg 目录中(位于您的 build.zig 文件旁边)。
例如,以下是在运行 zig build 后,awebo 项目中的一些结果:
$ du -sh zig-pkg/*
13M freetype-2.14.1-alzUkTyBqgBwke4Jsot997WYSpl207Ij9oO-2QOvGrOi
20K opus-0.0.2-vuF-cMAkAADVsm707MYCtPmqmRs0gzg84Sz0qGbb5E3w
4.3M pulseaudio-16.1.1-9-mk_62MZkNwBaFwiZ7ZVrYRIf_3dTqqJR5PbMRCJzSuLw
5.2M uucode-0.1.0-ZZjBPvtWUACf5dqD_f9I37VGFsN24436CuceC5pTJ25n
728K vaxis-0.5.1-BWNV_AxECQCj3p4Hcv4U3Yo1WMUJ7Z2FUj0UkpuJGxQQ
强烈建议将此目录添加到项目本地的版本控制忽略文件(例如 .gitignore)中。然而,由于它位于 .zig-cache 之外,这提供了分发自包含源码压缩包的可能性,这些压缩包包含所有依赖项,因此可用于离线构建或归档目的。
同时,依赖项的另一个副本会全局缓存。在基于路径过滤器过滤掉所有未使用的文件后,内容会被重新压缩:
$ du -sh ~/.cache/zig/p/*
2.4M freetype-2.14.1-alzUkTyBqgBwke4Jsot997WYSpl207Ij9oO-2QOvGrOi.tar.gz
4.0K opus-0.0.2-vuF-cMAkAADVsm707MYCtPmqmRs0gzg84Sz0qGbb5E3w.tar.gz
636K pulseaudio-16.1.1-9-mk_62MZkNwBaFwiZ7ZVrYRIf_3dTqqJR5PbMRCJzSuLw.tar.gz
880K uucode-0.1.0-ZZjBPvtWUACf5dqD_f9I37VGFsN24436CuceC5pTJ25n.tar.gz
120K vaxis-0.5.1-BWNV_BFECQBbXeTeFd48uTJRjD5a-KD6kPuKanzzVB01.tar.gz
此项变更的动机是使试验更容易。直接编辑这些文件,看看会发生什么。用 git clone 替换您的包目录。一次性 grep 所有依赖项。配置您的 IDE 基于 zig-pkg 目录进行自动补全。在您的依赖树上运行 baobab。此外,通过将全局缓存设为压缩文件,可以更轻松地在计算机之间共享缓存数据。未来,计划支持依赖树的点对点 torrent 分发。通过将包重新压缩为规范形式,这将允许对等节点以最小带宽共享 Zig 包。我喜欢这个想法,因为它同时提供了对网络中断的韧性,以及一个“人气竞赛”——根据种子数找出哪些开源包是流行的!
The second change here is the addition of the --fork flag to zig build.
In retrospect, it seems so obvious, I don’t know why I didn’t think of it since the beginning. It looks like this:
zig build --fork=[path]
This is a project override option. Given a path to a source checkout of a project, all packages matching that project across the entire dependency tree will be overridden.
Thanks to the fact that package content hashes include name and fingerprint, this resolves before the package is potentially fetched.
This is an easy way to temporarily use one or more forks which are in entirely separate directories. You can iterate on your entire dependency tree until everything is working, while using comfortably the development environment and source control of the dependency projects.
The fact that it is a CLI flag makes it appropriately ephemeral. The moment you drop the flags, you’re back to using your pristine, fetched dependency tree.
If the project does not match, an error occurs, preventing confusion:
$ zig build --fork=/home/andy/dev/mime error: fork /home/andy/dev/mime matched no mime packages $
If the project does match, you get a reminder that you are using a fork, preventing confusion:
$ zig build --fork=/home/andy/dev/dvui info: fork /home/andy/dev/dvui matched 1 (dvui) packages ...
This functionality is intended to enhance the workflow of dealing with ecosystem breakage. I already tried it a bit and found it to be quite pleasant to work with. The new workflow goes like this:
Fail to build from source due to ecosystem breakage.
Tinker with --fork until your project works again. During this time you can use the actual upstream source control, test suite, zig build test --watch -fincremental, etc.
Now you have a new option: be selfish and just keep working on your own stuff, or you can proceed to submit your patches upstream.
…and you can probably skip the step where you switch your build.zig.zon to your fork unless you expect upstream to take a long time to merge your fixes.
第二项变更是为 zig build 添加了 --fork 标志。
回想起来,这似乎如此明显,我不知道为什么从一开始没想到。它看起来是这样的:
zig build --fork=[路径]
这是一个项目覆盖选项。给定一个项目的源码检出路径,整个依赖树中匹配该项目的所有包都将被覆盖。
由于包内容哈希包含名称和指纹,这在包可能被获取之前就会解析。
这是一种简单的方法,可以临时使用位于完全独立目录中的一个或多个分支。您可以在整个依赖树上迭代,直到一切正常工作,同时舒适地使用依赖项目的开发环境和版本控制。
它是一个 CLI 标志这一事实使其具有适当的临时性。一旦您去掉标志,就回到使用原始的、已获取的依赖树。
如果项目不匹配,会报错,防止混淆:
$ zig build --fork=/home/andy/dev/mime
error: fork /home/andy/dev/mime matched no mime packages
$
如果项目匹配,您会收到一条提醒,告知您正在使用分支,防止混淆:
$ zig build --fork=/home/andy/dev/dvui
info: fork /home/andy/dev/dvui matched 1 (dvui) packages
...
此功能旨在增强处理生态系统破坏的工作流程。我已经尝试了一下,发现使用起来相当愉快。新的工作流程如下:
由于生态系统破坏,从源码构建失败。
使用 --fork 进行试验,直到项目再次正常工作。在此期间,您可以使用上游实际的版本控制、测试套件、zig build test --watch -fincremental 等。
现在您有一个新选择:自私一点,继续只做自己的事情,或者继续向上游提交补丁。
……而且您可能可以跳过将 build.zig.zon 切换到您的分支的步骤,除非您预期上游需要很长时间才能合并您的修复。
The Windows operating system provides a large ABI surface area for doing things in the kernel. However, not all ABIs are created equally. As Casey Muratori points out in his lecture, The Only Unbreakable Law, the organizational structure of software development teams has a direct impact on the structure of the software they produce.
The DLLs on Windows are organized into a heirarchy, with some of the APIs being high-level wrappers around lower-level ones. For example, whenever you call functions of kernel32.dll, ultimately, the actual work is done by ntdll.dll. You can observe this directly by using ProcMon.exe and examining stack traces.
What we’ve learned empirically is that the ntdll APIs are generally well-engineered, reasonable, and powerful, but the kernel32 wrappers introduce unnecessary heap allocations, additional failure modes, unintentional CPU usage, and bloat.
This is why the Zig standard library policy is to Prefer the Native API over Win32. We’re not quite there yet - we have plenty of calls into kernel32 remaining - but we’ve taken great strides recently. I’ll give you two examples.
Windows 操作系统提供了庞大的 ABI 表面积用于在内核中执行操作。然而,并非所有 ABI 都生而平等。正如 Casey Muratori 在其讲座《唯一不可打破的法则》中所指出的,软件开发团队的组织结构直接影响其生产的软件结构。
Windows 上的 DLL 是按层次组织的,有些 API 是底层 API 的高级包装器。例如,每当您调用 kernel32.dll 的函数时,实际工作最终由 ntdll.dll 完成。您可以通过使用 ProcMon.exe 并检查堆栈跟踪来直接观察到这一点。
我们根据经验学到的是,ntdll API 通常设计精良、合理且强大,但 kernel32 包装器引入了不必要的堆分配、额外的故障模式、无意的 CPU 使用和膨胀。
这就是为什么 Zig 标准库的政策是“偏好原生 API 而非 Win32”。我们尚未完全达到目标——仍有大量对 kernel32 的调用——但我们最近取得了重大进展。我将给您举两个例子。
According to the official documentation, Windows does not have a straightforward way to get random bytes.
Many projects including Chromium, boringssl, Firefox, and Rust call SystemFunction036 from advapi32.dll because it worked on versions older than Windows 8.
Unfortunately, starting with Windows 8, the first time you call this function, it dynamically loads bcryptprimitives.dll and calls ProcessPrng. If loading the DLL fails (for example due to an overloaded system, which we have observed on Zig CI several times), it returns error 38 (from a function that has void return type and is documented to never fail).
The first thing ProcessPrng does is heap allocate a small, constant number of bytes. If this fails it returns NO_MEMORY in a BOOL (documented behavior is to never fail, and always return TRUE).
bcryptprimitives.dll apparently also runs a test suite every time you load it.
All that ProcessPrng is really doing is NtOpenFile on "\Device\CNG" and reading 48 bytes with NtDeviceIoControlFile to get a seed, and then initializing a per-CPU AES-based CSPRNG.
So the dependency on bcryptprimitives.dll and advapi32.dll can both be avoided, and the nondeterministic failure and latencies on first RNG read can also be avoided.
根据官方文档,Windows 没有直接获取随机字节的方法。
许多项目,包括 Chromium、boringssl、Firefox 和 Rust,都调用 advapi32.dll 中的 SystemFunction036,因为它在 Windows 8 之前的版本上工作。
不幸的是,从 Windows 8 开始,第一次调用此函数时,它会动态加载 bcryptprimitives.dll 并调用 ProcessPrng。如果加载 DLL 失败(例如由于系统过载,我们在 Zig CI 上多次观察到),它返回错误 38(来自一个具有 void 返回类型且文档中声称从不失败的函数)。
ProcessPrng 做的第一件事是堆分配少量固定字节。如果失败,它会在 BOOL 中返回 NO_MEMORY(文档行为是永不失败,始终返回 TRUE)。
bcryptprimitives.dll 在每次加载时显然还会运行一个测试套件。
ProcessPrng 实际做的所有事情,就是在 "\\Device\\CNG" 上调用 NtOpenFile,并通过 NtDeviceIoControlFile 读取 48 字节以获取种子,然后初始化一个基于每个 CPU 的 AES 加密安全伪随机数生成器(CSPRNG)。
因此,对 bcryptprimitives.dll 和 advapi32.dll 的依赖都可以避免,并且首次 RNG 读取时的非确定性故障和延迟也可以避免。
ReadFile looks like this:
pub extern "kernel32" fn ReadFile( hFile: HANDLE, lpBuffer: LPVOID, nNumberOfBytesToRead: DWORD, lpNumberOfBytesRead: ?*DWORD, lpOverlapped: ?*OVERLAPPED, ) callconv(.winapi) BOOL;
NtReadFile looks like this:
pub extern "ntdll" fn NtReadFile( FileHandle: HANDLE, Event: ?HANDLE, ApcRoutine: ?*const IO_APC_ROUTINE, ApcContext: ?*anyopaque, IoStatusBlock: *IO_STATUS_BLOCK, Buffer: *anyopaque, Length: ULONG, ByteOffset: ?*const LARGE_INTEGER, Key: ?*const ULONG, ) callconv(.winapi) NTSTATUS;
As a reminder, the above function is implemented by calling the below function.
Already we can see some nice things about using the lower level API. For instance, the real API simply gives us the error code as the return value, while the kernel32 wrapper hides the status code somewhere, returns a BOOL and then requires you to call GetLastError to find out what went wrong. Imagine! Returning a value from a function 🌈
Furthermore, OVERLAPPED is a fake type. The Windows kernel doesn’t actually know or care about it at all! The actual primitives here are events, APCs, and IO_STATUS_BLOCK.
If you have a synchronous file handle, then Event and ApcRoutine must be null. You get the answer in the IO_STATUS_BLOCK immediately. If you pass an APC routine here then some old bitrotted 32-bit code runs and you get garbage results.
On the other hand if you have an asynchronous file handle, then you need to either use an Event or an ApcRoutine. kernel32.dll uses events, which means that it’s doing extra, unnecessary resource allocation and management just to read from a file. Instead, Zig now passes an APC routine and then calls NtDelayExecution. This integrates seamlessly with cancelation, making it possible to cancel tasks while they perform file I/O, regardless of whether the file was opened in synchronous mode or asynchronous mode.
For a deeper dive into this topic, please refer to this issue:
Windows: Prefer the Native API over Win32
ReadFile 看起来像这样:
pub extern "kernel32" fn ReadFile(
hFile: HANDLE,
lpBuffer: LPVOID,
nNumberOfBytesToRead: DWORD,
lpNumberOfBytesRead: ?*DWORD,
lpOverlapped: ?*OVERLAPPED,
) callconv(.winapi) BOOL;
NtReadFile 看起来像这样:
pub extern "ntdll" fn NtReadFile(
FileHandle: HANDLE,
Event: ?HANDLE,
ApcRoutine: ?*const IO_APC_ROUTINE,
ApcContext: ?*anyopaque,
IoStatusBlock: *IO_STATUS_BLOCK,
Buffer: *anyopaque,
Length: ULONG,
ByteOffset: ?*const LARGE_INTEGER,
Key: ?*const ULONG,
) callconv(.winapi) NTSTATUS;
提醒一下,上面的函数是通过调用下面的函数实现的。
我们已经可以看到使用底层 API 的一些好处。例如,真正的 API 简单地将错误代码作为返回值返回,而 kernel32 包装器则将状态码隐藏在某处,返回一个 BOOL,然后要求您调用 GetLastError 来找出问题所在。想象一下!从函数返回值 🌈
此外,OVERLAPPED 是一个假类型。Windows 内核实际上根本不知道也不关心它!这里真正的原语是事件、APC 和 IO_STATUS_BLOCK。
如果您有一个同步文件句柄,那么 Event 和 ApcRoutine 必须为 null。您会立即在 IO_STATUS_BLOCK 中得到结果。如果您在此处传递了 APC 例程,那么一些旧的有缺陷的 32 位代码会运行,您会得到垃圾结果。
另一方面,如果您有一个异步文件句柄,那么您需要使用一个 Event 或 ApcRoutine。kernel32.dll 使用事件,这意味着它为了从文件读取而执行额外不必要的资源分配和管理。相反,Zig 现在传递一个 APC 例程,然后调用 NtDelayExecution。这能与取消操作无缝集成,使得在任务执行文件 I/O 时取消它们成为可能,无论文件是以同步模式还是异步模式打开的。
如需深入了解此主题,请参阅此 issue:
Windows: Prefer the Native API over Win32
Over the past month or so, several enterprising contributors have taken an interest in the zig libc subproject. The idea here is to incrementally delete redundant code, by providing libc functions as Zig standard library wrappers rather than as vendored C source files. In many cases, these functions are one-to-one mappings, such as memcpy or atan2, or trivially wrap a generic function, like strnlen:
fn strnlen(str: [*:0]const c_char, max: usize) callconv(.c) usize { return std.mem.findScalar(u8, @ptrCast(str[0..max]), 0) orelse max; }
So far, roughly 250 C source files have been deleted from the Zig repository, with 2032 remaining.
With each function that makes the transition, Zig gains independence from third party projects and from the C programming language, compilation speed improves, Zig’s installation size is simplified and reduced, and user applications which statically link libc enjoy reduced binary size.
Additionally, a recent enhancement now makes zig libc share the Zig Compilation Unit with other Zig code rather than being a separate static archive, linked together later. This is one of the advantages of Zig having an integrated compiler and linker. When the exported libc functions share the ZCU, redundant code is eliminated because functions can be optimized together. It’s kind of like enabling LTO (Link-Time Optimization) across the libc boundary, except it’s done properly in the frontend instead of too late, in the linker.
Furthermore, when this work is combined with the recent std.Io changes, there is potential for users to seamlessly control how libc performs I/O - for example forcing all calls to read and write to participate in an io_uring event loop, even though that code was not written with such use case in mind. Or, resource leak detection could be enabled for third-party C code. For now this is only a vaporware idea which has not been experimented with, but the idea intrigues me.
Big thanks to Szabolcs Nagy for libc-test. This project has been a huge help in making sure that we don’t regress any math functions.
As a reminder to our users, now that Zig is transitioning to being the static libc provider, if you encounter issues with the musl, mingw-w64, or wasi-libc libc functionality provided by Zig, please file bug reports in Zig first so we don’t annoy maintainers for bugs that are in Zig, and no longer vendored by independent libc implementation projects.
在过去一个月左右的时间里,几位有进取心的贡献者对 zig libc 子项目产生了兴趣。这里的想法是通过将 libc 函数作为 Zig 标准库包装器而不是作为贩卖的 C 源文件提供,来逐步删除冗余代码。在许多情况下,这些函数是一对一的映射,例如 memcpy 或 atan2,或者简单地包装一个通用函数,如 strnlen:
fn strnlen(str: [*:0]const c_char, max: usize) callconv(.c) usize {
return std.mem.findScalar(u8, @ptrCast(str[0..max]), 0) orelse max;
}
到目前为止,大约 250 个 C 源文件已从 Zig 仓库中删除,剩余 2032 个。
随着每个函数完成过渡,Zig 获得了对第三方项目和 C 编程语言的独立性,编译速度得到提升,Zig 的安装体积得以简化缩小,而静态链接 libc 的用户应用也享受到了更小的二进制体积。
此外,最近的一项增强使 zig libc 现在与其他 Zig 代码共享 Zig 编译单元(ZCU),而不是作为一个单独的静态归档文件、之后才链接在一起。这是 Zig 拥有集成编译器和链接器的优势之一。当导出的 libc 函数共享 ZCU 时,冗余代码被消除,因为函数可以一起优化。这有点像跨越 libc 边界启用了 LTO,只不过是在前端正确完成的,而不是在链接器中进行,那时已经太晚了。
此外,当这项工作与最近的 std.Io 变更相结合时,用户有潜力无缝控制 libc 如何执行 I/O——例如,强制所有对 read 和 write 的调用参与 io_uring 事件循环,即使这些代码编写时并未考虑这种用例。或者,可以为第三方 C 代码启用资源泄漏检测。目前这只是一个尚未经过实验的雾件想法,但这个想法让我很感兴趣。
非常感谢 Szabolcs Nagy 提供的 libc-test。这个项目在帮助我们确保不退化任何数学函数方面发挥了巨大作用。
提醒我们的用户,既然 Zig 正在过渡为静态 libc 提供者,如果您遇到由 Zig 提供的 musl、mingw-w64 或 wasi-libc 的 libc 功能问题,请先在 Zig 中提交 bug 报告,这样我们就不会因 Zig 中的 bug 去打扰独立 libc 实现项目的维护者。