{
  "$type": "site.standard.document",
  "path": "/Zigs-New-Writer/",
  "publishedAt": "2026-04-16T13:12:46.932Z",
  "site": "https://www.openmymind.net",
  "tags": [
    "Zig's Writers",
    "Drain",
    "upcoming linked list changes",
    "vectored I/O",
    "The Interface",
    "Migrating",
    "Conclusion",
    "@This",
    "@fieldParentPtr"
  ],
  "textContent": "As you might have heard, Zig's `Io` namespace is being reworked. Eventually, this will mean the re-introduction of async. As a first step though, the Writer and Reader interfaces and some of the related code have been revamped.\n\n> This post is written based on a mid-July 2025 development release of Zig. It doesn't apply to Zig 0.14.x (or any previous version) and is likely to be outdated as more of the Io namespace is reworked.\n\nNot long ago, I wrote a blog post which tried to explain Zig's Writers. At best, I'd describe the current state as \"confusing\" with two writer interfaces while often dealing with `anytype`. And while `anytype` is convenient, it lacks developer ergonomics. Furthermore, the current design has significant performance issues for some common cases.\n\n### Drain\n\nThe new `Writer` interface is `std.Io.Writer`. At a minimum, implementations have to provide a `drain` function. Its signature looks like:\n\n\n     fn drain(w: *Writer, data: []const []const u8, splat: usize) Error!usize\n\nYou might be surprised that this is the method a custom writer needs to implemented. Not only does it take an array of strings, but what's that `splat` parameter? Like me, you might have expected a simpler `write` method:\n\n\n     fn write(w: *Writer, data: []const u8) Error!usize\n\nIt turns out that `std.Io.Writer` has buffering built-in. For example, if we want a `Writer` for an `std.fs.File`, we need to provide the buffer:\n\n\n     var buffer: [1024]u8 = undefined; var writer = my_file.writer(&buffer);\n\nOf course, if we don't want buffering, we can always pass an empty buffer:\n\n\n     var writer = my_file.writer(&.{});\n\nThis explains why custom writers need to implement a `drain` method, and not something simpler like `write`.\n\nThe simplest way to implement `drain`, and what a lot of the Zig standard library has been upgraded to while this larger overhaul takes place, is:\n\n\n     fn drain(io_w: *std.Io.Writer, data: []const []const u8, splat: usize) !usize { _ = splat; const self: *@This() = @fieldParentPtr(\"interface\", io_w); return self.writeAll(data[0]) catch return error.WriteFailed; }\n\nWe ignore the `splat` parameter, and just write the first value in `data` (`data.len > 0` is guaranteed to be true). This turns `drain` into what a simpler `write` method would look like. Because we return the length of bytes written, `std.Io.Writer` will know that we potentially didn't write all the data and call `drain` again, if necessary, with the rest of the data.\n\n> If you're confused by the call to `@fieldParentPtr`, check out my post on the upcoming linked list changes.\n\nThe actual implementation of `drain` for the `File` is a non-trivial ~150 lines of code. It has platform-specific code and leverages vectored I/O where possible. There's obviously flexibility to provide a simple implementation or a more optimized one.\n\n### The Interface\n\nMuch like the current state, when you do `file.writer(&buffer)`, you don't get an `std.Io.Writer`. Instead, you get a `File.Writer`. To get an actual `std.Io.Writer`, you need to access the `interface` field. This is merely a convention, but expect it to be used throughout the standard, and third-party, library. Get ready to see a lot of `&xyz.interface` calls!\n\nThis simplification of `File` shows the relationship between the three types:\n\n\n     pub const File = struct { pub fn writer(self: *File, buffer: []u8) Writer{ return .{ .file = self, .interface = std.Io.Writer{ .buffer = buffer, .vtable = .{.drain = Writer.drain}, } }; } pub const Writer = struct { file: *File, interface: std.Io.Writer, // this has a bunch of other fields fn drain(io_w: *std.Io.Writer, data: []const []const u8, splat: usize) !usize { const self: *Writer = @fieldParentPtr(\"interface\", io_w); // .... } } }\n\nThe instance of `File.Writer` needs to exist somewhere (e.g. on the stack) since that's where the `std.Io.Writer` interface exists. It's possible that `File` could directly have an `writer_interface: std.Io.Writer` field, but that would limit you to one writer per file and would bloat the `File` structure.\n\nWe can see from the above that, while we call `Writer` an \"interface\", it's just a normal struct. It has a few fields beyond `buffer` and `vtable.drain`, but these are the only two with non-default values; we have to provide them. The `Writer` interface implements a lot of typical \"writer\" behavior, such as a `writeAll` and `print` (for formatted writing). It also has a number of methods which only a `Writer` implementation would likely care about. For example, `File.Writer.drain` has to call `consume` so that the writer's internal state can be updated. Having all of these functions listed side-by-side in the documentation confused me at first. Hopefully it's something the documentation generation will one day be able to help disentangle.\n\n### Migrating\n\nThe new `Writer` has taken over a number of methods. For example, `std.fmt.formatIntBuf` no longer exists. The replacement is the `printInt` method of `Writer`. But this requires a `Writer` instance rather than the simple `[]u8` previous required.\n\nIt's easy to miss, but the `Writer.fixed([]u8) Writer` function is what you're looking for. You'll use this for any function that was migrating to `Writer` and used to work on a `buffer: []u8`.\n\nWhile migrating, you might run into the following error: _no field or member function named 'adaptToNewApi' in '...'_. You can see why this happens by looking at the updated implementation of `std.fmt.format`:\n\n\n     pub fn format(writer: anytype, comptime fmt: []const u8, args: anytype) !void { var adapter = writer.adaptToNewApi(); return adapter.new_interface.print(fmt, args) catch |err| switch (err) { error.WriteFailed => return adapter.err.?, }; }\n\nBecause this functionality was moved to `std.Io.Writer`, any `writer` passed into `format` has to be able to upgrade itself to the new interface. This is done, again only be convention, by having the \"old\" writer expose an `adaptToNewApi` method which returns a type that exposes a `new_interface: std.Io.Writer` field. This is pretty easy to implement using the basic `drain` implementation, and you can find a handful of examples in the standard library, but it's of little help if you don't control the legacy writer.\n\n### Conclusion\n\nI'm hesitant to provide opinion on this change. I don't understand language design. However, while I think this is an improvement over the current API, I keep thinking that adding buffering directly to the `Writer` isn't ideal.\n\nI believe that most languages deal with buffering via composition. You take a reader/writer and wrap it in a BufferedReader or BufferedWriter. This approach seems both simple to understand and implement while being powerful. It can be applied to things beyond buffering and IO. Zig seems to struggle with this model. Rather than provide a cohesive and generic approach for such problems, one specific feature (buffering) for one specific API (IO) was baked into the standard library. Maybe I'm too dense to understand or maybe future changes will address this more holistically.\n\nLeave a comment",
  "title": "Zig's new Writer",
  "updatedAt": "2025-07-17T00:00:00.000Z"
}