0.16.0
2026/4/13,0.16.0 发布,历时 8 个月,有 244 位贡献者参与,一共进行了 1183 次提交!
如果要用一句话概括这个版本,那就是:0.16.0 把上一轮预告过的大量基础设施重构真正落地了。
0.15.x 还在为 std.Io、增量编译和新的工具链架构铺路,到了 0.16.0,这些方向已经进入可以大规模体验的阶段:I/O 统一为接口、main 可以直接拿到 io 和 gpa、增量编译进一步可用、新 ELF linker 开始接入默认流程,同时语言层也继续清理历史设计。
目标支持
0.16.0 在目标支持上的一个重要变化,是 Zig 对“哪些平台值得持续投入工程质量”这件事变得更明确了。
比较值得注意的点有:
aarch64-freebsd、aarch64-netbsd、loongarch64-linux、powerpc64le-linux、s390x-linux、x86_64-freebsd、x86_64-netbsd、x86_64-openbsd这些目标现在都会在 Zig 的 CI 中原生测试(OSUOSL 提供了 AArch64 和 Power ISA 硬件,IBM 提供了 z/Architecture 硬件)- 新增
aarch64-maccatalyst与x86_64-maccatalyst的交叉编译支持。这部分支持几乎是“免费”得到的,因为 Zig 自带的libSystem.tbd本来就提供了对应符号 - 新增
loongarch32-linux的初始支持,不过当前仍不支持 libc,LLVM 也仍把这个目标的 ABI 视为不稳定,只有走std.os.linux的纯 syscall 程序才能跑 - Alpha、KVX、MicroBlaze、OpenRISC、PA-RISC、SuperH 等架构加入了基础支持。这些目标当前要求使用 Zig 的 C 后端(配合 GCC),或者外部的 LLVM/Clang fork
- Oracle Solaris、IBM AIX、z/OS 支持被移除——这些专有系统取系统头文件本身就不容易,影响了贡献的可审计性;
illumos是开源的 OpenSolaris fork,不受影响,仍然保留支持 - 栈回溯支持进一步扩大,几乎所有主流目标在崩溃或使用
DebugAllocator时都能得到更可靠的 stack trace - 一批主要影响弱内存序架构(AArch64 尤其在没有 LSE 的情况下、LoongArch、Power ISA)以及不常见 page size 的标准库 bug 被修复
- 影响 big-endian 主机的若干标准库与编译器 bug 被修复;big-endian ARM 目标在 ARMv6+ 下现在会输出 BE8 目标文件,而不是过去那套 BE32
对普通用户来说,这意味着 Zig 在常见 Linux / BSD / macOS / Windows 目标上的“可用性底线”又往前推了一步;而对于比较边缘的平台,官方也更清晰地区分了“支持”“实验性支持”和“不再支持”。
Tier 系统
Zig 把对各目标的支持程度划成四档(Tier 1 最高),具体含义如下:
- Tier 1:所有非实验性语言特性都已知正常工作;编译器可以不依赖 LLVM 直接为该目标生成机器码
- Tier 2:标准库的跨平台抽象覆盖了该目标;该目标具备调试信息能力,因此在断言失败 / crash 时能给出 stack trace;交叉编译时可获得 libc;CI 在每次推送时都会跑模块测试
- Tier 3:编译器可通过 LLVM 为该目标生成机器码;链接器可生成对象文件、库与可执行文件;该目标在 LLVM 中不被视为实验性
- Tier 4:编译器只能通过 LLVM 为该目标生成汇编源码
Tier 1 的长期目标是“没有任何被禁用的测试”——这一条也会变成 Zig 1.0 之后版本的硬性要求。
其他附加目标
除了 Tier 1–4 这套划分之外,Zig 对下面这些目标也有不同程度的支持,但 tier 系统本身并不完全适用:
aarch64-driverkit、aarch64(_be)-freestanding、aarch64-uefi、alpha-freestanding、amdgcn-amdhsa、amdgcn-amdpal、amdgcn-mesa3d、arc(eb)-freestanding、arm(eb)-freestanding、arm-3ds、arm-uefi、arm-vita、avr-freestanding、bpf(eb,el)-freestanding、csky-freestanding、hexagon-freestanding、hppa(64)-freestanding、kalimba-freestanding、kvx-freestanding、lanai-freestanding、loongarch(32,64)-freestanding、loongarch(32,64)-uefi、m68k-freestanding、microblaze(el)-freestanding、mips(64)(el)-freestanding、mipsel-psp、msp430-freestanding、nvptx(64)-cuda、nvptx(64)-nvcl、or1k-freestanding、powerpc(64)(le)-freestanding、powerpc64-ps3、propeller-freestanding、riscv(32,64)(be)-freestanding、riscv(32,64)-uefi、s390x-freestanding、sh(eb)-freestanding、sparc(64)-freestanding、spirv(32,64)-opencl、spirv(32,64)-opengl、spirv(32,64)-vulkan、thumb(eb)-freestanding、ve-freestanding、wasm(32,64)-emscripten、wasm(32,64)-freestanding、wasm(32,64)-wasi、x86(_64)-driverkit、x86(_64)-elfiamcu、x86(_64)-freestanding、x86(_64)-uefi、xcore-freestanding、xtensa-freestanding。
这些目标常见于嵌入式、unikernel、固件、内核扩展、GPU 计算、UEFI、各类游戏机/掌机、WebAssembly 宿主等场景。
系统最低版本要求
| 操作系统(Operating System) | 最低版本要求(Minimum Version) |
|---|---|
| DragonFly BSD | 6.0 |
| FreeBSD | 14.0 |
| Linux | 5.10 |
| NetBSD | 10.1 |
| OpenBSD | 7.8 |
| macOS | 13.0 |
| Windows | 10 |
语言变动
switch 继续补齐语义
switch 是这一轮里继续被打磨的语言特性之一。现在,packed struct 和 packed union 可以直接作为 prong item,比较规则按照 backing integer 来做;同时,decl literals、需要结果类型的表达式、union tag capture 等场景也获得了更一致的支持。
这类变更本身并不一定会让旧代码报错,但它明显减少了过去一些“语言明明应该支持、但实现上还没补齐”的边角问题。
@cImport 正式进入“迁移期”
0.16.0 仍然保留了 @cImport,但已经明确将其标记为 deprecated。官方方向是把 C 头文件翻译迁到构建系统中,通过 build.zig 里的 addTranslateC 生成模块,再在 Zig 代码里使用 @import("c")。
这和 Zig 未来“逐步把对 LLVM / Clang 的库级依赖转向进程级依赖”的方向是一致的。
同时,translate-c 的实现现在已经从 libclang 切换到了 Aro / translate-c 方案。对大多数用户来说这是透明的,但如果你升级后发现 C 头文件翻译行为有差异,它更可能是实现 bug,而不是新的预期行为。
@Type 被拆分为多个独立内建函数
这是 0.16.0 最明显的语言级 breaking change 之一。@Type 被移除,原来依赖它造类型的元编程代码,需要迁移到新的内建函数:
@EnumLiteral()@Int()@Tuple()@Pointer()@Fn()@Struct()@Union()@Enum()
这项改动的核心目标,是让“构造类型”这件事更直观,也让常见场景不必再绕一层 std.meta.Int、std.meta.Tuple 之类的辅助函数。
packed / extern 相关规则更严格
这次发布继续收紧了位级布局和 ABI 边界的隐式行为:
packed union现在要求明确的 backing integer 语义:不允许 unused bits,且字段 bit size 必须与 backing integer 一致packed struct/packed union不再允许直接放指针字段extern场景下,enum与packed类型不能再依赖隐式推断的底层整数类型
换句话说,packed union(T) 不能再留下 padding / unused bits;每个字段的 @bitSizeOf 都必须等于 backing integer T 的 @bitSizeOf。
从设计上看,这些限制的方向非常统一:凡是会影响 ABI 或精确内存布局的内容,Zig 都更倾向于要求你显式写出来。
向量语义进一步收紧
0.16.0 禁止了运行时向量索引,同时也不再鼓励通过旧式内存强转在数组和向量之间来回转换。简单来说,向量更明确地被当成“值语义上的 SIMD 数据”,而不是“碰巧可以按数组方式随便访问的内存”。
另外,小整数类型在“绝对不会丢精度”的前提下,现在可以安全地隐式转换为浮点类型,这也让数值代码更顺手了一些。
类型解析与依赖环错误大幅重做
0.16.0 还重做了编译器内部的类型解析流程。这个改动的影响非常深:
- 许多以前会误报 dependency loop 的代码现在可以正常工作
- 增量编译和普通编译之间的一致性明显增强
- 一小部分本来就存在真实依赖环的代码,现在会更早、更明确地报错
如果你升级后遇到以前没见过的 dependency loop,先别急着回退版本。因为 0.16.0 的错误报告已经能更清楚地指出环路是怎么形成的,通常只要打断其中一条依赖即可。
一元浮点内建会向下转发结果类型
@sqrt、@sin、@cos、@tan、@exp、@exp2、@log、@log2、@log10、@floor、@ceil、@trunc、@round 现在都会把外层的结果类型向内转发。意味着以前必须借中间变量才能写的式子(例如 const x: f64 = @sqrt(@floatFromInt(N));)现在可以直接成立。这条改动是配合“为做游戏开发改善 ergonomics”的更大计划。
浮点取整可以直接产出整数
@floor、@ceil、@round、@trunc 现在不仅会做取整,还能在结果类型是整数时直接给出整数值,省掉一层 @intFromFloat。
局部变量地址不能再被返回
返回局部变量地址(return &local;)这个新手常见错误现在直接编译报错。
这件事的难点其实在于:返回一个无效指针本身是合法的(例如 fn foo() *i32 { return undefined; }),非法只发生在解引用之后;甚至 fn bar() noreturn { unreachable; // equivalent to foo().* } 也是合法函数。Zig 的解法是“句法层面的较真”:所有“不需要类型检查就能 trivially 降级到 return undefined 的表达式”都被禁止,要求你直接写规范的 return undefined。return &local; 就属于这一类——编译器会给出 error: returning address of expired local variable 'x' 这样的清晰诊断,并标出 var x 是“declared runtime-known here”。
修复方式无非三种:返回值、由调用方传入 buffer、改用堆分配。后续还会有更多类似性质的编译错误被加进来。
Lazy 字段分析
struct / union / enum / opaque 现在只有在真正需要它的尺寸或字段类型时才会被解析。把类型当作命名空间使用,或者只是构造非解引用指针 *T,都不会再触发 T 的字段解析。这条改动直接消除了“引用一下 std.Io.Writer 就把整个 std.Io 的 vtable 都拉进来”这类问题,进而减少了无谓 codegen 和体积膨胀。
指向 comptime-only 类型的指针不再是 comptime-only
comptime_int 是 comptime-only 类型,但 *comptime_int / []comptime_int 现在不是。最直观的例子是函数指针:*const fn () void 是运行时类型——你不能在运行时解引用它,但它本身可以在运行时存在。配合反射,这意味着 []const std.builtin.Type.StructField 现在可以直接传给运行时函数,并通过它读出每个字段的 name。
*u8 与 *align(1) u8 不再是同一类型
这两个类型现在被视为不同类型(之前 == 比较为真)。两者依然可以互相转换,包括嵌套在指针里,所以日常用法几乎不需要变。可以理解为类似 u32 与 c_uint 的差别——技术上不同,行为一致。
Zero-bit tuple 字段不再隐式 comptime
0.14 引入的“tuple 中 zero-bit 字段隐式变成 comptime 字段”这条规则被回滚。对绝大多数代码无影响——zero-bit 字段的值依然是 comptime-known 的——只有直接读 StructField.is_comptime 反射或依赖类型等价性的代码需要调整。
Reworked Byval Syntax Lowering
编译器前端早期为了减少中间指令尝试用“byval”语义降级表达式,但这个实验带来了数组访问性能问题、显式拷贝下的意外别名、退化场景代码质量极差等一系列 issue。0.16.0 改成全程 byref 降级,只在最后一次 load 时才取值,从根上修掉这些问题。
标准库
I/O 作为 Interface
这是 0.16.0 最重头的内容,没有之一。
从这个版本开始,所有输入输出相关能力都围绕 std.Io 展开。更准确地说,凡是可能阻塞控制流,或会引入非确定性的操作,都被纳入了 Io 的抽象边界内。
当前官方提供了几种典型实现:
Io.Threaded:基于线程,功能最完整,也是从0.15.x升级时最接近旧行为的实现Io.Evented:仍在实验阶段,用来推动接口演进Io.Uring/Io.Kqueue/Io.Dispatch:分别基于 Linux io_uring、kqueue 和 macOS Grand Central Dispatch 的概念验证实现,尚未完整Io.failing:用于模拟“不支持任何操作”的环境
围绕它,标准库引入了整套新的任务和并发抽象:
FutureGroupBatchSelectQueue(T)Clock/Duration/Timestamp/Timeout等时间度量类型- 统一的 cancelation 模型
这不仅是 API 改名,而是 Zig 对“并发 I/O 应该怎样进入语言生态”给出的新答案。文件系统、网络、进程、同步原语、定时器等能力,都围绕这套接口重新组织了。
Future
Future 是构建在函数之上的任务级抽象。
io.async(fn, args) 会返回一个 Future(T),其中 T 是被调用函数的返回类型。async 表达的是“这次调用与其他逻辑相互独立”——也就是 asynchrony。因此创建任务是不会失败的,并且能在缺乏并发机制的精简 Io 实现上工作(这种实现可以在 async 调用里直接同步执行那个函数)。
io.concurrent(fn, args) 与 io.async 类似,但语义上要求“必须并发执行才能保证正确性”。这必然涉及内存分配,所以可能返回 error.ConcurrencyUnavailable。
Future(T) 提供两个方法:
await:阻塞控制流直到任务完成,并返回函数的返回值cancel:和await相同,但额外请求Io实现去打断该操作并返回error.Canceled。绝大多数 I/O 操作的错误集合里现在都包含error.Canceled
为了避免资源泄漏并优雅处理 cancelation,推荐这种写法:
var foo_future = io.async(foo, .{args});
defer if (foo_future.cancel(io)) |resource| resource.deinit() else |_| {}
var bar_future = io.async(bar, .{args});
defer if (bar_future.cancel(io)) |resource| resource.deinit() else |_| {}
const foo_result = try foo_future.await(io);
const bar_result = try bar_future.await(io);如果函数没有返回需要释放的资源,那个 if 可以简化成 _ = foo.cancel(io) catch {};如果返回 void,丢弃也可以省掉。但 cancel 这一步是必要的,因为它在出错(包括 error.Canceled)时也会释放 async 任务本身占用的资源。
Group
当多个任务有相同生命周期时,Group 是更合适的选择。它的开销是 O(1):开 N 个任务也只需要常数级的额外空间。
下面是一个用 Group.async 实现 sleep sort 的例子:
const std = @import("std");
const Io = std.Io;
test "sleep sort" {
const io = std.testing.io;
const rng_impl: std.Random.IoSource = .{ .io = io };
const rng = rng_impl.interface();
var array: [10]i32 = undefined;
for (&array) |*elem| elem.* = rng.uintLessThan(u16, 1000);
var sorted: [10]i32 = undefined;
var index: std.atomic.Value(usize) = .init(0);
var group: Io.Group = .init;
defer group.cancel(io);
for (&array) |elem| group.async(io, sleepAppend, .{ io, &sorted, &index, elem });
try group.await(io);
for (sorted[0 .. sorted.len - 1], sorted[1..]) |a, b| {
try std.testing.expect(a <= b);
}
}
fn sleepAppend(io: Io, result: []i32, i_ptr: *std.atomic.Value(usize), elem: i32) !void {
try io.sleep(.fromMilliseconds(elem), .awake);
result[i_ptr.fetchAdd(1, .monotonic)] = elem;
}Cancelation
顺便注意拼写:标准命名采用单
l的cancelation。
和 for 循环里的“提前 break”是同一类问题——一旦多个任务并发执行,就会遇到“某个任务已经完成或失败,其它任务的结果或副作用对全局已经不再重要,甚至需要回滚”的场景。
Future / Group / Batch 都支持发起 cancelation 请求。请求可能被确认,也可能不会。一旦被确认,对应的 I/O 操作会返回 error.Canceled。即便是 Io.Threaded 也支持这套机制——它通过给线程发信号让阻塞 syscall 返回 EINTR,再以此为契机检查 cancelation 请求并决定是否重试。
只有“发起这次取消请求”的代码自己才能安全地忽略 error.Canceled。其它情况下处理这个错误有三种方式(按常见程度排序):
- 把它向上传播
- 收到后调用
io.recancel(),再选择不传播——这会重新激活 cancelation 请求,下一个检查点会有机会再次确认 - 用
io.swapCancelProtection()把它变成 unreachable
cancel 在语义上等同于 await + 取消请求。也就是说,请求取消之后任务可能依然成功完成,你仍然能拿到返回值——这意味着诸如已分配的资源等副作用必须被正确处理:
const std = @import("std");
const Io = std.Io;
test "trivial cancel demo" {
const io = std.testing.io;
var file_task = io.async(Io.Dir.openFile, .{ .cwd(), io, "hello.txt", .{} });
defer if (file_task.cancel(io)) |file| file.close(io) else |_| {};
}由于 await 和 cancel 都是幂等的,最常见的写法是“创建任务后立刻 defer 取消”,这样无论函数从哪里返回都能保证并发任务被回收。
通常 Zig 程序员不需要主动写代码支持 cancelation,因为 error.Canceled 已经被烘焙进所有可取消 I/O 操作的错误集合。但如果有长时间运行的 CPU 密集任务需要响应取消,可以显式调用 io.checkCancel 添加额外的 cancelation point。
Batch
Batch 是更底层的并发原语,它在 Operation 这一层提供并发能力。优点是高效且可移植,缺点是抽象起来更难——尤其当你需要在操作之间穿插一些自定义逻辑时。
未来大部分文件系统和网络功能预计都会迁到基于 Operation 的实现上,进而都能配合 Batch 使用,并通过 operateTimeout 给任意 I/O 操作加超时。当前能用 Batch 跑的 Operation 列表是:
FileReadStreamingFileWriteStreamingDeviceIoControlNetReceive
而 Future 是同样思想在“函数”层面的版本——更灵活、更顺手,但在使用 concurrent 时需要分配任务内存并可能返回 error.ConcurrencyUnavailable,使用 async 时也可能引入意外的阻塞操作。
简单说:写最优、可复用的代码时,如果你只是想同时做几件事,Batch 更合适;如果绕开它会让你重新发明 future,那就用 Future。也可以先用 Future 写出来,之后再把热点路径改造成 Batch 来减少任务开销。
Juicy Main
为了配合新的 std.Io,main 也获得了一个很实用的新入口:std.process.Init。
只要把 main 写成:
const std = @import("std");
pub fn main(init: std.process.Init) !void {
const gpa = init.gpa;
const io = init.io;
_ = gpa;
_ = io;
}你就能直接拿到:
gpaioarenaenviron_mappreopensminimal.args/minimal.environ
这让应用入口第一次真正成了“进程上下文的注入点”。对于应用开发来说,这个改动的体感甚至不亚于 std.Io 本身。
环境变量和进程参数不再是全局状态
和 Juicy Main 配套的另一项重要变化,是环境变量和进程参数都不再被鼓励当成全局状态来访问。
现在,环境变量原则上只存在于应用入口的 Init 里;需要使用它们的函数,应当显式接收需要的值,或者接收 *const std.process.Environ.Map。
这个方向很符合 Zig 一贯的设计哲学:尽量少依赖隐式的全局上下文,让副作用和依赖关系都显式体现在函数签名里。
线程与分配器模型继续更新
围绕新的 std.Io,标准库的并发相关设施也继续收敛:
std.Thread.Pool被移除;简单的spawnWg/ group-await 模式可迁到std.Io.async/std.Io.Group.async,复杂同步或必须保证并发执行的场景需参考std.Io.concurrent文档std.heap.ArenaAllocator变成了 thread-safe 且 lock-freestd.heap.ThreadSafeAllocator被移除
如果把这些变化放在一起看,会发现 Zig 正在逐步放弃一些“靠包装器补线程安全”的旧路子,转而更偏向于:让真正需要并发的基础组件自己具备合适的并发语义。
文件系统、路径与容器 API 持续整理
除了 std.Io 大迁移之外,这次标准库还有很多看起来零碎、但真实影响升级体验的整理工作:
std.io继续收敛到std.Iostd.fs的一批常用入口迁到std.Io.Dir/std.Io.Filestd.process.getCwd*改名为currentPath*fs.path.relative变成纯函数,需要显式传入上下文File.Stat.atime变成可选值std.mem里“index of”系列统一更名为“find”- 一批容器继续向 unmanaged 方向迁移,
PriorityQueue/PriorityDequeue的命名也更统一了
这些调整单看都不算大新闻,但合在一起,就是一次很典型的 Zig 式“去历史包袱”整理。
调试信息与栈追踪重做
0.16.0 重做了一批和调试信息相关的标准库 API,特别是栈追踪(stack trace)。这件事的真正动机是:在没有帧指针(例如 libc 用 -fomit-frame-pointer 编译)的前提下,依然能做到“快又安全”的栈展开——既不需要为每个栈帧检查地址是否越界、又能避开过去那些“崩溃时去做栈展开反而再次崩溃”的边角情况。
这其实是个比想象中复杂得多的问题,因为它真正的解法是 unwind information,而不同目标对 unwind 信息的编码方式各不相同。Zig 的标准库以前虽然也支持过 unwind 信息,但实现 buggy 又不完整,性能也常常拖后腿。0.16.0 之后,标准库默认会优先使用基于 unwind 的“安全”栈展开;和原来基于帧指针的展开相比,性能开销在大多数场景下是可以接受的。
这条改动同样是“为做游戏开发改善 Zig 体验”的一部分。
跨进程进度上报支持 Windows
std.Progress 现在可以在 Windows 上跨进程上报进度,子进程的进度会自动反映到父进程的进度树里。同时,最大节点名称长度也从 40 提升到 120。
这件事看起来很小,但对“构建系统跑大量子工具”的工作流来说体感差异很明显:以前 Windows 下子工具只能各自打印日志,现在可以像 Linux / macOS 一样汇聚到统一的进度面板。
Windows 网络不再依赖 ws2_32.dll
0.16.0 把 Windows 上所有网络 API 直接迁到了 AFD(Ancillary Function Driver),不再走 ws2_32.dll。
这个改动一次性带来了几个实际收益:
- 修掉了一批历史 bug
- 让 cancelation 和 Batch 在网络操作上正确工作
- 避开了
ws2_32.dll内部的性能陷阱——例如它会为每个 socket handle 维护一个完全多余的 hash table,需要分配和同步,而不是简单地在accept时把 socket mode 和 protocol 一起传下去
简单一句话:Windows 上网络 API 现在更稳、更快,也更适配 std.Io 的并发模型。
完成向 NtDll 的迁移
0.16.0 之后,Windows 上几乎所有标准库功能都直接基于最低层级、稳定的 syscall 接口实现。剩下还会显式调用 Windows DLL 的 extern 函数只剩这些:
kernel32.CreateProcessWcrypt32一组:CertOpenStore/CertCloseStore/CertEnumCertificatesInStore/CertFreeCertificateContext/CertAddEncodedCertificateToStore/CertOpenSystemStoreW/CertGetCertificateChain/CertFreeCertificateChain/CertVerifyCertificateChainPolicy
这套迁移直接修掉了一批 Windows 上的 bug、性能问题和缺失功能,让 Zig 程序在 Windows 上比许多其它语言更精简、更可靠。Batch 与 Cancelation 这两套在 Windows 上能用并且实现高效,就是这次迁移直接带来的结果。
如果你想兼容 XP 这类更老的 Windows 版本,或者就是更倾向于走 kernel32 这种较高层的 DLL,官方建议作为社区方案做一份不依赖 NtDll 的第三方 Io 实现。CreateProcessW 与 crypt32 这一组短期内不会再继续迁移。
Deflate 压缩器与解压缩简化
std.compress.flate 这一轮新增了从零实现的 deflate 压缩器:以 writer 缓冲区作为历史窗口、用链式哈希表寻找匹配,token 累积到阈值后整块输出。除此之外,还提供了两种额外的 writer:
Raw:完全只输出 store block(即未压缩字节),借助数据向量高效发送 block header 与数据Huffman:只做 Huffman 压缩,不做匹配
这两者因为不需要保留历史,可以更直接地利用新的 writer 语义。
token 中的字面量与距离编码参数也被重做:参数现在是数学方法推导出来的,更昂贵的那部分依然走查表(ReleaseSmall 例外)。
解压侧的 bit 读取也大幅简化,充分利用了底层 reader 可以 peek 的能力,并修掉了若干和 limit 处理相关的 bug。
与 zlib 的对比
性能与压缩比层面,简单概括是:
- 压缩比:默认压缩级别下 zlib 比 Zig 的实现高 1.00%,最高压缩级别下高 0.77%——zlib 选取的匹配略有不同,但匹配字节总数其实更少,未来还可以继续打磨向 zlib 看齐
- 默认级别压缩性能:Zig 的 std-deflate 比 zlib 大约快 9.7%,CPU 周期少 9.8%,cache miss 与分支误预测都明显更少;instruction 数因为算法选择不同会高约 18.9%,但被更友好的访存模式抵消
- 最高级别压缩性能:和 zlib 持平(差异在 1% 以内)
- 解压性能(vs 上一版):新解压实现比 0.15.x 大约快 9.5%,CPU 周期与指令数都减少约 10%,分支误预测下降约 18%
换句话说,Zig 0.16.0 自带的 deflate 在常见场景下已经能在性能上和 zlib 打平甚至略胜,压缩比仍然是后续的优化方向。
std.crypto 新增 AES-SIV / AES-GCM-SIV
过去 Zig 标准库一直缺少“可抗 nonce 重用”的 AEAD 方案;0.16.0 把这一缺口补上了:
- AES-SIV:在密钥包装(key wrapping)这类场景里特别有用
- AES-GCM-SIV:在嵌入式目标上尤其合适
这两个原语都是“nonce reuse-resistant”AEAD 的事实标准选择。
std.crypto 新增 Ascon-AEAD / Ascon-Hash / Ascon-CHash
Ascon 是 NIST 为轻量级密码学标准化的一系列构造。Zig 标准库以前已经提供了 Ascon 置换本身,但建立在置换之上的高层构造一直被刻意推迟,等待 NIST 发布最终规范。
NIST SP 800-232 已经正式发布之后,Zig 0.16.0 把这一组高层构造一次性补齐:
Ascon-AEAD:AEAD 加密Ascon-Hash:定长哈希Ascon-CHash:可定制化的哈希
对嵌入式 / IoT 场景这是一项久违的能力补齐。
Windows 标准库实现继续下沉
Windows 也是 0.16.0 里非常有意思的一条线,前面几节已经覆盖了核心内容:
- 网络 API 不再依赖
ws2_32.dll,而是直接基于 AFD 实现 - 标准库基本完成向 NtDll 收敛,仅剩
CreateProcessW与一组crypt32函数 std.Progress现在也支持 Windows 下的跨进程进度上报- inline caller 现在会从 PDB 调试信息里被解析;如果 debug info 模糊,会把所有候选 caller 都打印出来;error return trace 也会在所有平台上包含 inline caller
这些工作虽然对多数用户不可见,但会真实影响程序的健壮性、性能,以及 cancelation / batch 模型在 Windows 上的完整度。
构建系统
依赖目录改到项目本地 zig-pkg
从 0.16.0 开始,依赖包会被拉取到项目根目录旁边的 zig-pkg 目录,而不是继续使用过去那种全局解压缓存模式。
这个变化的好处很直接:
- 你可以更方便地阅读、搜索、修改依赖源码
- 可以更自然地把依赖目录换成本地 git clone
- IDE 也更容易直接索引整棵依赖树
zig build --fork
构建系统新增了 --fork=[path] 参数,可以让你临时用本地目录里的 fork 覆盖依赖树中的匹配包。
这对生态 breakage 的排查非常有帮助:你可以在不改版本元数据的前提下,直接调试一整串依赖之间的兼容问题。
依赖元数据更严格
0.16.0 还提高了 build.zig.zon 的要求:
- 缺少
fingerprint会直接失败 name不能再用字符串,必须写成 enum literal- 旧 hash 格式支持已被移除
这意味着旧项目在升级时,最好顺手检查一遍所有依赖元数据,而不是等到 zig build 报错再逐个补。
新增测试超时与错误输出样式
zig build 新增了几项很适合日常开发的参数:
--test-timeout--error-style--multiline-errors
同时,旧的 --prominent-compile-errors 被移除了,对应的新写法是 --error-style minimal。
临时文件 API 被重构
Build.makeTempPath 和 RemoveDir step 都被清理掉了,新的推荐路径是:
Build.addTempFilesBuild.addMutateFilesBuild.tmpPath
这项重构背后的核心思路,是把“临时目录”“可变文件”“缓存语义”这些东西从一开始就表达清楚,而不是让旧 API 在 configure 阶段偷偷做一堆文件系统副作用。
Compiler
translate-c 改用 Aro
编译器内部的 translate-c 现在基于 Aro / translate-c,而不是 libclang。这一改动直接从编译器源码树里删掉了 5,940 行 C++(剩余 3,763 行),也让 Zig 离“把对 LLVM 的库级依赖换成对 Clang 的进程级依赖”又近了一步。
对普通用户来说,这件事大体上是非破坏性的——但也确实是“一个 C 编译器替换成另一个”,所以如果你升级后发现某个 C 头文件翻译结果不一致,请按 bug 反馈。
旧 @cImport 路径的实现仍然是“懒加载”的:第一次遇到 @cImport 时再从源码构建;新的推荐路径是提前在 build.zig 里用 addTranslateC 生成模块。
LLVM 后端
LLVM 后端这一轮的进展有几条:
- 实验性支持增量编译——这并不会加速“LLVM Emit Object”这一步(LLVM 自己负责的部分我们做不了什么),但加速了 Zig 编译器侧生成 LLVM bitcode 的过程,因此当你的代码本身就有编译错误时,你能在 LLVM 后端下也获得近乎瞬时的反馈
- LLVM bitcode 体积下降 3-7%
- 在某些情况下编译速度略提升约 3%
- 修掉了零字节 payload union 的调试信息
- 调试信息现在对所有类型都包含正确的名字
- error set 类型现在以 enum 方式 lower,使得 error 名字在运行时可见
LLVM 后端目前通过了 2004 / 2010(100%)行为测试。Matthew 还实验过把 tagged union 和 error union 用 DWARF 的 variant 类型表达,让调试器只显示“当前激活的字段”——但 LLDB 对 variant 类型的支持只有在语言被标成 Rust 时才启用,因此暂未落地。这条路径未来下游若改善还可能再走。
后续还会继续推进 LLVM 后端的并行化:让多个线程同时为不同函数生成 LLVM IR,再由一个“linker”线程合并。
重做 byval 语法降级
编译器前端早期为了减少中间指令尝试用“byval”语义降级表达式,但这个实验带来了三类典型问题:数组访问性能问题、显式拷贝下的意外别名、退化场景代码质量极差。
0.16.0 改成全程 byref 降级,只在最后一次 load 时才取值,从根上修掉这些问题。
类型解析重构
前面在“语言变动”里提过,0.16.0 大幅重做了类型解析。这件事的真正动机有两个:简化 Zig 语言规范的撰写,以及修掉一大批和增量编译相关的编译器 bug。
新规则总体上比旧规则更宽松:以前能 work 的代码大多数仍然能 work,一些以前会因为 dependency loop 报错的代码现在反而能正常编译。但它并不是“严格更宽松”——例如下面这种结构体在自身对齐查询里依赖 @This() 的写法,从 0.16.0 起会被识别为依赖环并直接报错:
const S = struct {
foo: [*]align(@alignOf(@This())) u8,
};不过这次配套的错误信息也清晰了许多。例如某条长度为 3 的依赖环——“S 默认字段值用了 default_val,default_val 用了 other_val,other_val 又通过 @typeInfo(S) 回到 S”——错误输出会把这三跳依次列出来,并提示“破坏其中任何一条都能解开环”。
这件事对编译器本身还有两个非常重要的连锁收益:
- 依赖环报错更可解释
- 增量编译和普通编译之间的一致性更强
这也是为什么你会发现,本版本许多看似分散的改动,最后都会回到“为了更可靠的增量编译”这个主题上。
增量编译继续前进
增量编译让 Zig 编译器只重新编译自上次构建以来真正改动的代码——小改动可以从“秒 / 分钟”降到“毫秒”。0.16.0 这一轮里它的进步非常具体:
- 避免“过度分析”——大部分场景下,编译器不再因为某个改动牵连出本不需要重建的代码。在 Zig 编译器自己身上做实验:以前会几乎重新编译整个编译器的改动,现在能在毫秒级完成。这里的关键,是“类型解析重做”让编译器内部依赖图变成了无环图(dependency loop 除外)
- 增量编译和非增量编译之间,不再因为对方触发 dependency loop 而表现不一致。这是上一版里最大的不一致来源
- 在 ELF 目标上,自托管后端 +
-fincremental时默认启用新的 ELF linker——更快、对增量编译的支持也更稳 - 整体稳定性明显提升,崩溃和误编译比上一版少得多
- LLVM 后端现在也支持增量编译
官方现在明确鼓励大家实际使用:
zig build -fincremental --watch哪怕只是“近乎瞬时拿到编译错误”,多数用户也会被它实际的提速程度震惊到。当然,它仍然有已知 bug、甚至可能包含误编译;所以 0.16.0 里增量编译依然不是默认开启。
x86 后端
0.16.0 里 x86 自托管后端:
- 修复 11 个 bug
- 常量
memcpy的代码生成更优
和 LLVM 后端相比,x86 后端通过的行为测试更多、编译速度显著更快、调试信息更优,机器码质量稍逊;它仍然是 Debug 模式下的默认后端。
aarch64 后端
仍处于开发中。这个版本周期内,由于 std.Io 带来的标准库 churn,aarch64 后端的进度暂时放慢;当前在跑行为测试时会崩溃。等标准库这边的 churn 结束之后,进度会重新拾起。
WebAssembly 后端
目前通过了 1813 / 1970(约 92%)项行为测试(与 LLVM 后端做对比)。
不再依赖 LLVM 生成 .def 导入库
0.16.0 把“从 .def 文件生成导入库”这件事从 LLVM 那边切了出来——具体面向的是 Zig 自带的 MinGW-w64 .def 文件。新实现大体参考 LLVM 的 COFFModuleDefinition.cpp 与 COFFImportFile.cpp。
这条同样是“把对 LLVM 的库级依赖转向对 Clang 的进程级依赖”这条长期路线的一环。
for 循环安全检查的代码生成改进
针对 slice 的 for 循环,安全检查相关的代码生成减少了大约 30%。这意味着典型的循环代码在 Debug 构建里不仅仍然带越界检查,体积也明显小于上一版。
链接器(Linker)
新 ELF Linker
0.16.0 的新 ELF linker 可以通过 -fnew-linker 显式启用,或者在 build 脚本里设置 exe.use_new_linker = true。更重要的是:在 -fincremental 且目标是 ELF 时,它现在会默认启用。
官方给出的一组数据点非常直观——对 Zig 编译器本体先做一次完整构建,然后做一次单行改动,再做第二次单行改动:
- 旧 linker:
14s、194ms、191ms - 新 linker:
14s、65ms、64ms(快约 66%) - 完全跳过链接:
14s、62ms、62ms(快约 68%)
这也意味着过去那种专门暴露 -Dno-bin、只求快速拿到编译错误的工作流,收益已经几乎可以忽略——你大可以让 codegen 和链接一直开着,构建结束顺手拿到一个可执行文件。
不过要注意,新 linker 目前还没补齐旧 linker / LLD 的能力,例如生成物还缺少 DWARF 信息。所以它已经够快、够值得试,但还没有到“所有场景都能无脑切换”的程度。当新 linker 功能完全对齐之后,旧 linker 会被删掉,LLD 也会从依赖里移除。
Fuzzer(模糊测试器)
Smith 取代 []const u8
Fuzz 测试接口是 0.16.0 里另一个会直接影响用户代码的 breaking change。过去 fuzz test 习惯接收 []const u8 输入;现在统一改成 *std.testing.Smith,由它来生成结构化值。
Smith 提供的基础方法包括:
value:生成任意类型的值eos:生成 end-of-stream 标记,且保证“最终一定会返回true”bytes:填充字节数组slice:填充缓冲区的一部分并给出长度
每个方法都有支持权重的对应版本,权重通过 []const Smith.Weight 表达,可以用来:
- 让“有意思”的值出现得更频繁
- 减少不必要的工作量
- 限制可选值的范围
值得注意的是,权重只能用于能放进 64 位的类型;空权重切片意味着所有值的权重为零,因此都不会被选中。除此之外还提供:
baselineWeights:为某个类型构造覆盖所有可能值的权重集boolWeighted/eosSimpleWeighted:方便地为true/false配权重valueRangeAtMost/valueRangeLessThan:只生成某个范围内的值
每个方法还有一个接收 hash 参数的对应版本——同 hash 的值更倾向于一起被变异。常规方法本身已经基于 callee 返回地址生成 hash,因此你通常不需要手动调用这一组,主要是 inline 的场景下才会用到。
多进程、多核与 crash dump
除了接口变化之外,fuzzer 本身也更强了:
- 多进程 / 多核:现在可以利用多核,受
-j构建选项控制;受限模糊(limited fuzzing)仍然只用一个核 - 无限模式:当提供多个 fuzz test 时,fuzzer 会在它们之间切换并优先运行更“有产出”的测试;随着时间推移,已经被反复探索过的测试会被分得越来越少的预算
- 崩溃输入会自动落盘:crash 信息中会指出落盘文件路径,便于配合
std.testing.FuzzInputOptions.corpus与@embedFile复现
配套的 AST Smith 已经替 zig fmt 找到 20 个 bug
新的 Smith 接口本身已经被用来构建一个“AST Smith”——专门生成随机但合法的 AST。把它丢给 zig fmt(再加上一些更早期的简单随机源码测试),一共发现并修复了 20 个独立 bug,其中一部分是新发现的。
它还顺手修了几个“PEG 与 parser 不一致”的问题,例如以前 tuple 不能包含以 extern 或 inline 起头的类型——const T = struct { u64, extern struct { a: u64 }, u32 } 在以前会直接报错。
Bug 修复
本轮发布周期内,Zig 一共关闭了 345 个 bug 报告。许多 bug 是在本周期内被引入又被修复的;为了简洁,绝大多数 bug 修复并未单独列出。
这个版本仍然包含已知 bug
官方明确写在 release notes 里——
Zig 仍然存在已知 bug、误编译(miscompilation)和回归(regression)。
对稍微复杂一点的项目,使用 0.16.x 仍然意味着要准备好参与开发流程:提交最小复现、跟踪 issue、必要时切版本验证。
这并不意外。因为 0.16.0 本质上是一个“大迁移版本”,它把很多还在演进中的长期工程方向一次性推到了用户面前。Zig 1.0 之后,Tier 1 支持会获得一项额外要求:bug policy。
工具链(Toolchain)
LLVM 21
0.16.0 升级到了 LLVM 21.1.0,覆盖了 Clang(即 zig cc)、libc++、libc++abi、libunwind、libtsan 等组件。
为绕过回归,loop vectorization 被全局关闭
这里有一个非常值得注意的 caveat:LLVM 21 上游存在一个严重回归,会在常见配置下误编译 Zig 编译器自身。试图通过禁用某些 CPU 特性来绕过太脆弱,所以 Zig 在 0.16.x 中完全禁用了 loop vectorization。这会让某些代码生成结果比理想情况更保守,但它仍然比“在常见配置下误编译 Zig 编译器自身”要好得多。
这个 bug 已经向上游报告并修复,但写文档时,修复尚未被 cherry-pick 到 LLVM 22.x 分支。因此官方预计这个性能回退不止会影响 0.16.x,还会延续到 0.17.x,大概率要等到 0.18.x 才会彻底解决。
musl 1.2.5
0.16.0 分发 musl 1.2.5 + 回移的安全修复。上游已经标了 1.2.6,未来某个 Zig 版本会跟进。
由于很多函数被迁到了 zig libc,相比上一版少分发了 331 个 musl C 源文件(剩 1,206 个)。这意味着如果你遇到 musl libc 相关 bug,应当先报到 Zig 的 issue tracker 而不是 musl。
另外,0.16.0 不受 CVE-2026-40200 影响——musl 的 qsort / qsort_r 已经不再被使用。
glibc 2.43
交叉编译时现在可以选择 glibc 2.43。
Linux 6.19 headers
随 Zig 分发的 Linux 内核头文件升级到 6.19。
macOS 26.4 headers
随 Zig 分发的 macOS 系统头文件升级到 26.4。
MinGW-w64
0.16.0 继续分发 MinGW-w64 commit 38c8142f660b6ba11e7c408f2de1e9f8bfaf839e。但很多函数已经迁到 zig libc:本版相比上一版少分发 99 个 MinGW-w64 C 源文件(剩 398 个)。同样地,相关 bug 应当先报到 Zig 这边而不是 MinGW-w64。
FreeBSD 15.0 libc
交叉编译时现在可以选择 FreeBSD libc 15.0。
WASI libc
0.16.0 升级到 WASI libc commit c89896107d7b57aef69dcadede47409ee4f702ee。
虽然很多函数已经迁到 zig libc,但因为新版 WASI libc 添加了 pthread shim,且 WASI libc 大部分源文件其实和 musl 共享,WASI libc 自身的源文件数反而从 196 增加到 228。
zig libc
zig libc 这一轮继续吞并原来来自 musl、MinGW-w64、WASI libc 的一部分 C 实现。Zig 分发的 C 源文件总数从 2,270 降到了 1,873,减少了约 17%。
这里尤其值得一提的是:很多数学函数,以及 malloc 系列函数,现在都已经进入 zig libc 的实现范围。这条进展受益于 Szabolcs Nagy 提供的 libc-test。
zig cc
zig cc / zig c++ 现在基于 Clang 21.1.8。这意味着 Zig 在“C / C++ 工具链外壳”这条线上,也继续和整体 LLVM 版本一同推进。本周期内一共修复了 9 个 zig cc 相关 bug。
交叉编译时支持动态链接的 OpenBSD libc
Zig 现在允许交叉编译到 OpenBSD 7.8+:和 glibc 类似,会提供动态 libc 的 stub library,并附带全部 libc 头文件以及大多数系统头文件。
路线图(Roadmap)
官方对 0.17.0 的规划很明确:这是一个相对短周期版本,主要目标是升级到 LLVM 22,并完成“把构建执行阶段和 build.zig 配置阶段分离”的工作。
在这之后,更长期的大方向仍然是:
- 继续完成并稳定语言本身
- 做完 aarch64 后端,并让它成为 Debug 模式默认后端
- 继续增强链接器,减少对 LLD 的依赖,并服务增量编译
- 继续增强内置 fuzzer,让它和 AFL 等业界最先进的 fuzzer 处于同一水平线
- 继续把对 LLVM 的依赖,从“链接库”转向“调用 Clang 进程”
如果说 0.15.x 还是“这些方向马上就要影响到你了”,那 0.16.0 就是“它们已经开始真正影响你的日常开发了”。