diff --git a/course/.vitepress/sidebar.ts b/course/.vitepress/sidebar.ts index f6aa15b6..945ed777 100644 --- a/course/.vitepress/sidebar.ts +++ b/course/.vitepress/sidebar.ts @@ -220,6 +220,14 @@ export default [ text: "版本说明", collapsed: true, items: [ + { + text: "0.16.0 升级指南", + link: "/update/upgrade-0.16.0", + }, + { + text: "0.16.0 版本说明", + link: "/update/0.16.0-description", + }, { text: "0.15.1 升级指南", link: "/update/upgrade-0.15.1", diff --git a/course/.vitepress/theme/config.ts b/course/.vitepress/theme/config.ts index 49d49919..08af1f81 100644 --- a/course/.vitepress/theme/config.ts +++ b/course/.vitepress/theme/config.ts @@ -1,3 +1,3 @@ -const version: string = "0.15.1"; +const version: string = "0.16.0"; export { version }; diff --git a/course/update/0.16.0-description.md b/course/update/0.16.0-description.md new file mode 100644 index 00000000..e5bf8396 --- /dev/null +++ b/course/update/0.16.0-description.md @@ -0,0 +1,363 @@ +--- +outline: deep +comments: false +showVersion: false +--- + +# `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 中原生测试 +- 新增 `aarch64-maccatalyst` 与 `x86_64-maccatalyst` 的交叉编译支持 +- 新增 `loongarch32-linux` 的初始支持,不过当前仍不支持 libc +- Alpha、KVX、MicroBlaze、OpenRISC、PA-RISC、SuperH 等架构加入了基础支持 +- Oracle Solaris、IBM AIX、z/OS 支持被移除;`illumos` 不受影响,仍然保留支持 +- 栈回溯支持进一步扩大,几乎所有主流目标在崩溃时都能得到更可靠的 stack trace + +对普通用户来说,这意味着 Zig 在常见 Linux / BSD / macOS / Windows 目标上的“可用性底线”又往前推了一步;而对于比较边缘的平台,官方也更清晰地区分了“支持”“实验性支持”和“不再支持”。 + +## 系统最低版本要求 + +| 操作系统(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 语义 +- `packed struct` / `packed union` 不再允许直接放指针字段 +- `extern` 场景下,`enum` 与 `packed` 类型不能再依赖隐式推断的底层整数类型 + +从设计上看,这些限制的方向非常统一:**凡是会影响 ABI 或精确内存布局的内容,Zig 都更倾向于要求你显式写出来。** + +### 向量语义进一步收紧 + +`0.16.0` 禁止了运行时向量索引,同时也不再鼓励通过旧式内存强转在数组和向量之间来回转换。简单来说,向量更明确地被当成“值语义上的 SIMD 数据”,而不是“碰巧可以按数组方式随便访问的内存”。 + +另外,小整数类型在“绝对不会丢精度”的前提下,现在可以安全地隐式转换为浮点类型,这也让数值代码更顺手了一些。 + +### 类型解析与依赖环错误大幅重做 + +`0.16.0` 还重做了编译器内部的类型解析流程。这个改动的影响非常深: + +- 许多以前会误报 dependency loop 的代码现在可以正常工作 +- 增量编译和普通编译之间的一致性明显增强 +- 一小部分本来就存在真实依赖环的代码,现在会更早、更明确地报错 + +如果你升级后遇到以前没见过的 dependency loop,先别急着回退版本。因为 `0.16.0` 的错误报告已经能更清楚地指出环路是怎么形成的,通常只要打断其中一条依赖即可。 + +## 标准库 + +### I/O 作为 Interface + +这是 `0.16.0` 最重头的内容,没有之一。 + +从这个版本开始,所有输入输出相关能力都围绕 `std.Io` 展开。更准确地说,凡是可能阻塞控制流,或会引入非确定性的操作,都被纳入了 `Io` 的抽象边界内。 + +当前官方提供了几种典型实现: + +- `Io.Threaded`:基于线程,功能最完整,也是从 `0.15.x` 升级时最接近旧行为的实现 +- `Io.Evented`:仍在实验阶段,用来推动接口演进 +- `Io.failing`:用于模拟“不支持任何操作”的环境 + +围绕它,标准库引入了整套新的任务和并发抽象: + +- `Future` +- `Group` +- `Batch` +- `Select` +- `Queue(T)` +- 统一的 cancelation 模型 + +这不仅是 API 改名,而是 Zig 对“并发 I/O 应该怎样进入语言生态”给出的新答案。文件系统、网络、进程、同步原语、定时器等能力,都围绕这套接口重新组织了。 + +### `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` 被移除,官方建议迁移到 `std.Io.async` / `std.Io.Group.async` +- `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 式“去历史包袱”整理。 + +### Windows 标准库实现继续下沉 + +Windows 也是 `0.16.0` 里非常有意思的一条线: + +- 网络 API 不再依赖 `ws2_32.dll`,而是直接基于 AFD 实现 +- 标准库继续向 NtDll 收敛 +- `std.Progress` 现在也支持 Windows 下的跨进程进度上报 + +这些工作虽然对多数用户不可见,但会真实影响程序的健壮性、性能,以及 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`。这使 Zig 离“摆脱对 LLVM 的库级依赖”又近了一步。 + +对普通用户来说,这更多体现为长期方向上的信号:Zig 仍在持续拆除自己对 LLVM 的深绑定。 + +### 类型解析重构 + +前面在“语言变动”里提过,`0.16.0` 大幅重做了类型解析。这件事对编译器本身还有两个非常重要的连锁收益: + +- 依赖环报错更可解释 +- 增量编译和普通编译之间的一致性更强 + +这也是为什么你会发现,本版本许多看似分散的改动,最后都会回到“为了更可靠的增量编译”这个主题上。 + +### 增量编译继续前进 + +`0.16.0` 的增量编译已经比 `0.15.x` 实用得多: + +- 大多数场景下减少了“过度重编译” +- LLVM 后端也开始支持增量编译 +- ELF 目标在 `-fincremental` 下会默认启用新的 ELF linker +- 稳定性明显提升,虽然依然不是默认开启 + +官方现在明确鼓励大家实际使用: + +```sh +zig build -fincremental --watch +``` + +当然,它仍然有已知 bug,甚至可能包含误编译;所以这项功能在 `0.16.0` 里依然不是默认值。 + +### 后端进展 + +这一轮里: + +- x86 自托管后端修了 11 个 bug,仍然是 Debug 模式下的默认后端 +- aarch64 后端因为 `std.Io` 带来的标准库 churn 暂时放慢了节奏 +- Zig 的 WebAssembly 后端目前通过了 1813 / 1970(约 92%)项行为测试 + +## 链接器(Linker) + +### 新 ELF Linker + +`0.16.0` 的新 ELF linker 可以通过 `-fnew-linker` 显式启用,或者在 build 脚本里设置 `exe.use_new_linker = true`。更重要的是:**在 `-fincremental` 且目标是 ELF 时,它现在会默认启用。** + +官方给出的一个数据点非常直观:对 Zig 编译器本体做单行改动时,旧 linker 需要大约 `194ms`,而新 linker 只需要 `65ms`,几乎接近“完全跳过链接”的速度。 + +这也意味着过去那种专门暴露 `-Dno-bin`、只求快速拿到编译错误的工作流,收益已经没有以前那么明显了。 + +不过要注意,新 linker 目前还没完全补齐旧 linker / LLD 的能力,例如生成物还缺少 DWARF 信息。所以它已经够快、够值得试,但还没有到“所有场景都能无脑切换”的程度。 + +## Fuzzer(模糊测试器) + +### `Smith` 取代 `[]const u8` + +Fuzz 测试接口是 `0.16.0` 里另一个会直接影响用户代码的 breaking change。过去 fuzz test 习惯接收 `[]const u8` 输入;现在统一改成 `*std.testing.Smith`,由它来生成结构化值。 + +这套接口不仅更适合做复杂输入生成,还支持权重、范围、哈希相关性等能力,明显比过去的“原始字节切片”模式更强。 + +### 多进程、多核与 crash dump + +除了接口变化之外,fuzzer 本身也更强了: + +- 现在可以利用多核,受 `-j` 控制 +- 多个 fuzz test 会自动轮换并优先运行更“有产出”的测试 +- 崩溃输入会自动落盘,便于复现 + +## Bug 修复 + +本轮发布周期内,Zig 一共关闭了 345 个 bug 报告。 + +不过官方也直说了:**这个版本仍然包含已知 bug、误编译和回归问题。** 对于稍微复杂一点的项目,使用 `0.16.x` 仍然意味着要准备好参与 issue 反馈、最小复现和版本试验。 + +这并不意外。因为 `0.16.0` 本质上是一个“大迁移版本”,它把很多还在演进中的长期工程方向一次性推到了用户面前。 + +## 工具链(Toolchain) + +### LLVM 21 + +`0.16.0` 升级到了 LLVM `21.1.0`,覆盖了 Clang、libc++、libc++abi、libunwind、libtsan 等组件。 + +不过这里有一个非常值得注意的 caveat:为了规避 LLVM 上游的严重回归,Zig 在 `0.16.x` 中**完全禁用了 loop vectorization**。这会让某些代码生成结果比理想情况更保守,但它仍然比“在常见配置下误编译 Zig 编译器自身”要好得多。 + +官方预计这个性能回退不止会影响 `0.16.x`,甚至还会延续到 `0.17.x`,大概率要等到 `0.18.x` 才会彻底解决。 + +### libc 与系统头文件更新 + +这一轮工具链同时带来了: + +- `musl 1.2.5`(附带安全修复回移) +- `glibc 2.43` +- Linux `6.19` headers +- macOS `26.4` headers +- FreeBSD `15.0` libc +- 交叉编译时支持动态链接的 OpenBSD libc + +### `zig libc` 继续扩张 + +`zig libc` 继续吞并原来来自 musl、MinGW-w64、WASI libc 的一部分 C 实现。`0.16.0` 中,随 Zig 分发的 C 源文件总数从 `2270` 降到了 `1873`,减少了约 `17%`。 + +这里尤其值得一提的是:很多数学函数,以及 `malloc` 相关函数,现在都已经进入 `zig libc` 的实现范围。 + +### `zig cc` + +`zig cc` / `zig c++` 现在基于 Clang `21.1.8`。这意味着 Zig 在“C / C++ 工具链外壳”这条线上,也继续和整体 LLVM 版本一同推进。 + +## 路线图(Roadmap) + +官方对 `0.17.0` 的规划很明确:这是一个相对短周期版本,主要目标是升级到 LLVM `22`,并完成“把构建执行阶段和 `build.zig` 配置阶段分离”的工作。 + +在这之后,更长期的大方向仍然是: + +- 继续完成并稳定语言本身 +- 做完 aarch64 后端,并让它成为 Debug 模式默认后端 +- 继续增强链接器,减少对 LLD 的依赖,并服务增量编译 +- 继续增强内置 fuzzer +- 继续把对 LLVM 的依赖,从“链接库”转向“调用 Clang 进程” + +如果说 `0.15.x` 还是“这些方向马上就要影响到你了”,那 `0.16.0` 就是“它们已经开始真正影响你的日常开发了”。 diff --git a/course/update/upgrade-0.16.0.md b/course/update/upgrade-0.16.0.md new file mode 100644 index 00000000..f69bdd5a --- /dev/null +++ b/course/update/upgrade-0.16.0.md @@ -0,0 +1,753 @@ +--- +outline: deep +showVersion: false +--- + +本篇文档将介绍如何从 `0.15.1` 版本升级到 `0.16.0`。 + +`0.16.0` 是 Zig 近几个版本里破坏性变更最集中的一次更新之一。最大的主题是:**I/O 被统一到了新的 `std.Io` 接口**,同时语言层也清理了 `@Type`、`@cImport`、`packed` / `extern` 类型规则,以及一批历史 API。 + +## 语言变动 + +### `switch` 能力继续增强 + +`0.16.0` 继续补齐了 `switch` 的一些语义细节。最直观的一点是:`packed struct` 和 `packed union` 现在也可以直接作为 prong item 使用,并且比较规则基于它们的 backing integer。 + +此外,下面这些行为也得到增强或修复: + +- 需要结果类型的表达式(例如 `@enumFromInt`)可以直接写进 `switch` prong +- 联合类型的 tag capture 不再只限于 `inline` prong +- 对 `void` 做 `switch` 时,不再一律强制要求 `else` +- 一些错误集合和 one-possible-value 类型上的历史问题被修复了 + +这部分通常不需要你主动迁移,但如果你有比较复杂的 `switch` 写法,可以顺便回头看一下是否能更简洁。 + +### `@cImport` 开始迁移到构建系统 + +`@cImport` 在 `0.16.0` 里还没有被移除,但已经被正式标记为 deprecated。官方建议开始把 C 头文件翻译工作迁移到 `build.zig` 中,通过 `addTranslateC` 生成模块,再在 Zig 代码里 `@import("c")`。 + +旧写法: + +```zig +pub const c = @cImport({ + @cInclude("stdio.h"); + @cInclude("math.h"); + @cInclude("stdlib.h"); +}); + +const c = @import("c.zig").c; +``` + +新写法: + +`c.h` + +```c +#include +#include +#include +``` + +`build.zig` + +```zig +const translate_c = b.addTranslateC(.{ + .root_source_file = b.path("src/c.h"), + .target = target, + .optimize = optimize, +}); + +const exe = b.addExecutable(.{ + .name = "example", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ + .name = "c", + .module = translate_c.createModule(), + }, + }, + }), +}); +``` + +```zig +const c = @import("c"); +``` + +如果你升级到 `0.16.0` 后发现同一份 C 头文件翻译结果和以前不一致,也不用急着怀疑自己。因为 `translate-c` 的底层实现已经从 `libclang` 切换到了 Aro,这类差异更应该视为 bug 并反馈给 Zig。 + +### `@Type` 被拆分为独立的类型构造内建 + +这是 `0.16.0` 最重要的语言级 breaking change 之一。`@Type` 被移除了,原本依赖 `@Type(.{ ... })` 或 `std.meta.*` 造类型的代码,需要迁移到新的内建函数。 + +新增的核心内建包括: + +- `@EnumLiteral()` +- `@Int()` +- `@Tuple()` +- `@Pointer()` +- `@Fn()` +- `@Struct()` +- `@Union()` +- `@Enum()` + +常见迁移: + +```zig +@Type(.enum_literal) +``` + +⬇️ + +```zig +@EnumLiteral() +``` + +```zig +@Type(.{ .int = .{ .signedness = .unsigned, .bits = 10 } }) +``` + +⬇️ + +```zig +@Int(.unsigned, 10) +``` + +```zig +std.meta.Tuple(&.{ u32, [2]f64 }) +``` + +⬇️ + +```zig +@Tuple(&.{ u32, [2]f64 }) +``` + +如果你的项目大量依赖元编程,这一项往往是升级时最先爆出来的报错来源。建议先全局搜索 `@Type(` 和 `std.meta.`,再逐个迁移。 + +### 小整数类型现在可以安全地隐式转换为浮点 + +如果某个整数类型的所有可能值,都能被目标浮点类型精确表示,那么现在可以直接发生隐式 coercion。 + +旧写法: + +```zig +var foo_int: u24 = 123; +var foo_float: f32 = @floatFromInt(foo_int); +``` + +新写法: + +```zig +var foo_int: u24 = 123; +var foo_float: f32 = foo_int; +``` + +注意这只适用于“不会丢精度”的情况。像 `u25 -> f32` 这种仍然需要显式写 `@floatFromInt`。 + +### 运行时向量索引被禁止 + +此前很多人会把向量当成“可在运行时索引的数组”来用。`0.16.0` 不再允许这种写法。 + +旧写法: + +```zig +for (0..vector_len) |i| { + _ = vector[i]; +} +``` + +新写法: + +```zig +const vector_type = @typeInfo(@TypeOf(vector)).vector; +const array: [vector_type.len]vector_type.child = vector; + +for (&array) |elem| { + _ = elem; +} +``` + +如果你确实需要逐项遍历向量,请先把它显式 coercion 成数组,再做索引或遍历。 + +### 数组与向量不再支持旧式内存强转 + +`0.16.0` 不再鼓励通过 `@ptrCast` 在数组内存和向量内存之间来回转换。如果你之前是在做同构数据的值级转换,请直接使用 coercion: + +```zig +const arr: [4]i32 = .{ 1, 2, 3, 4 }; +const vec: @Vector(4, i32) = arr; +const back: [4]i32 = vec; +``` + +如果你的类型外层还包了一层 `error!` 或其他容器类型,先解包,再在内部做数组和向量之间的转换。 + +### 不再允许返回局部变量地址 + +下面这种初学者常见错误,现在会直接给出明确的编译错误: + +```zig +fn foo() *i32 { + var x: i32 = 1234; + return &x; +} +``` + +如果你在升级后遇到这类错误,正确修复方式通常是三种之一: + +- 直接返回值,而不是返回指针 +- 让调用方传入 buffer / 输出参数 +- 改用堆分配,并明确约定释放责任 + +### `packed` 与 `extern` 规则更严格 + +#### `packed union` 需要明确 backing integer,并保证各字段 bit size 一致 + +以前 Zig 对 `packed union` 的位级布局有一些隐式推断。`0.16.0` 开始要求它更明确。 + +旧写法: + +```zig +const U = packed union { + x: u8, + y: u16, +}; +``` + +新写法: + +```zig +const U = packed union(u16) { + x: packed struct(u16) { + data: u8, + padding: u8 = 0, + }, + y: u16, +}; +``` + +总结一下这条规则:如果你需要 `packed union`,就请明确写出 backing integer,并保证每个字段都能映射到同样大小的位表示。 + +#### `packed struct` / `packed union` 不再允许指针字段 + +如果你过去把指针直接塞进 `packed` 类型里,现在需要改成整数保存地址: + +```zig +const addr: usize = @intFromPtr(ptr); +const ptr_again: *T = @ptrFromInt(addr); +``` + +这项变更的核心原因是:很多目标平台里,指针并不只是“裸地址位”,而 `packed` 类型又承诺了精确的位级布局,因此两者不再兼容。 + +#### `extern` 场景下必须显式指定 tag type / backing type + +`enum`、`packed struct`、`packed union` 只要被用于 `extern` / `export` 场景,就不能再依赖隐式推断的底层整数类型。 + +旧写法: + +```zig +const Enum = enum { a, b, c, d }; +const PackedStruct = packed struct { a: u4, b: u4 }; +const PackedUnion = packed union { a: u8, b: i8 }; +``` + +新写法: + +```zig +const Enum = enum(u8) { a, b, c, d }; +const PackedStruct = packed struct(u8) { a: u4, b: u4 }; +const PackedUnion = packed union(u8) { a: u8, b: i8 }; +``` + +如果你的类型需要跨 ABI 边界导出,请把 tag type 或 backing type 明确写出来,不要再依赖编译器推断。 + +### 浮点取整内建现在可以直接产出整数 + +`@floor`、`@ceil`、`@round`、`@trunc` 现在可以直接把浮点值转成整数值。 + +旧写法: + +```zig +const x: i32 = @intFromFloat(@round(value)); +``` + +新写法: + +```zig +const x: i32 = @round(value); +``` + +这项改动本身不一定会让旧代码报错,但会让很多“先取整、再转整数”的写法明显简化。 + +### 类型解析规则被重做,依赖环错误会更清晰 + +`0.16.0` 彻底重做了编译器内部的类型解析流程。结果是: + +- 大多数以前能工作的代码会继续工作 +- 一些以前会莫名其妙报 dependency loop 的代码,现在反而能正常工作 +- 也有一小部分旧代码会因为真正的依赖环而在 `0.16.0` 开始报错 + +最常见的症状,是结构体默认字段值、对 `@This()` 的对齐查询、`@typeInfo` 反射等路径互相依赖。 + +这类问题没有统一的机械式修法,但 `0.16.0` 的错误信息比以前清楚很多,通常你只需要打断这条环上的任意一条依赖即可。 + +## 标准库 + +### `std.Io` 成为新的核心 I/O 抽象 + +这是 `0.16.0` 最大的标准库变更。现在,凡是“可能阻塞控制流”或“会引入非确定性”的能力,基本都要求显式接收一个 `std.Io` 实例。 + +这意味着下面这些领域都围绕 `std.Io` 重新组织了: + +- 文件系统 +- 网络 +- 进程 +- 同步原语 +- 定时器与睡眠 +- 一部分流式读写接口 + +对于从 `0.15.x` 升级的项目来说,如果你只是想先获得和以前类似的行为,通常可以从 `Io.Threaded` 开始: + +```zig +var threaded: std.Io.Threaded = .init_single_threaded; +const io = threaded.io(); +``` + +但这只是临时过渡方案。更理想的做法,仍然是把 `io: std.Io` 作为参数一路向下传递,或者放到你的应用上下文里统一管理。 + +测试代码则建议优先使用 `std.testing.io`。 + +### `main` 现在可以直接接收 `std.process.Init` + +`0.16.0` 给 `main` 函数增加了一个非常实用的新入口。你可以直接在参数里拿到已经初始化好的常用资源: + +- `init.gpa` +- `init.io` +- `init.arena` +- `init.environ_map` +- `init.preopens` + +新写法示例: + +```zig +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const io = init.io; + try std.Io.File.stdout().writeStreamingAll(io, "Hello, world!\n"); +} +``` + +`main` 现在有三种合法形态: + +- `pub fn main() ...`:仍然合法,但拿不到参数和环境变量 +- `pub fn main(init: std.process.Init.Minimal) ...`:只拿到原始 `argv` / `environ` +- `pub fn main(init: std.process.Init) ...`:拿到完整预初始化资源 + +### 环境变量与命令行参数不再是全局状态 + +`0.16.0` 之后,环境变量和进程参数被明确收敛到 `main` 的初始化参数里,不再鼓励像以前一样把它们当作“随处可取的全局状态”。 + +读取参数: + +```zig +const std = @import("std"); + +pub fn main(init: std.process.Init.Minimal) void { + var args = init.args.iterate(); + while (args.next()) |arg| { + std.log.info("arg: {s}", .{arg}); + } +} +``` + +读取环境变量: + +```zig +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + for (init.environ_map.keys(), init.environ_map.values()) |key, value| { + std.log.info("env: {s}={s}", .{ key, value }); + } +} +``` + +如果你的库函数仍然需要环境变量,请改成显式传参,或者显式接收 `*const std.process.Environ.Map`。 + +### 进程 API 的入口被重新整理 + +围绕新的 `std.Io`,进程相关 API 也发生了明显变化。 + +启动子进程: + +```zig +var child = std.process.Child.init(argv, gpa); +child.stdin_behavior = .Pipe; +child.stdout_behavior = .Pipe; +child.stderr_behavior = .Pipe; +try child.spawn(io); +``` + +⬇️ + +```zig +var child = try std.process.spawn(io, .{ + .argv = argv, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .pipe, +}); +``` + +运行并捕获输出: + +```zig +const result = try std.process.run(allocator, io, .{ + .argv = argv, +}); +``` + +替换当前进程镜像: + +```zig +const err = std.process.replace(io, .{ .argv = argv }); +``` + +### `std.Thread.Pool` 被移除 + +`std.Thread.Pool` 已经从标准库中移除。最常见的迁移方向,是改用 `std.Io.async` 或 `std.Io.Group.async`。 + +如果你过去用的是“提交一组任务,然后等待全部结束”的模式,通常可以这样迁移: + +```zig +fn doAllTheWork(io: std.Io) !void { + var group: std.Io.Group = .init; + errdefer group.cancel(io); + + group.async(io, doSomeWork, .{ io, &group, first_work_item }); + try group.await(io); +} +``` + +另外要特别注意:如果你的旧代码里除了 `Thread.Pool` 之外,还用了 `Thread.Mutex`、`Thread.Condition`、`Thread.ResetEvent` 等同步原语,那么升级到 `std.Io` 时,它们也应该一起迁到对应的 `std.Io.*` 类型。 + +### `std.io` 进一步收敛到 `std.Io` + +这一轮更新里,`GenericReader`、`AnyReader`、`FixedBufferStream` 等历史接口继续退出。 + +常见映射: + +- `std.io` ➡️ `std.Io` +- `std.Io.GenericReader` ➡️ `std.Io.Reader` +- `std.Io.AnyReader` ➡️ `std.Io.Reader` +- `std.leb.readUleb128` ➡️ `std.Io.Reader.takeLeb128` +- `std.leb.readIleb128` ➡️ `std.Io.Reader.takeLeb128` + +读取固定缓冲区: + +```zig +var fbs = std.io.fixedBufferStream(data); +const reader = fbs.reader(); +``` + +⬇️ + +```zig +var reader: std.Io.Reader = .fixed(data); +``` + +写入固定缓冲区: + +```zig +var fbs = std.io.fixedBufferStream(buffer); +const writer = fbs.writer(); +``` + +⬇️ + +```zig +var writer: std.Io.Writer = .fixed(buffer); +``` + +### 文件系统和路径 API 有一批实用迁移点 + +#### `readFileAlloc` + +旧写法: + +```zig +const contents = try std.fs.cwd().readFileAlloc(allocator, file_name, 1234); +``` + +新写法: + +```zig +const contents = try std.Io.Dir.cwd().readFileAlloc(io, file_name, allocator, .limited(1234)); +``` + +注意新的限制语义更严格:到达上限本身也会报错,错误名也从 `FileTooBig` 变成了 `StreamTooLong`。 + +#### `readToEndAlloc` + +旧写法: + +```zig +const contents = try file.readToEndAlloc(allocator, 1234); +``` + +新写法: + +```zig +var file_reader = file.reader(&.{}); +const contents = try file_reader.interface.allocRemaining(allocator, .limited(1234)); +``` + +#### 当前目录 API 更名 + +旧写法: + +```zig +std.process.getCwd(buffer) +std.process.getCwdAlloc(allocator) +``` + +新写法: + +```zig +std.process.currentPath(io, buffer) +std.process.currentPathAlloc(io, allocator) +``` + +#### `fs.path.relative` 变成纯函数 + +旧写法: + +```zig +const relative = try std.fs.path.relative(gpa, from, to); +defer gpa.free(relative); +``` + +新写法: + +```zig +const cwd_path = try std.process.currentPathAlloc(io, gpa); +defer gpa.free(cwd_path); + +const relative = try std.fs.path.relative(gpa, cwd_path, environ_map, from, to); +defer gpa.free(relative); +``` + +也就是说,`relative` 不再自己偷偷读取当前工作目录和环境变量,而是要求你把这些上下文显式传进去。 + +#### `File.Stat.atime` 现在是可选值 + +读取访问时间: + +```zig +stat.atime +``` + +⬇️ + +```zig +stat.atime orelse return error.FileAccessTimeUnavailable +``` + +设置时间戳: + +```zig +try file.setTimestamps(io, src_stat.atime, src_stat.mtime); +``` + +⬇️ + +```zig +try file.setTimestamps(io, .{ + .access_timestamp = .init(src_stat.atime), + .modify_timestamp = .init(src_stat.mtime), +}); +``` + +#### 其他值得顺手处理的文件系统改动 + +- `std.fs.wasi.Preopens` ➡️ `std.process.Preopens` +- 原来的 `atomicFile` 流程重构为 `createFileAtomic` +- `fs.getAppDataDir` 已被移除,应用应自行决定“应用数据目录”的策略 + +### `std.posix` 和 `std.os.windows` 的中层 API 被移除 + +这次标准库很明确地砍掉了很多“中不溜”的系统接口。如果你升级后是在 `std.posix` 或 `std.os.windows` 里踩雷,官方建议只选两条路: + +- 往上走,改用 `std.Io` +- 往下走,直接使用 `std.posix.system` + +也就是说,Zig 不再想长期维护那批半高层、半底层的历史包装函数。 + +### `std.mem` 的 “index of” 系列统一更名为 “find” + +`std.mem` 现在统一使用 `find` 作为“查找子串位置”的概念名称,并新增了 `cut`、`cutPrefix`、`cutSuffix`、`cutScalar`、`cutLast`、`cutLastScalar` 等函数。 + +如果你项目里大量用了 `indexOf` / `lastIndexOf` / `indexOfScalar` 这类 API,可以统一按新的 `find*` 命名规则做搜索替换。 + +### 容器继续向 unmanaged 方向收敛 + +这部分延续了 `0.14` 和 `0.15` 的趋势:标准库越来越倾向于“容器本身不持有 allocator,把 allocator 显式传给需要分配的方法”。 + +这次比较关键的变化有: + +- `ArrayHashMap`、`AutoArrayHashMap`、`StringArrayHashMap` 被移除 +- `AutoArrayHashMapUnmanaged` ➡️ `std.array_hash_map.Auto` +- `StringArrayHashMapUnmanaged` ➡️ `std.array_hash_map.String` +- `ArrayHashMapUnmanaged` ➡️ `std.array_hash_map.Custom` +- `PriorityQueue` 和 `PriorityDequeue` 都继续往 `.empty` / `push` / `pop` 风格迁移 + +`PriorityQueue` 常见重命名: + +- `init` ➡️ `initContext` +- `add` ➡️ `push` +- `addUnchecked` ➡️ `pushUnchecked` +- `addSlice` ➡️ `pushSlice` +- `remove` ➡️ `pop` +- `removeOrNull` ➡️ `pop` +- `removeIndex` ➡️ `popIndex` + +`PriorityDequeue` 常见重命名: + +- `init` ➡️ `.empty` +- `add` ➡️ `push` +- `addSlice` ➡️ `pushSlice` +- `addUnchecked` ➡️ `pushUnchecked` +- `removeMinOrNull` / `removeMin` ➡️ `popMin` +- `removeMaxOrNull` / `removeMax` ➡️ `popMax` +- `removeIndex` ➡️ `popIndex` + +### 分配器与并发模型继续调整 + +有两条需要直接注意: + +- `std.heap.ArenaAllocator` 现在变成了 thread-safe 且 lock-free +- `std.heap.ThreadSafeAllocator` 被移除 + +如果你的旧代码是“在外层包一层 `ThreadSafeAllocator`”,现在应改为直接选用本身适合并发场景的 allocator,或者改造调用结构,避免再依赖这层包装器。 + +### Fuzz 测试接口改成 `std.testing.Smith` + +如果你的项目用到了 fuzz 测试,这也是一个直接的 breaking change。 + +旧写法: + +```zig +const std = @import("std"); + +fn fuzzTest(_: void, input: []const u8) !void { + var sum: u64 = 0; + for (input) |b| sum += b; + try std.testing.expect(sum != 1234); +} +``` + +新写法: + +```zig +const std = @import("std"); + +fn fuzzTest(_: void, smith: *std.testing.Smith) !void { + var sum: u64 = 0; + while (!smith.eosWeightedSimple(7, 1)) { + sum += smith.value(u8); + } + try std.testing.expect(sum != 1234); +} +``` + +同时,`0.16.0` 的 fuzzer 还支持了多进程、多核利用和崩溃输入落盘。如果你本来就依赖 fuzz 测试,这次升级是值得顺手把整套流程一起更新的。 + +## 构建系统 + +### 依赖目录改到项目本地 `zig-pkg` + +从 `0.16.0` 开始,依赖包不再解压到全局缓存目录下,而是会被拉到项目根目录旁边的 `zig-pkg` 目录中。 + +这带来两个直接结果: + +- 调试依赖更方便,能直接 grep、编辑、替换 +- 你通常不应该把 `zig-pkg` 提交进仓库,但如果团队确实想这么做,也不是不允许 + +### `build.zig.zon` 的依赖信息更严格 + +这次升级后,如果依赖缺少 `fingerprint`,或者 `name` 还在用字符串而不是 enum literal,`zig build` 会直接失败。 + +另外,旧的 hash 格式也已经不再支持。也就是说,`0.15.x` 时还能勉强工作的某些老 `build.zig.zon`,到 `0.16.0` 可能需要顺手全部更新一遍。 + +### 可以通过 `zig build --fork` 临时覆写依赖 + +新加入的 `--fork=[path]` 允许你在不改 `build.zig.zon` 的前提下,临时把整棵依赖树中的某个包替换成本地目录里的 fork。 + +```sh +zig build --fork=/path/to/your-package +``` + +这对排查生态 breakage、联调依赖、离线开发都很有帮助。 + +### 新的错误输出与测试超时选项 + +`0.16.0` 的 `zig build` 新增或调整了这些常用参数: + +- `--test-timeout`:为每个 Zig 单元测试设置超时 +- `--error-style`:控制构建错误输出样式 +- `--multiline-errors`:控制多行错误信息展示方式 + +其中,旧的 `--prominent-compile-errors` 已被移除。对应的新写法是: + +```sh +zig build --error-style minimal +``` + +如果你平时配合 `--watch` 或增量编译工作流,`verbose_clear` / `minimal_clear` 这两种 error style 会比较顺手。 + +### 临时文件 API 被重构 + +这次构建系统还清理了旧的临时目录 API: + +- `Build.makeTempPath` 被移除 +- `RemoveDir` step 被移除 + +迁移方向是: + +- 用 `Build.addTempFiles` 创建非缓存的临时文件目录 +- 用 `Build.addMutateFiles` 表达“会修改文件”的流程 +- 用 `Build.tmpPath` 作为便捷入口 + +如果你以前是在 configure 阶段预先创建临时目录,再在 make 阶段清理,`0.16.0` 之后应该把这套逻辑迁到新的 `WriteFile` / temp files API 上。 + +### `builtin.subsystem` 被移除,`Target.SubSystem` 也迁了位置 + +如果你的代码依赖 `std.builtin.subsystem`,现在需要重新设计:真正的 subsystem 直到链接阶段才知道,编译期再去猜它并不可靠。 + +另外,`std.Target.SubSystem` 被移动到了 `std.zig.Subsystem`。旧名字目前仍有 deprecated alias,可暂时过渡,但新代码最好直接跟着新命名走。 + +### 增量编译与新 ELF linker 继续前进 + +`0.16.0` 里,增量编译已经明显比 `0.15.x` 更实用了: + +- LLVM 后端也开始支持增量编译 +- 在 ELF 目标上,`-fincremental` 会默认启用新的 ELF linker +- 对很多项目来说,`-Dno-bin` 的收益已经不再明显 + +常见工作流现在可以直接写成: + +```sh +zig build -fincremental --watch +``` + +不过也要注意: + +- 增量编译依然不是默认开启 +- 依然存在已知 bug 和误编译 +- 新 ELF linker 还不完整,例如目前生成物仍然缺少 DWARF 信息 + +换句话说,`0.16.0` 的增量编译已经值得日常试用,但如果你碰到诡异问题,仍要记得先排除它。