Skip to content

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 BSD6.0
FreeBSD14.0
Linux5.10
NetBSD10.1
OpenBSD7.8
macOS13.0
Windows10

语言变动 ​

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:用于模拟“不支持任何操作”的环境

围绕它,标准库引入了整套新的任务和并发抽象:

  • Future
  • Group
  • Batch
  • Select
  • Queue(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,推荐这种写法:

zig
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 的例子:

zig
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。其它情况下处理这个错误有三种方式(按常见程度排序):

  1. 把它向上传播
  2. 收到后调用 io.recancel(),再选择不传播——这会重新激活 cancelation 请求,下一个检查点会有机会再次确认
  3. 用 io.swapCancelProtection() 把它变成 unreachable

cancel 在语义上等同于 await + 取消请求。也就是说,请求取消之后任务可能依然成功完成,你仍然能拿到返回值——这意味着诸如已分配的资源等副作用必须被正确处理:

zig
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 列表是:

  • FileReadStreaming
  • FileWriteStreaming
  • DeviceIoControl
  • NetReceive

而 Future 是同样思想在“函数”层面的版本——更灵活、更顺手,但在使用 concurrent 时需要分配任务内存并可能返回 error.ConcurrencyUnavailable,使用 async 时也可能引入意外的阻塞操作。

简单说:写最优、可复用的代码时,如果你只是想同时做几件事,Batch 更合适;如果绕开它会让你重新发明 future,那就用 Future。也可以先用 Future 写出来,之后再把热点路径改造成 Batch 来减少任务开销。

Juicy Main ​

为了配合新的 std.Io,main 也获得了一个很实用的新入口:std.process.Init。

只要把 main 写成:

zig
const std = @import("std");

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    const io = init.io;
    _ = gpa;
    _ = io;
}

你就能直接拿到:

  • gpa
  • io
  • arena
  • environ_map
  • preopens
  • minimal.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-free
  • std.heap.ThreadSafeAllocator 被移除

如果把这些变化放在一起看,会发现 Zig 正在逐步放弃一些“靠包装器补线程安全”的旧路子,转而更偏向于:让真正需要并发的基础组件自己具备合适的并发语义。

文件系统、路径与容器 API 持续整理 ​

除了 std.Io 大迁移之外,这次标准库还有很多看起来零碎、但真实影响升级体验的整理工作:

  • std.io 继续收敛到 std.Io
  • std.fs 的一批常用入口迁到 std.Io.Dir / std.Io.File
  • std.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.CreateProcessW
  • crypt32 一组: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.addTempFiles
  • Build.addMutateFiles
  • Build.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 起会被识别为依赖环并直接报错:

zig
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 后端现在也支持增量编译

官方现在明确鼓励大家实际使用:

sh
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 就是“它们已经开始真正影响你的日常开发了”。