Creating a seekable reader from a slice of bytes

Hi,

I would like to use a seekable reader on a slice of bytes ([]const u8). The bytes are gotten from a message in a websocket. I would like to use methods such as seekTo and use properties like pos. I am able to use the file reader interface for data in a file. But I can’t find a similar reader for a []const 8. I am using Zig 0.15.2.

How do I go about this?

1 Like

There is no seekable interface, it’s up to implementations to provide that extended functionality, which unfortunately makes you dependent on a discrete set of implementations.

The only reader for a slice is Io.Reader.fixed which does not provide any extended functionality, and it does not even have extra state, instead reusing the readers existing buffer functionality. You could take advantage of that and just modify reader.seek field directly. However, since it does not have extra state it returns Io.Reader directly which unfortunately means there is no way to distinguish a fixed reader and any other reader at a type level.

You could compare the address of the vtable to the expected address of the fixed implementation, but that is kinda hacky.

One way to do this agnostic of the implementation would be to pass another vtable alongside the reader that has contains seeking functions.

3 Likes

I’ve been experimenting with this pattern some time ago:

fn parse(state: *State, r: *std.Io.Reader) union(enum) { done, seek: i64 } {
    // If this function needs to seek somewhere, it can communicate that to the caller:
    if (need_seek) return .{ .seek = -10 };
    // Other return codes can be introduced as needed
    return .done;
}

pub fn main() !void {
    var state: State = .init;
    while (true) {
        switch (state.parse()) {
            // Any kind of reader that's capable of seeking can be used here,
            // e.g. a `std.Io.File.Reader` or a `std.Io.Reader.fixed()`
            .seek => |amount| try my_reader.seekBy(amount),
            .done => break,
        }
    }
}

If the next seek position is saved in your state, you could even use an error set with error.WantSeek or something like that for control flow which might compose better with std.Io.Reader.Error.

This pattern also has the advantage that the parse function doesn’t have to deal with the different kinds of errors different seek implementations could produce.

2 Likes

For a “fixed” buffer Reader, all you need to do is set the seek field of the Reader.

If you need to use Files and “fixed” readers inter-changeably, then the best I’ve managed to come up with for my own code is a tagged union like this:

/// Allows Files and memory images to be used interchangeably for reading
pub const SeekableReader = union(enum) {
    on_disk: *std.Io.File.Reader,
    in_memory: *std.Io.Reader,

    pub fn seekTo(self: SeekableReader, offset: u64) File.Reader.SeekError!void {
        switch (self) {
            .on_disk => |file| try file.seekTo(offset),
            .in_memory => |mem| {
                std.debug.assert(offset <= mem.buffer.len);
                mem.seek = offset;
            },
        }
    }

    pub fn seekPos(self: SeekableReader) usize {
        return switch (self) {
            .on_disk => |file| file.logicalPos(),
            .in_memory => |mem| mem.seek,
        };
    }

    pub fn interface(self: SeekableReader) *std.Io.Reader {
        return switch (self) {
            .on_disk => |file| &file.interface,
            .in_memory => |mem| mem,
        };
    }
};
2 Likes