Resetting of write/read buffer

In situations where an Io.Writer/Reader is reused with the same buffer, we want to ensure there is no garbage left over in the buffer from a previous failed write/read. Do you prefer always doing this before or after (via defer). E.g:

const std = @import("std");
const mem = std.mem;
const testing = std.testing;
const Io = std.Io;

pub fn writeStartup(args: struct {
    writer: Io.Writer,
    protocol_version: [4]u8 = .{ 0, 3, 0, 0 },
    user: []const u8,
    database: []const u8,
}) !void {
    // `{len:i32}{version:i32}user\0{user}\0database\0{database}\0\0
    // length = 4 + 4 + 4 + 1 + user.len + 1 + 8 + 1 + database.len + 1 + 1
    const length = 25 + args.user.len + args.database.len;
    var w = args.writer;

    // which is better:
    w.end = 0; // this
    defer w.end = 0; // or this

    try w.writeInt(u32, @intCast(length), .big);
    _ = try w.write(&args.protocol_version);
    _ = try w.write(&[_]u8{ 'u', 's', 'e', 'r', 0 });
    _ = try w.write(args.user);
    try w.writeByte(0);
    _ = try w.write(&[_]u8{ 'd', 'a', 't', 'a', 'b', 'a', 's', 'e', 0 });
    _ = try w.write(args.database);
    _ = try w.writeByte(0);
    try w.flush();
}

That is not garbage data, it is data that was buffered but not yet written to wherever the writer writes to. Setting end to 0 beforehand just discards it to never be written, that would be a hair pulling bug.

And doing it after is pointless since flush will write the buffered data, setting end to 0 in the process.

a failed write should not leave written data in the buffer, and a failed read won’t add more data to the buffer. Ofc if using a higher level function which reads/writes multiple times, an earlier read/write may have succeeded and modified the buffer.

Regardless such a thing would be done in an error branch, and certainly in a function further up the call stack.


write returns how much was written, which may be less than what you intended, discarding it is a bug. Instead, use writeAll, which calls write in a loop until all data is written.

You can "foobar\x00" to add a 0 to the end of a string literal, or you can do a separate writeByte like you already do with non literals.

Avoid calling flush in functions like this, you call it at the point where you are truly done writing data, when you want to ensure any data in the buffer is written to wherever. It is easy to imagine the caller of this function wanting to write more data afterwards, if that is not already the case. By calling flush often you deminish the value of it being buffered in the firstplace, that is to minimise the underlying writes/reads.

Even if that functio is always the last to write, I still argue for the caller to flush for clarity.

6 Likes

Thanks so much for all the pointers. I agree flush should be done by the caller. Theoretically if it wasn’t i guess it would have been better to errdefer writer.end=0 iso defer to avoid the redundant end=0 after flushing

Maybe you don’t care about the buffered data on the error path, but I still wouldn’t make that decision in such a function.

2 Likes