Zig 0.17.0 发布:构建系统重构、增量编译落地与 @bitCast 重定义

Zig 在 10 月 2 日发布 0.17.0 版本。这轮更新包含 5 个月的工作量,来自 206 位贡献者的 925 个提交,修复了 329 个 bug 报告。版本号本身也说明了项目所处的阶段:Zig 官方明确表示语言稳定化是通往 1.0 的前置条件,而 0.17.0 在这条路上迈出了实质性的一步——上一个发布周期里,团队讨论并裁决了大约 150 份语言提案,接受约 25 份、拒绝约 125 份,目前只剩 23 份提案在 Codeberg 追踪器、61 份在旧 GitHub 追踪器上待定。

这篇文逐段把 0.17.0 的变化按影响面拆开:构建系统重构、增量编译铺开、语言语义变更、标准库调整,最后是升级时需要注意的破坏性清单。

Zig 官方吉祥物 Ziggy

构建系统:build runner 这个概念没有了

0.17.0 对构建系统动了最大的一刀:原来的 build runner 被拆成两个独立进程,configurer(执行 build.zig 逻辑、生成构建图配置)和 maker(执行构建图)。拆分的直接收益是 zig build 变快了:

  • maker 可执行文件在 build.zig 被编辑时保持不变,安装 Zig 后只需构建一次,后续不再重复构建;
  • maker 以开启优化的方式编译,--watch 和 --fuzz 这类常驻场景直接受益;
  • 部分 CLI 参数组合下可以完全跳过 build.zig 逻辑。

配置结果现在序列化成紧凑的二进制格式,第三方工具可以直接消费——这是新的 Build Server Protocol(BSP)的一部分。旧的"fork build runner"协作方式被移除。配合这轮重构,Zig 自带的缓存系统也从文本格式切换为二进制格式,缓存文件体积缩小约 25%,缓存命中速度提升 5% 到 10%(#36822)。缓存系统还新增了"解释未命中原因"的能力,排查缓存失效不再靠猜。新增的 zig cache-cat 子命令可以直接查看 zig-cache 目录里的二进制缓存文件。

包管理功能整体从编译器搬进了构建系统,zig build、zig fetch、zig init、zig libc、zig cache-cat 这些子命令的实现不再编译进编译器可执行文件,而是以源码形式随构建系统分发。这个取舍有一处副作用:这些代码从 -Ofast 改为 -Osafe 编译。换取的是 SafeAllocator 一类的安全检查在全链路生效。

BSP 的首个消费者本该是 ZLS(Zig 语言服务器),但这次拆分恰好是破坏性变更——ZLS 在 0.17.0 上暂时无法工作。Zig 团队和 ZLS 团队正在协作扩展协议,目标是让 ZLS 不仅恢复功能,还能通过 BSP 拿到类型系统信息、重构支持等更深的编辑器能力(#615)。官方还计划让 Zig 自己的第一方构建工具也成为 BSP 客户端,用 dogfooding 保证第三方工具能拿到同等能力。

增量编译:x86_64-linux 用户全员可开

增量编译(改完代码后近乎即时的重新编译)在 0.17.0 里进入可日常使用的状态:大量 bug 被修复,上一版引入的新 ELF 链接器对增量编译的支持已经成熟。对大多数以 x86_64-linux 为目标的项目,加上两个参数就能启用:

bash
zig build -fincremental --watch

构建系统会监听源文件变化并做增量重建。新的 ELF 链接器虽然还没完全追平旧实现的特性集,所以默认仍处于关闭状态,但实际上已经能构建绝大多数 x86_64-linux 的 Zig 项目;在增量编译场景下它默认启用。官方的下一版目标是彻底用新实现替换旧 ELF 链接器。

这里有一条清晰的技术路线:0.16 引入新 ELF 链接器,0.17 让增量编译在 x86_64-linux 落地,后续版本计划加入 Mach-O 链接器和自托管的 aarch64 后端,并支持不依赖 --watch 的增量编译。对反馈循环敏感的开发者来说,这是 Zig 工程体验变化最大的一块。

语言变更:@bitCast 重定义与一批"还债"式清理

@bitCast 的定义被重写,这是本次最需要认真对待的语义变更。新定义是:按类型的"逻辑位表示"重新解释一个值。整数、浮点数从最低有效位到最高有效位;数组、向量的所有元素按顺序拼接。整数之间的转换、整数与 packed struct/union 之间的转换行为不变,但涉及数组或向量类型的 @bitCast 语义变了——而且这类改动可能不会触发编译错误,直接静默改变行为。官方的升级建议是:审计代码里所有涉及数组/向量类型的 @bitCast 调用。

新定义的好处是完全与字节序无关(endian-agnostic),同一份代码在大端、小端目标上行为一致;代价是 extern struct/extern union 之间的位转换被禁止——这类需求本来就是要重新解释内存布局,官方指路用 @ptrCast 或 extern union。

两个新内建函数替代了废弃的 @intFromEnum/@enumFromInt:

zig
const backing = @backingInt(color);      // 枚举 → 底层整数
const color2 = @fromBackingInt(2);       // 底层整数 → 枚举(结果类型自动推断)

@backingInt 对所有枚举生效,也能用于显式指定底层整数类型的 bitpack 和 tagged union;@fromBackingInt 传入无效的枚举 tag 值会触发安全检查的非法行为。@bitCast 在目标类型是枚举时现在也会做无效 tag 值的安全检查。空枚举的底层整数类型被要求为 noreturn,因为它们无法实例化。zig fmt 会自动完成这类迁移。

其他语言层面的变化:

  • @cImport 在 0.16 被标记废弃,0.17 正式移除。替代方案是显式的包依赖:zig fetch --save git+https://codeberg.org/ziglang/translate-c,然后在新翻译器上配置 C 源文件和系统库。官方 translate-c 包与主工具链解耦,可以独立发版,还开放了更多转换配置项。
  • @SpirvType 内建函数落地(提案 #35240),SPIR-V 独有的 sampler、image、sampled_image、runtime_array 类型可以在 Zig 里作为普通全局变量声明,不用再写内联汇编。
  • 新增 @divCeil,整数除法向正无穷取整,与 @divTrunc/@divFloor/@divExact 凑齐四件套,不用再写 std.math.divCeil(a, b) catch unreachable。
  • 数组乘法语法 a ** b 移除,统一用 @splat。
  • errdefer 的捕获语法 errdefer |err| 移除;官方给的迁移模式是把函数拆成内外两层,在内层函数上用 catch |err|。
  • i0 整数类型移除(这个类型本来就没有意义,可直接换成 u0)。
  • void{} 语法移除,统一用 {};comptime 长度切片(length slices)允许。
  • @hasDecl 行为收紧:现在只对 public 声明返回 true,不再对同文件内的非 public 声明放行。
  • 形式文法 grammar.peg 与实际解析器长期不一致的问题修复了。做法是把 grammar.peg 喂给工具生成一个递归下降解析器,作为 oracle 与手写的 std.zig.Ast.parse() 做模糊测试比对,保证单一事实来源。这为后续的语法变更和语言规范工作扫清了障碍。

标准库:DebugAllocator 换成线程安全实现

std.heap.DebugAllocator 被替换为线程安全的分配器,保证相当强硬:deinit 会报告所有泄漏并释放全部后备内存;大多数释放后的写入会直接段错误或被检测后 panic。实现上每次分配都跟着一个 AllocFooter 元数据块,用校验和保护,能捕获越写破坏。配套新增了两个模糊测试,检查内存不复用、返回内存可写、不被覆写,多线程版本在 TSAN 下做过大量验证。

效果有基准数据支撑:用 -Osafe 构建标准库测试,新旧对比(各 3 轮),新实现的 wall time 从 29.4s 降到 22.1s(-24.7%),峰值内存从 2.24GB 降到 1.11GB(-50.3%),cache miss 减少 14.9%,branch miss 减少 10.2%。

其他标准库变更:

  • std.zon.parse 重做:改用 struct 参数、arena 分配结果,fromSliceAlloc 改名 fromSlice,新增 updateFrom 系列方法可以在内存中直接更新值。
  • std.builtin 命名空间更名 std.lang;OptimizeMode 更名 Optimize,四个模式的值从 ReleaseSafe/ReleaseFast/ReleaseSmall 改为 safe/fast/small。
  • 一大批 API 改名收拢:mem.readPackedIntNative/readPackedIntForeign 统一为 mem.readPackedInt;bit_set 的 initEmpty/initFull 改为 empty/full;DoublyLinkedList.pop 改为 popLast;ascii.indexOfIgnoreCase 系列改为 findIgnoreCase 系列。
  • StackFallbackAllocator 重新设计,buffer 改为参数传入,与其他需要 buffer 的标准库 API 风格一致。
  • fmt.allocPrint 移到 mem.Allocator 上作为方法。
  • debug.SafetyLock 新增 lockShared/unlockShared 共享锁。

后端与目标平台:Wasm 后端行为测试追平 LLVM

自托管的 WebAssembly 后端在行为测试上达到 2060/2054(100%),追平了 LLVM 后端。不过因为还缺调试信息支持(#37032),debug 模式下它还不是默认后端。SPIR-V 后端变成多线程,新增 task/mesh shader 的调用约定,本周期修了 22 个 bug;SPIR-V 链接器也重写了,支持增量编译和链接外部 .spv 目标文件。

目标平台方面,aarch64-openbsd 进入 Tier 2 并在 CI 中原生测试;sparc64-linux 得益于新 ELF 链接器达到 Tier 2(官方称对新目标的支持已好于 LLD);新增 loongarch32-linux-gnu、xtensa-linux(经 C 后端)等目标;自托管的 loongarch64 后端有了初始实现(实验性)。也有收紧:powerpc-linux-gnueabi[hf] 因 glibc 只支持 "double-double" 的 long double 格式而被放弃,musl 变体保留。

工具链版本:LLVM 升到 22.1.8(zig cc/zig c++ 同步),glibc 交叉编译支持到 2.44,musl 为 1.2.5 加 backport,macOS 系统头文件为 27.0。上一版因 LLVM miscompilation 而被迫禁用的循环向量化优化 pass 在本版继续禁用——修复已进 LLVM 主线,但要等 0.18 升到 LLVM 23 才能重新启用。新增 zig objdump 子命令,配合 --redact/--elements 做链接器快照测试;--complexity 旗子可以统计 token 和 AST 节点数,官方用它展示了把 std.fmt.allocPrint 换成 arena.print 后代码复杂度的实际下降。

升级清单

把升级时最可能踩到的点按影响排序:

  1. 审计数组/向量类型的 @bitCast:语义变化可能不报编译错误,直接静默改变运行行为,这是官方点名要人工检查的项。
  2. @cImport 没了:迁移到 translate-c 包依赖,zig fetch --save git+https://codeberg.org/ziglang/translate-c。
  3. ZLS 暂不可用:构建系统拆分是破坏性变更,语言服务器要等协议适配。重度依赖 ZLS 的项目可以暂缓升级。
  4. errdefer |err| 捕获语法移除:按官方模式拆函数迁移。
  5. 命名空间/API 改名:std.builtin→std.lang、ReleaseSafe→safe 等一批机械替换。
  6. libc.txt 的 gcc_dir 改名 cc_dir,且在 Linux 目标上必填。
  7. 最低系统版本提高:Linux 内核 5.10+、macOS 15.0+、Windows 10+。

已知问题方面,std.debug.simple_panic 无法编译、std.Build.Step.Run 存在回归;官方也照例声明 0.17.x 仍有已知 bug 和误编译,非小型项目参与开发流程时可能需要跟进上游。

距离 1.0 还有多远

0.17.0 的变化组合起来看,Zig 团队的优先级很明确:先把工程地基打好(构建系统、缓存、链接器、增量编译),同时快速收敛语言设计(一个周期裁决约 150 份提案)。语言层的破坏性变更仍在发生(@bitCast 重定义、@cImport 移除),0.x 阶段的每个大版本都默认会破坏兼容性,这也是 Zig 社区熟知的 0.x 哲学。

对评估是否引入 Zig 的团队,0.17.0 的增量编译和 BSP 是两个实质性的工程体验改善,但 ZLS 的暂时不可用说明工具链生态仍处于剧烈重构期。观望者可以等 0.18(LLVM 23、循环向量化恢复、Wasm 后端默认化待定)再评估;已经在用 Zig 的项目,升级前优先做 @bitCast 审计。

来源: