Hacker News

Writergate: Zig I/O Interface Overhaul

Writergate Writergate is the informal name for Zigโ€™s I/O interface overhaul that began in late 2023 and culminated in August 2025 with the complete removal of GenericWriter , GenericReader , AnyWriter , and AnyReader . If youโ€™ve touched Zig I/O code recently, youโ€™ve felt the impact. What changed The old API used generic types with type parameters: // Old (removed) const stdout = std.io.getStdOut(); const writer = stdout.writer(); try writer.print("Hello {s}\n", .{"world"}); The new API uses concrete types with vtables and explicit buffering: // New (0.15+) const stdout = std.fs.File.stdout(); var buffer: [4096]u8 = undefined; var file_writer = stdout.writer(&buffer); const writer = &file_writer.interface; defer writer.flush() catch {}; try writer.print("Hello {s}\n", .{"world"}); The breaking changes: - Namespace: std.io becamestd.Io - Buffering: Caller provides the buffer, not the implementation - Types: Writer/Reader are concrete types with vtables, not generics - Flush: You must flush explicitly; output may not appear without it Why it matters The old generic design poisoned APIs: any function accepting a writer became generic, which forced all containing structs to become generic. Andrew Kelleyโ€™s Writergate PR describes the old interface as โ€œpoisoning structs that contain themโ€. Iโ€™ve seen this pattern infect entire codebases: one anytype parameter spreads until half your library is generic. It limited API reusability and hurt compile times. The follow-up in Zig 0.16 treats I/O like memory allocation: code depends on an Io instance the same way it depends on an Allocator . This enables: - Async: The 0.16 Io vtable includesasync ,await , andcancel primitives. Same code works with thread pools today, io_uring or kqueue as those backends mature. - Performance: Buffer sits above the vtable, so buffered writes donโ€™t hit virtual dispatch in hot paths. - Precise errors: Instead of anyerror everywhere, backend operations carry specific error sets; the Writer/Reader interfaces expose a compactWriteFailed /ReadFailed , with details kept on the concrete implementation. The vtable architecture The new system has three levels: Io (Backend) โ† Threaded, Evented, Uring... (0.16) โ†“ Io.Writer / Io.Reader โ† drain, stream, flush, rebase โ†“ File.Writer / File.Reader โ† Concrete implementations Custom writers embed the interface and recover the parent via @fieldParentPtr : pub const MyWriter = struct { my_data: u32, interface: std.Io.Writer, fn drain(io_w: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { const self: *MyWriter = @alignCast(@fieldParentPtr("interface", io_w)); _ = self.my_data; // Can access parent struct fields // Process buffered + incoming data, return bytes consumed. // Every slice counts once, except the last: it repeats splat times. io_w.end = 0; var total: usize = 0; for (data[0 .. data.len - 1]) |slice| total += slice.len; total += data[data.len - 1].len * splat; return total; } }; Common pitfalls Iโ€™ve hit all of these at least once: - Forgetting flush: Bytes still sitting in the buffer at exit are silently lost. A short program runs, prints nothing, exits successfully. Maddening. - Format specifier: Use "{f}" for types withformat methods, not"{}" - Standard streams: std.io.getStdOut() is nowstd.fs.File.stdout() - Copying interfaces: Never copy an interface embedded in a parent implementation ( var w = impl.interface ); always use pointers (&impl.interface ). The vtable recovers the parent with@fieldParentPtr , and the copy breaks that. Standalone writers likeWriter.fixed are plain values and copy fine. See the migration guide for details.

Read on Hacker News ↗ ← Back to News

Comments

No comments yet. Start the discussion.