# Zig 0.16.0 Release Notes Migration Reference Primary source: https://ziglang.org/download/0.16.0/release-notes.html Use this file as the first stop when upgrading or reviewing Zig 0.16 code. It is organized in the same broad order as the official release notes and converts the release-note material into practical coding guidance. ## Version Posture Zig 0.16.0 is not a small stdlib polish release. It changes ownership of I/O, synchronization, file system access, process spawning, time, randomness, containers, C translation, and several language edge cases. General rule for new Zig 0.16 code: - APIs that can block, touch the OS, use entropy, query time, spawn work, or otherwise introduce nondeterminism should accept or store a `std.Io`. - Application entry points should get `io` from Juicy Main (`std.process.Init`) when possible. - Tests should prefer `std.testing.io`. - A local `std.Io.Threaded.init_single_threaded` is acceptable only as a temporary adapter at a boundary that cannot yet receive an `io`. ## Target Support The release notes broaden native CI coverage and add/remove several targets. The important migration guidance is: - Do not assume an untested target is broken just because old Zig versions were rough there; 0.16 improves stack traces and weakly-ordered architecture behavior. - Minimum OS versions changed. Current notable minimums include Linux 5.10, macOS 13, Windows 10, FreeBSD 14.0, NetBSD 10.1, OpenBSD 7.8, and DragonFly BSD 6.0. - If a product's target matrix depends on older OS versions, check the official support table before promising support. - More targets have usable stack traces, which makes debug/unwind behavior more valuable but also means verbose crash/log paths may become more expensive if left in hot paths. ## Language Changes ### switch 0.16 extends valid switch prong expressions and fixes several switch edge cases. Practical notes: - Packed structs/unions can appear as switch prong items and compare by backing integer. - Decl literals and result-typed expressions such as `@enumFromInt` are more broadly usable in prongs. - Union tag captures are allowed for all prongs, not just inline prongs. - Switch prong captures cannot all be discarded. - Error switches have stricter and more consistent unreachable-else handling. ### @cImport Moves Toward the Build System `@cImport` is deprecated as the long-term C translation API. The official upgrade path is: 1. Put includes in a real header. 2. Add a build-system `b.addTranslateC(...)` step. 3. Import `translate_c.createModule()` into the Zig module graph. For ABI-sensitive bindings: - Prefer build-system translation for new C bindings. - Keep translated bindings stable and check generated ABI-sensitive structs with comptime size/alignment/offset assertions. - If generated C output differs across translation approaches, treat it as a bug-risk investigation, not cosmetic churn. ### @Type Removed `@Type` is replaced by specific type-constructing builtins: - `@EnumLiteral` - `@Int` - `@Tuple` - `@Pointer` - `@Fn` - `@Struct` - `@Union` - `@Enum` - `@Opaque` Migration rule: - Use the narrow builtin that matches the type you are constructing. - Keep `@typeInfo` for reflection. - Replace old `@Type(.{ .@"struct" = ... })` helpers with `@Struct(...)`, and similarly for unions/enums/pointers/functions. ### Numeric and Vector Changes 0.16 allows small integer types to coerce to floats in more cases, but it also tightens vector and array representation rules. Practical notes: - Runtime vector indexes are forbidden. Use scalar extraction patterns, compile-time indexes, or restructure the vector operation. - Vectors and arrays no longer support in-memory coercion. Make conversions explicit with loads, stores, or element-wise construction. - Unary float builtins forward result type. Code such as `const x: f64 = @sqrt(@floatFromInt(n));` now works as intended. - `@floor`, `@ceil`, `@round`, and `@trunc` can convert floats to integer result types. `@intFromFloat` is now redundant with `@trunc` and is deprecated. ### Returning Local Addresses The compiler now diagnoses trivially returning the address of an expired local variable. Review rule: - If a function returns a pointer, confirm the pointee outlives the function. - Prefer caller-provided output buffers, allocator-owned results, or stable owner structs. ### Packed and Extern Type Tightening 0.16 makes packed/extern layout more explicit: - Packed union fields must have an unambiguous backing bit size. - Pointers are forbidden in packed structs and packed unions; store an integer address only when that is actually the ABI. - Packed unions may specify explicit backing integers. - Enum and packed types in extern contexts need explicit backing types. Binding rule: - For ABI-sensitive C bindings, preserve or add comptime checks for size, alignment, and field offsets. - Do not accept layout churn in translated bindings without checking the C header contract. ### Type Resolution 0.16 reworks type resolution. Some dependency loops disappear, while other previously accepted self-dependent constructs are now rejected with clearer diagnostics. Practical notes: - Do not assume every new dependency-loop diagnostic is a false positive. - Prefer moving self-referential size/alignment queries behind explicit helper types or runtime fields. - Lazy field analysis means more namespace-like types can exist without forcing full field resolution. - Pointers to comptime-only types are no longer themselves comptime-only, but dereferencing them at runtime is still invalid. - Explicitly aligned pointer types are distinct from naturally aligned pointer types, even if they coerce easily. - Zero-bit tuple fields are no longer implicitly marked comptime in type info. ## Standard Library ### Top-Level Additions, Removals, and Renames Important removals/renames: - `SegmentedList` removed. - `std.meta.declList` removed. - `std.Io.GenericWriter`, `std.Io.AnyWriter`, `std.Io.null_writer`, and `std.Io.CountingReader` removed. - `std.Thread.Mutex.Recursive` removed. - `std.fmt.format` is replaced by `std.Io.Writer.print`. - `std.fmt.Formatter` is renamed to `std.fmt.Alt`. - `std.fmt.FormatOptions` is renamed to `std.fmt.Options`. - `std.fmt.bufPrintZ` is renamed to `std.fmt.bufPrintSentinel`. - `std.DynLib` removed Windows support; use platform APIs (`LoadLibraryExW`, `GetProcAddress`) directly or through a local abstraction. - `BitSet` and `EnumSet` use decl literals instead of `initEmpty` / `initFull`. Important error changes: - `error.RenameAcrossMountPoints` and `error.NotSameFileSystem` become `error.CrossDevice`. - `error.SharingViolation` becomes `error.FileBusy`. - `error.EnvironmentVariableNotFound` becomes `error.EnvironmentVariableMissing`. - `std.Io.Dir.rename` returns `error.DirNotEmpty` rather than `error.PathAlreadyExists` for non-empty destination directories. ### I/O as an Interface The core 0.16 rule: all input/output functionality requires an `std.Io` instance. Anything that might block, interact with the outside world, depend on the OS, wait on concurrency, use entropy, or introduce nondeterminism belongs under the `Io` interface. Main patterns: ```zig const std = @import("std"); pub fn main(init: std.process.Init) !void { const gpa = init.gpa; const io = init.io; _ = gpa; _ = io; } ``` Temporary adapter when no owner can pass `io` yet: ```zig var threaded: std.Io.Threaded = .init_single_threaded; const io = threaded.io(); ``` Testing: ```zig const io = std.testing.io; ``` I/O implementations mentioned by the release notes: - `std.Io.Threaded`: feature-complete threaded implementation and closest behavior to 0.15 blocking APIs. - `std.Io.Evented`: experimental M:N/user-space stack switching implementation. - `std.Io.Uring`, `std.Io.Kqueue`, `std.Io.Dispatch`: early or platform-specific implementations. - `std.Io.failing`: no-operation/failing implementation for unsupported contexts and tests. ### Future, Group, Cancelation, and Batch New task-level and operation-level APIs live under `std.Io`. Guidance: - `io.async(...)` creates a future for function-level task independence. - `std.Io.Group` manages many tasks and can await or cancel them together. - `std.Io.Batch` is lower-level and operation-oriented; use it when you need efficient independence among I/O operations rather than arbitrary Zig functions. - Propagate `error.Canceled` unless the code that requested cancelation is the code handling it. - If you handle `error.Canceled` locally and continue, use `io.recancel()` when the cancelation should remain active. - Prefer `errdefer group.cancel(io)` after spawning grouped work so task resources are released on all exits. ### Synchronization Primitives Synchronization that can block must migrate to `std.Io` equivalents so it cooperates with the chosen I/O backend. Map: - `std.Thread.ResetEvent` -> `std.Io.Event` - `std.Thread.WaitGroup` -> `std.Io.Group` - `std.Thread.Futex` -> `std.Io.Futex` - `std.Thread.Mutex` -> `std.Io.Mutex` - `std.Thread.Condition` -> `std.Io.Condition` - `std.Thread.Semaphore` -> `std.Io.Semaphore` - `std.Thread.RwLock` -> `std.Io.RwLock` - `std.once` removed Use cancelable locks when cancelation should be honored: ```zig try mutex.lock(io); defer mutex.unlock(io); ``` Use uncancelable locks only for short critical sections or cleanup paths: ```zig mutex.lockUncancelable(io); defer mutex.unlock(io); ``` Lock-free atomics do not need `std.Io`. ### Entropy and Random Entropy moved under `std.Io`. Patterns: ```zig var bytes: [32]u8 = undefined; io.random(&bytes); const rng_source: std.Random.IoSource = .{ .io = io }; const rng = rng_source.interface(); ``` Use `io.randomSecure(...)` when fresh OS-backed cryptographic entropy is required and failures should be reported. ### Time The old wall-clock/monotonic split is now routed through `std.Io` time types for clock operations that may depend on the runtime. Migration highlights from the release notes: - `std.time.Instant` -> `std.Io.Timestamp` - `std.time.Timer` -> `std.Io.Timestamp` - `std.time.timestamp` -> `std.Io.Timestamp.now` Application preference: - Put common timestamp reads behind a shared application helper so callsites do not reinvent `Io.Timestamp` clock-selection or conversion rules. - Store `io` on timing systems that need repeated timestamps instead of constructing local fallback `Io.Threaded` instances. ### File System Most file system APIs moved from `std.fs` handles to `std.Io.Dir` and `std.Io.File`. Basic patterns: ```zig const cwd = std.Io.Dir.cwd(); const file = try cwd.openFile(io, "data.txt", .{}); defer file.close(io); var buf: [4096]u8 = undefined; var reader = file.reader(io, &buf); const bytes = try reader.interface.allocRemaining(gpa, .limited(1024 * 1024)); defer gpa.free(bytes); ``` ```zig const out = try std.Io.Dir.cwd().createFile(io, "out.txt", .{}); defer out.close(io); var buf: [4096]u8 = undefined; var writer = out.writer(io, &buf); try writer.interface.print("value={d}\n", .{42}); try writer.interface.flush(); ``` Convenience methods: ```zig const bytes = try std.Io.Dir.cwd().readFileAlloc(io, "data.txt", gpa, .limited(1024 * 1024)); defer gpa.free(bytes); try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = "out.txt", .data = "hello\n", }); ``` Common migrations: - `file.close()` -> `file.close(io)` - `std.fs.cwd()` -> `std.Io.Dir.cwd()` - `std.fs.File.stdout()` -> `std.Io.File.stdout()` - `std.fs.Dir.readFileAlloc(...)` -> `std.Io.Dir.readFileAlloc(io, ..., .limited(max))` - `std.fs.File.readToEndAlloc(...)` -> file reader + `reader.interface.allocRemaining(...)` - `fs.copyFileAbsolute` and other absolute helpers move to `std.Io.Dir.*Absolute` - Many `Z` and `W` path-specific helpers were removed; use the cross-platform `[]u8` path APIs. ### Networking and HTTP Networking moved under `std.Io.net`, and higher-level clients hold an `io`. Patterns: ```zig var client: std.http.Client = .{ .allocator = gpa, .io = io, }; defer client.deinit(); ``` The release notes call out that DNS, parallel connection attempts, and cancelation now work through the chosen `Io` implementation. Do not bypass this with direct platform sockets in new generic code. ### Process, Args, Env, and Preopens Process APIs now use `std.Io`, and Juicy Main makes args/env/preopens non-global. Patterns: ```zig pub fn main(init: std.process.Init) !void { const io = init.io; const gpa = init.gpa; const args = try init.minimal.args.toSlice(init.arena.allocator()); _ = args; _ = gpa; _ = io; } ``` Run a child and capture output: ```zig const result = try std.process.run(gpa, io, .{ .argv = &.{ "git", "status", "--short" }, .stdout_limit = .limited(64 * 1024), .stderr_limit = .limited(64 * 1024), }); defer gpa.free(result.stdout); defer gpa.free(result.stderr); ``` Spawn a child: ```zig var child = try std.process.spawn(io, .{ .argv = &.{ "tool", "--flag" }, .stdout = .pipe, .stderr = .pipe, }); defer child.kill(io); const term = try child.wait(io); _ = term; ``` Environment guidance: - Prefer `init.environ_map` from `std.process.Init` at app boundaries. - `error.EnvironmentVariableNotFound` is now `error.EnvironmentVariableMissing`. - Current directory is now `std.process.currentPath(io, buffer)` or `std.process.currentPathAlloc(io, allocator)`. - WASI preopens moved to `std.process.Preopens` and are available through Juicy Main. ### File.MemoryMap Memory-mapped file APIs moved under `std.Io.File.MemoryMap`. Treat maps as file-backed I/O resources and keep their lifetime explicit. ### posix and os.windows Removals Several low-level stdlib wrappers were removed as part of moving blocking/nondeterministic operations behind `std.Io`. Prefer the `std.Io` abstraction for portable code. Drop down to `std.posix` or `std.os.windows` only for explicit platform-specific code. ### Allocators `heap.ArenaAllocator` is now thread-safe and lock-free. `heap.ThreadSafe` allocator was removed. If shared allocation still needs protection, choose an allocator that is already thread-safe or place synchronization at the owner of the shared data. ### Compression Compression/decompression APIs continue moving toward `std.Io.Reader` and `std.Io.Writer`. LZMA, LZMA2, and XZ are specifically called out as updated. Deflate compression was added and decompression simplified. Review rule: - Do not reintroduce old `std.io.GenericReader` adapter patterns for compressors. ### Debug Information Debug information was reworked and improved for several kinds of types. Expect better type names and better error-set runtime names, but also keep debug/unwind cost in mind for hot logging paths. ### Inter-Process Progress on Windows Progress reporting now has better Windows inter-process support. Prefer std build/progress APIs over custom ad hoc pipes for build step progress where practical. ### Windows Networking and NtDll Networking no longer requires `ws2_32.dll` in the same way old code did, and more Windows implementation moved toward NtDll. Prefer std `Io.net` when writing portable networking code. ### mem cut/find APIs The mem API emphasizes `find*` naming and adds `cut*` helpers. Common names in 0.16: - `std.mem.find` - `std.mem.findLast` - `std.mem.findScalar` - `std.mem.findScalarLast` - `std.mem.findAny` - `std.mem.findNone` - `std.mem.cut` - `std.mem.cutLast` - `std.mem.cutScalar` - `std.mem.cutPrefix` - `std.mem.cutSuffix` Avoid adding new `indexOf` / `lastIndexOf` style calls in 0.16 code. ### Directory Walking and Paths `std.Io.Dir.walkSelectively` was added for recursive walks where the walker decides which directories to enter. `std.fs.path` behavior changed for Windows path handling: - UNC, rooted, and drive-relative paths are handled more consistently. - `relative`, `relativeWindows`, and `relativePosix` are now pure and require the current directory path and sometimes an environment map as input. Pattern: ```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, init.environ_map, from, to); defer gpa.free(relative); ``` ### File.Stat Access time is optional: ```zig const stat = try file.stat(io); const atime = stat.atime orelse return error.FileAccessTimeUnavailable; ``` Timestamp-setting APIs also use structured options rather than positional atime/mtime values. ### Atomic and Temporary Files Atomic temporary file handling now uses `std.Io.File.Atomic` and the `Io` entropy path. Do not hand-roll random temp names when std already provides an atomic file helper. ### Current Directory API Use: - `std.process.currentPath(io, buffer)` - `std.process.currentPathAlloc(io, allocator)` Avoid old `std.process.getCwd*` patterns. ### Migration to Unmanaged Containers 0.16 continues the container migration toward allocator-free fields and allocator-at-call-site methods. Important changes: - `ArrayHashMap`, `AutoArrayHashMap`, and `StringArrayHashMap` removed. - `AutoArrayHashMapUnmanaged` -> `std.array_hash_map.Auto` - `StringArrayHashMapUnmanaged` -> `std.array_hash_map.String` - `ArrayHashMapUnmanaged` -> `std.array_hash_map.Custom` - `PriorityQueue` and `PriorityDequeue` no longer store allocators. ### PriorityQueue and PriorityDequeue Priority containers now prefer `.empty` and push/pop terminology. Common migrations: - `init` -> `.empty` or `initContext` - `add` -> `push` - `addSlice` -> `pushSlice` - `addUnchecked` -> `pushUnchecked` - `remove` / `removeOrNull` -> `pop` - `removeIndex` -> `popIndex` - PriorityDequeue min/max variants become `popMin` / `popMax` ### Thread.Pool Removed `std.Thread.Pool` is removed. Migrate simple independent jobs to `std.Io.async` or `std.Io.Group.async` when they are actually asynchronous from the caller. If tasks synchronize with the caller or each other, re-evaluate the design rather than doing a mechanical replacement. Any blocking synchronization used by `Io` tasks must move from `std.Thread.*` primitives to `std.Io.*` primitives. ### subsystem APIs `std.builtin.subsystem` was removed. `std.Target.SubSystem` moved to `std.zig.Subsystem` with field-name updates, while deprecated aliases remain for some build-script compatibility. ### Reader/Writer Removals `std.io` is now `std.Io`. Migration map: - `std.Io.GenericReader` -> `std.Io.Reader` - `std.Io.AnyReader` -> `std.Io.Reader` - `std.io.fixedBufferStream(data).reader()` -> `var r: std.Io.Reader = .fixed(data)` - `std.io.fixedBufferStream(buffer).writer()` -> `var w: std.Io.Writer = .fixed(buffer)` - `std.leb.readUleb128` / `readIleb128` -> `std.Io.Reader.takeLeb128` ### Duration Formatting The `{D}` duration format specifier was removed. Format `std.Io.Duration` with `{f}`. ```zig try writer.print("{f}", .{std.Io.Duration{ .nanoseconds = ns }}); ``` ### fs.getAppDataDir Removed Application data directory policy is now application-owned. Use app-specific logic or a third-party package such as known-folders if desired. ### Io.Writer.Allocating `std.Io.Writer.Allocating` now stores an `alignment` field. Prefer the provided initializers rather than field literals unless the field set is intentional and complete. ### Crypto 0.16 adds AES-SIV, AES-GCM-SIV, and the Ascon AEAD/hash constructions. Existing crypto code also needs the entropy migration described above. ## Build System ### Local Package Overrides The build system can override packages locally. Use this for development overrides rather than editing dependency cache contents. ### Project-Local Package Fetching Packages can be fetched into a project-local directory. This is useful for reproducible workspaces and local depot workflows. ### Unit Test Timeouts `zig build test --test-timeout ` can bound tests by real time. Use this for runaway tests, but remember scheduler load can make real-time limits flaky. ### Error Formatting New flags: - `--error-style verbose|minimal|verbose_clear|minimal_clear` - `--multiline-errors indent|newline|none` `--prominent-compile-errors` was removed. Use `--error-style minimal` for the closest behavior. ### Temporary Files `Build.makeTempPath` and the RemoveDir step are gone. Use: - `b.addTempFiles` - `b.addMutateFiles` - `b.tmpPath` - `std.Build.Step.WriteFile` in temporary/mutate modes Do not create temporary directories during the configure phase and then mutate them during make. ## Compiler ### C Translation Translate-c is now based on Aro/translate-c rather than libclang. The change is intended to be non-breaking, but generated code can differ. For ABI-sensitive libraries: - Regenerate translated C bindings deliberately. - Compare struct layouts, enum values, constants, calling conventions, and macro translation. - Keep compile-time asserts next to translated bindings. ### LLVM Backend The LLVM backend has experimental incremental compilation support, smaller bitcode output, some compile-time improvements, and better debug info for several type cases. ### Type Resolution Compiler type resolution changed significantly. See the language section above for source-level effects. ### Incremental Compilation Incremental compilation is more usable but still disabled by default. Try `zig build -fincremental --watch` for local iteration, but do not treat it as a required CI mode yet. ### x86 and Other Backends The x86 backend remains the default for Debug mode and has faster compile times than LLVM, with lower machine-code quality. Other self-hosted backends continue to mature. ## Linker The new ELF linker is available with `-fnew-linker` or build-script options and is default when using incremental compilation for ELF. It is faster for incremental relinks but not feature-complete, especially around debug information. Use it for iteration when it works; validate release/QA builds on the intended linker path. ## Fuzzer Fuzz tests now use `*std.testing.Smith` instead of a raw `[]const u8` input. `Smith` generates structured values, bytes, slices, and weighted choices. Migration rule: - Update fuzz entry points to accept `*std.testing.Smith`. - Generate inputs through Smith rather than manually slicing the old byte stream. ## Toolchain 0.16 updates major toolchain components including LLVM 21, musl 1.2.5, glibc 2.43, Linux 6.19 headers, macOS 26.4 headers, MinGW-w64, FreeBSD 15.0 libc, WASI libc, and zig libc/zig cc updates. Practical effects: - Cross-compilation behavior can change even when Zig code did not. - Re-check C ABI integration and platform feature detection after upgrading. - Treat changed C translation output as worthy of investigation, especially for ABI-sensitive or platform bindings. ## Application Upgrade Checklist When upgrading an application to Zig 0.16: 1. Prefer `pub fn main(init: std.process.Init) !void` for applications and tools. 2. Pass `init.io` down through setup rather than constructing local `Io.Threaded` instances. 3. Use `std.testing.io` in tests. 4. Add `io: std.Io` fields to long-lived systems, allocators, registries, timers, and queues that must perform blocking work later. 5. Use `std.Io.Mutex`/`Condition`/`Semaphore` when the lock can block. Use `lockUncancelable(io)` only for short critical sections where cancellation must not interrupt cleanup or queue integrity. 6. Migrate file operations to `std.Io.Dir`/`std.Io.File`. 7. Keep platform-specific DynamicLib behavior behind a local abstraction on Windows. 8. Preserve translated-C ABI asserts and expand them when translation output changes. 9. Replace old `std.crypto.random` usage with `io.random` or `std.Random.IoSource`. 10. Replace old process helpers with `std.process.run(gpa, io, ...)` or `std.process.spawn(io, ...)`. 11. Replace old `std.Thread.Pool` assumptions with an application scheduler or carefully designed `std.Io.Group` usage. 12. Keep migration notes updated when the codebase adopts application-specific policy decisions.