Zig 0.16's I/O overhaul — the community calls it Writergate — replaced the old generic reader/writer plumbing with explicit vtable-based std.io.Reader and std.io.Writer interfaces. For a codebase like ABI, where the database, the network layer, and the GPU pipeline all stream bytes, that change touched everything: 95 files modified before the build went green again.
ENGINEERING · January 8, 2026 · 6 min read
Surviving Writergate: Migrating ABI to Zig 0.16
Zig 0.16 rebuilt std.io around vtable-based Reader and Writer interfaces. Ninety-five files later, here's what the migration actually looked like — and the patterns we standardized on.
By MLAI Runtime Engineering
What actually broke
- Every anytype writer parameter — the old duck-typing pattern — needed an explicit interface type.
- Buffering moved into the caller's hands; the new interfaces make the buffer visible instead of hiding it behind generics.
- Error sets narrowed. Code that relied on inferred error unions through generic writers had to name its failures.
- Container init churn landed at the same time: we standardized on ArrayListUnmanaged.empty and explicit allocator passing at every call site.
// 0.15 — generic duck typing
fn serialize(self: *Block, writer: anytype) !void {
try writer.writeAll(self.header());
}
// 0.16 — explicit vtable interface
fn serialize(self: *Block, writer: *std.io.Writer) !void {
try writer.writeAll(self.header());
}The migration was mechanical but not mindless. The new interfaces are better — buffer ownership is explicit, error sets are honest, and the vtable indirection costs less than the comptime bloat the old pattern generated across 95 instantiation sites.
Pin everything
The operational lesson: dev-channel Zig moves fast enough that unpinned CI is a time bomb. We pin the exact toolchain in build.zig.zon, mirror the pin in CI, and gate merges on a version-check script that fails loudly when the local compiler drifts from the pin. The same discipline now applies to 0.17.0-dev as that migration begins.
# fail fast when the toolchain drifts from the pin
zig version | grep -q "0.16.0-dev" || {
echo "toolchain drift: expected pinned 0.16.0-dev"; exit 1;
}
./build.sh checkNinety-five files sounds like a war story, but the honest summary is gentler: Zig's churn is loud, shallow, and finite. The language keeps trading short-term breakage for long-term explicitness — and for a database that promises deterministic latency, explicitness is the whole brand.