Implementing a `std.io.Reader`

I’m trying to implement something that adheres to the reader interface so I can use its many conveniences.

I have a function that will return 4 bytes or an error (readSII4ByteFP) and I would like to wrap it in a Reader.

I think I am close but I am having trouble. I have been looking at std.io.fixedBufferStream as an example.

pub const SIIStream = struct {
    port: *Port,
    station_address: u16,
    retries: u32,
    recv_timeout_us: u32,
    eeprom_timeout_us: u32,
    eeprom_address: u32, // WORD (2-byte) address

    last_four_bytes: [4]u8 = .{ 0, 0, 0, 0 },
    remainder: u2 = 0,

    pub fn init(
        port: *Port,
        station_address: u16,
        eeprom_address: u16,
        retries: u32,
        recv_timeout_us: u32,
        eeprom_timeout_us: u32,
    ) SIIStream {
        return SIIStream{
            .port = port,
            .station_address = station_address,
            .eeprom_address = eeprom_address,
            .retries = retries,
            .recv_timeout_us = recv_timeout_us,
            .eeprom_timeout_us = eeprom_timeout_us,
        };
    }

    const ReadError = error{};

    pub fn reader(self: *SIIStream) std.io.Reader {
        return std.io.AnyReader(@This(), @This().ReadError, read);
    }

    fn read(self: *SIIStream, buf: []u8) !usize {
        if (self.remainder == 0) {
            self.last_four_bytes = try readSII4ByteFP(
                self.port,
                self.station_address,
                self.eeprom_address, // eeprom address is WORD address
                self.retries,
                self.recv_timeout_us,
                self.eeprom_timeout_us,
            );
            self.eeprom_address += 2;
        }

        if (buf.len >= 4) {
            @memcpy(buf[0..4], self.last_four_bytes);
            self.remainder = 0;

            return 4;
        } else {
            @memcpy(buf, self.last_four_bytes[0..buf.len]);
            self.remainder -%= @as(u2, @intCast(buf.len));
            return buf.len;
        }
    }
};

And do I need to handle slices with zero length?

1 Like

What kind of trouble? Trouble compiling or trouble getting it working right?

One thing that sticks out to me:

These are not congruent.

  • Your read function should have ReadError as its error set (i.e. ReadError!usize)
  • The possible errors from readSII4ByteFP should be in your ReadError error set

Another important note for implementing a reader is in the readFn doc comment:

If readSII4ByteFP returns an error on EOF, then you’ll want to handle that and return 0 instead (most likely).

1 Like

You are correct about my errors.

There was a deprecation warning for std.io.Reader() so I switched to using std.io.GenericReader()

pub const SIIStream = struct {
    port: *Port,
    station_address: u16,
    retries: u32,
    recv_timeout_us: u32,
    eeprom_timeout_us: u32,
    eeprom_address: u16, // WORD (2-byte) address

    last_four_bytes: [4]u8 = .{ 0, 0, 0, 0 },
    remainder: u2 = 0,

    pub fn init(
        port: *Port,
        station_address: u16,
        eeprom_address: u16,
        retries: u32,
        recv_timeout_us: u32,
        eeprom_timeout_us: u32,
    ) SIIStream {
        return SIIStream{
            .port = port,
            .station_address = station_address,
            .eeprom_address = eeprom_address,
            .retries = retries,
            .recv_timeout_us = recv_timeout_us,
            .eeprom_timeout_us = eeprom_timeout_us,
        };
    }

    pub const ReadError = error{
        Timeout,
        SocketError,
    };

    pub fn reader(self: *SIIStream) std.io.GenericReader(*@This(), ReadError, read) {
        return .{ .context = self };
    }

    fn read(self: *SIIStream, buf: []u8) ReadError!usize {
        if (self.remainder == 0) {
            self.last_four_bytes = try readSII4ByteFP(
                self.port,
                self.station_address,
                self.eeprom_address, // eeprom address is WORD address
                self.retries,
                self.recv_timeout_us,
                self.eeprom_timeout_us,
            );
            self.eeprom_address += 2;
        }

        if (buf.len >= 4) {
            @memcpy(buf[0..4], self.last_four_bytes[0..]);
            self.remainder = 0;

            return 4;
        } else {
            @memcpy(buf, self.last_four_bytes[0..buf.len]);
            self.remainder -%= @as(u2, @intCast(buf.len));
            return buf.len;
        }
    }
};

Oh man I got that reader working and now I have no idea how to use it.

I have read

but now I want to pass this reader into a function.

Am I restricted to using anytype?

pub fn packFromECatReader(comptime T: type, reader: anytype) !T {
    var bytes: [@divExact(@bitSizeOf(T), 8)]u8 = undefined;
    try reader.readNoEof(&bytes);
    return packFromECat(T, bytes);
}

This is so brutal that I lose all the type information.

I think I may give up on implementing a reader due to this. It is possible to use std.io.AnyReader as described here: Complicated ownership using AnyReader · Issue #17458 · ziglang/zig · GitHub

But this may be too advanced for my zigbrain.

You can use AnyReader when writing the function, so the IDE gives you all the amenities, and when you’re done you switch to anytype.
There’s nothing complicated about AnyReader. The normal Reader stores the read function in the type itself. AnyReader stores it as a pointer in a field, so all AnyReaders have the same type. It’s a type erasure.