"Correct" way to chain writers together?

So I’m downloading a file via std.http.Client and I want to print out the progress as bytes are downloaded, and additionally write those bytes to a file. Essentially what I want is a version of stream that works for WriterWriter.

For now, I have a wrapper writer that copies the buffer state of the file writer. This way, the Writer interface sees a non-empty buffer, and I don’t have to go through the interface twice (as compared to calling writeAll on the file writer from inside drain).

As far as I can tell, this works, but it feels unnecessarily convoluted.

Slightly simplified version of my implementation

ProgressWriter.zig:

const std = @import("std");
const Writer = std.Io.Writer;

sink: *Writer,
count: usize = 0,
/// Use this to perform writer operations.
interface: Writer,

pub fn init(sink: *Writer) @This() {
    return .{
        .sink = sink,
        .interface = .{
            .buffer = sink.buffer,
            .end = sink.end,
            .vtable = &.{
                .drain = &drain,
                .flush = &flush,
                .rebase = &rebase,
            },
        },
    };
}

/// Synchronise the sink to our state, so that the sink may drain properly.
pub fn sync(self: *@This()) void {
    self.sink.buffer = self.interface.buffer;
    self.sink.end = self.interface.end;
}

/// Synchronise our state to the sink's. Must be called after calling a function in the sink's vtable.
pub fn syncBack(self: *@This()) void {
    self.interface.buffer = self.sink.buffer;
    self.interface.end = self.sink.end;
}

pub fn writeProgress(self: *const @This()) void {
    std.debug.print("{}B\n", .{self.count});
}

fn drain(w: *Writer, data: []const []const u8, splat: usize) Writer.Error!usize {
    const pw: *@This() = @fieldParentPtr("interface", w);
    const buffered = pw.interface.end;
    const written = blk: {
        pw.sync();
        defer pw.syncBack();
        break :blk try pw.sink.vtable.drain(pw.sink, data, splat);
    };
    const new_buffered = pw.interface.end;
    pw.count += buffered + written - new_buffered;
    pw.writeProgress();
    return written;
}

fn flush(w: *Writer) Writer.Error!void {
    const pw: *@This() = @fieldParentPtr("interface", w);
    pw.sync();
    defer pw.syncBack();
    pw.count += pw.interface.end;
    pw.writeProgress();
    return pw.sink.vtable.flush(pw.sink);
}

fn rebase(w: *Writer, preserve: usize, capacity: usize) Writer.Error!void {
    const pw: *@This() = @fieldParentPtr("interface", w);
    pw.sync();
    defer pw.syncBack();
    return pw.sink.vtable.rebase(pw.sink, preserve, capacity);
}

And the use site looks like this:

fn downloadTheFile(client: *std.http.Client, fw: *std.Io.File.Writer) !void {
    var pw: ProgressWriter = .init(&fw.interface);
    _ = try client.fetch(.{
        // ...HTTP stuff...
        .response_writer = &pw.interface,
    });
    pw.sync();
    try fw.end();
}

I am confused, the existing ReaderWriter stream gives you the amount of bytes moved.

Your issue stems from using client.fetch, which abstracts away the reader. If you use the lower level client.request api (you can largely copy from fetch), then you will have direct access to the reader, and therefore the bytes moved each stream call.

1 Like

I had a look at the source code for fetch, and yeah you’re completely correct. Guess I’ll copy it over and modify to my needs. Thanks!