Pass BufferedWriter as argument

Hi, just started learning zig (v0.13) and this is my first post here.

I was wondering about title and upon searching for it I only found answers for generic writes, not buffered.
I’ve tried a bunch of things but ran out of ideas:

const std = @import("std");

// fn usage(buffer: std.fs.File.Writer) !void {
// fn usage(buffer: std.io.buffered_writer.BufferedWriter) !void {
// fn usage(buffer: std.io.BufferedWriter) !void {
// fn usage(buffer: std.io.BufferedWriter.Writer) !void {
// fn usage(buffer: std.io.Writer) !void {
fn usage(buffer: anytype) !void {
    try buffer.writeAll("lol");
}

pub fn main() !void {
    const stdout_file = std.io.getStdOut().writer();
    var bw = std.io.bufferedWriter(stdout_file);
    const stdout = bw.writer();

    try usage(stdout);
    try bw.flush();
}

So, anytype Does work, but im not entirely happy with it

Doing

fn usage(buffer: std.fs.File.Writer) !void {

Gives: (aside: can i wrap text in preformatted?)

error: expected type 'io.GenericWriter(fs.File,error{DiskQuota,FileTooBig,InputOutput,NoSpaceLeft,DeviceBusy,InvalidArgument,AccessDenied,BrokenPipe,SystemResources,OperationAborted,NotOpenForWriting,LockViolation,WouldBlock,ConnectionResetByPeer,Unexpected},(function 'write'))', found 'io.GenericWriter(*io.buffered_writer.BufferedWriter(4096,io.GenericWriter(fs.File,error{DiskQuota,FileTooBig,InputOutput,NoSpaceLeft,DeviceBusy,InvalidArgument,AccessDenied,BrokenPipe,SystemResources,OperationAborted,NotOpenForWriting,LockViolation,WouldBlock,ConnectionResetByPeer,Unexpected},(function 'write'))),error{DiskQuota,FileTooBig,InputOutput,NoSpaceLeft,DeviceBusy,InvalidArgument,AccessDenied,BrokenPipe,SystemResources,OperationAborted,NotOpenForWriting,LockViolation,WouldBlock,ConnectionResetByPeer,Unexpected},(function 'write'))'

Ok, seems it should be io.GenericWriter(*io.buffered_writer.BufferedWriter…

fn usage(buffer: std.io.buffered_writer.BufferedWriter) !void {

And with that

error: root struct of file 'io' has no member named 'buffered_writer'

Oh, lets remove that

fn usage(buffer: std.io.BufferedWriter) !void {

But

error: expected type 'type', found 'fn (comptime usize, comptime type) type'

So it seems thats a function, not a type? The docs Zig Documentation say Type Function. Dont fully know what that means but i can see it making sense. In the same docs, under Types, it list Writer so lets try with that

fn usage(buffer: std.io.BufferedWriter.Writer) !void {

gives

error: type 'fn (comptime usize, comptime type) type' does not support field access

And trying a more generic approach?

fn usage(buffer: std.io.Writer) !void {

gives

error: expected type 'type', found 'fn (comptime type, comptime type, comptime anytype) type'

And thats it. Cant think what to try next. Thanks in advance

I’m sorry, that’s how it is for now.

A type function is a function that returns a type. So it’s still ultimatly a function. You must either use the correct concrete type (if there’s only one), or anytype.

I agree that, while anytype is a good abstraction in itself and that it has its use cases, it’s a bad replacement for compile-time interfaces imo. It doesn’t convey intent precisely and doesn’t work well with the tooling. But that’s how it is for now and hey, at least it works as intended.

Edit: another possibility is using a dynamic interface like std.io.AnyWriter which is an actual type, it makes sense if you’re using dynamic libraries or smth, but there’s the overhead of using virtual functions, keep that in mind.

1 Like

I see. Thanks fro the quick reply :slight_smile:

There is a long discussion at replace anytype · Issue #17198 · ziglang/zig · GitHub about anytype in similar use cases.

The linked comment in particular has an interesting suggestion to continue using anytype as the parameter type, but to wrap it in a comptime function where you can assert the presence of particular fields or methods.

I haven’t yet read or written enough Zig to say whether that style is idiomatic at all, but it does seem to provide an intermediate solution until the language evolves more.

If you look at the source code for std.io.bufferedWriter:

pub fn bufferedWriter(underlying_stream: anytype) BufferedWriter(4096, @TypeOf(underlying_stream)) {
    return .{ .unbuffered_writer = underlying_stream };
}

The type it is returning is a generic, parameterized with two values:

  1. The size of the buffer (4096)
  2. The type it is wrapping (in this case we know the type that will be passed in, std.fs.File.Writer)

So the type returned from std.io.bufferedWriter will be std.io.BufferedWriter(4096, std.fs.File.Writer).

However this still results in a compile error:

const std = @import("std");

fn usage(buffer: std.io.BufferedWriter(4096, std.fs.File.Writer)) !void {
    try buffer.writeAll("lol");
}

pub fn main() !void {
    const stdout_file = std.io.getStdOut().writer();
    var bw = std.io.bufferedWriter(stdout_file);
    const stdout = bw.writer();

    try usage(stdout);
    try bw.flush();
}
~/tmp〉zig run pass-buffered-writer.zig
pass-buffered-writer.zig:12:15: error: expected type 'io.buffered_writer.BufferedWriter(4096,io.GenericWriter(fs.File,error{DiskQuota,FileTooBig,InputOutput,NoSpaceLeft,DeviceBusy,InvalidArgument,AccessDenied,BrokenPipe,SystemResources,OperationAborted,NotOpenForWriting,LockViolation,WouldBlock,ConnectionResetByPeer,Unexpected},(function 'write')))', found 'io.GenericWriter(*io.buffered_writer.BufferedWriter(4096,io.GenericWriter(fs.File,error{DiskQuota,FileTooBig,InputOutput,NoSpaceLeft,DeviceBusy,InvalidArgument,AccessDenied,BrokenPipe,SystemResources,OperationAborted,NotOpenForWriting,LockViolation,WouldBlock,ConnectionResetByPeer,Unexpected},(function 'write'))),error{DiskQuota,FileTooBig,InputOutput,NoSpaceLeft,DeviceBusy,InvalidArgument,AccessDenied,BrokenPipe,SystemResources,OperationAborted,NotOpenForWriting,LockViolation,WouldBlock,ConnectionResetByPeer,Unexpected},(function 'write'))'
    try usage(stdout);
              ^~~~~~
/nix/store/jybk0d4ivdxh1bhx8nw5ryg37m3zf3xq-zig-0.13.0/lib/std/io.zig:309:12: note: struct declared here
    return struct {
           ^~~~~~
/nix/store/jybk0d4ivdxh1bhx8nw5ryg37m3zf3xq-zig-0.13.0/lib/std/io/buffered_writer.zig:7:12: note: struct declared here
    return struct {
           ^~~~~~
pass-buffered-writer.zig:3:39: note: parameter type declared here
fn usage(buffer: std.io.BufferedWriter(4096, std.fs.File.Writer)) !void {
                 ~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    callMain: /nix/store/jybk0d4ivdxh1bhx8nw5ryg37m3zf3xq-zig-0.13.0/lib/std/start.zig:524:32
    callMainWithArgs: /nix/store/jybk0d4ivdxh1bhx8nw5ryg37m3zf3xq-zig-0.13.0/lib/std/start.zig:482:12
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

If we take a look at the BufferedWriter type now:

pub fn BufferedWriter(comptime buffer_size: usize, comptime WriterType: type) type {
    return struct {
        unbuffered_writer: WriterType,
        buf: [buffer_size]u8 = undefined,
        end: usize = 0,

        pub const Error = WriterType.Error;
        pub const Writer = io.Writer(*Self, Error, write);

        const Self = @This();

        pub fn flush(self: *Self) !void {
            try self.unbuffered_writer.writeAll(self.buf[0..self.end]);
            self.end = 0;
        }

        pub fn writer(self: *Self) Writer {
            return .{ .context = self };
        }

        pub fn write(self: *Self, bytes: []const u8) Error!usize {
            if (self.end + bytes.len > self.buf.len) {
                try self.flush();
                if (bytes.len > self.buf.len)
                    return self.unbuffered_writer.write(bytes);
            }

            const new_end = self.end + bytes.len;
            @memcpy(self.buf[self.end..new_end], bytes);
            self.end = new_end;
            return bytes.len;
        }
    };
}

It contains another type called Writer that is returned when you call writer():

pub const Writer = io.Writer(*Self, Error, write);

So our full type is std.io.BufferedWriter(4096, std.fs.File.Writer).Writer.

Trying it out:

const std = @import("std");

fn usage(buffer: std.io.BufferedWriter(4096, std.fs.File.Writer).Writer) !void {
    try buffer.writeAll("lol");
}

pub fn main() !void {
    const stdout_file = std.io.getStdOut().writer();
    var bw = std.io.bufferedWriter(stdout_file);
    const stdout = bw.writer();

    try usage(stdout);
    try bw.flush();
}
~/tmp〉zig run pass-buffered-writer.zig
lol

But in general I agree that the way the standard library handles generic writers isn’t great.

3 Likes

Thats interesting. And precise, if sadly a bit verbose and unwieldy. Thanks!
It does make me wonder if it could be generalized a bit for no specific buffer size
something like

fn usage(buffer: std.io.BufferedWriter(usize, std.fs.File.Writer).Writer) !void {}

but thisgives

error: expected type 'usize', found 'type'

You’re on the right track. You can do this by accepting a comptime parameter and passing it in to std.io.BufferWriter():

fn usage(comptime buffer_size: usize, buffer: std.io.BufferedWriter(buffer_size, std.fs.File.Writer).Writer) !void {
    try buffer.writeAll("lol");
}

Here’s the full version:

//! pass-generic-size-buffered-writer.zig

const std = @import("std");

fn usage(comptime buffer_size: usize, buffer: std.io.BufferedWriter(buffer_size, std.fs.File.Writer).Writer) !void {
    try buffer.writeAll("lol");
}

pub fn main() !void {
    const stdout_file = std.io.getStdOut().writer();
    var bw = std.io.bufferedWriter(stdout_file);
    const stdout = bw.writer();

    try usage(4096, stdout);
    try bw.flush();
}
~/tmp〉zig run pass-generic-size-buffered-writer.zig
lol

Getting the size of the buffer “automatically” takes a bit more work:

pub fn main() !void {
    // ... snip
    try usage(@sizeOf(@TypeOf(bw.buf)), stdout);
    // ... snip
}

But it also works:

~/tmp〉zig run pass-generic-size-buffered-writer.zig
lol

Anymore “automatic” than that will require using anytype.

3 Likes