#Returning an Io.Reader interface from a function

1 messages · Page 1 of 1 (latest)

polar shadow
#

Hi! New Zig user here.

I want to wrap a File.Reader in a struct together with some other functionality, and want to make use of it through the Io.Reader interface. For MVE's sake the following code suffices (it reads its own source file):

const std = @import("std");
const expect = std.testing.expect;

const SelfReader = struct {
    reader: std.fs.File.Reader = undefined,
    buffer: [1024]u8 = undefined,

    pub fn init() !SelfReader {
        const path = @src().file;
        const file = try std.fs.cwd().openFile(path, .{ .mode = .read_only });
        var sr = SelfReader{};
        sr.reader = file.reader(&sr.buffer);
        return sr;
    }

    pub fn getInterface(self: SelfReader) *std.Io.Reader {
        return @constCast(&self.reader.interface);
    }

    pub fn close(self: SelfReader) void {
        self.reader.file.close();
    }
};

fn takeByte(iface: *std.Io.Reader) !u8 {
    return iface.takeByte();
}

test "get interface" {
    var sr = try SelfReader.init();
    defer sr.close();

    const reader = &sr.reader.interface;
    try expect(try reader.takeByte() == 'c');
    try expect(try takeByte(reader) == 'o');
}

So far so good. The above test works as expected and I can pass the *Io.Reader to another function (which I want to do in my full code).

However, I would like the 'get interface' step to also be part of the struct, hence the getInterface function. Even though I would expect this to work the same, using the *Io.Reader returned by getInterface results in an error.

test "get interface through function" {
    var sr = try SelfReader.init();
    defer sr.close();

    const reader = sr.getInterface();
    try expect(reader.takeByte() == std.Io.Reader.Error.ReadFailed);
    // Subsequent reads give random data, I assume undefined contents of the buffer
}

I don't understand why this fails, also after reading through some documentation and blog posts. The closest I have come to an answer is that somehow part of the underlying File.Reader goes out of scope, but I don't see where or how that should happen here as I am (I assume) correctly passing around a pointer to the Io.Reader. When using this pointer as a function argument like in the first test it works, so why should it fail here?

After messing around with the code a bit, I found the following 'workaround' which might point to an actual answer but to me makes it only more confusing. By first accessing the Io.Reader directly, the pointer returned by getInterface suddenly does work!

test "get interface through function workaround" {
    var sr = try SelfReader.init();
    defer sr.close();

    try sr.reader.interface.fill(1);

    const reader = sr.getInterface();
    try expect(try reader.takeByte() == 'c');
    try expect(try takeByte(reader) == 'o');
}

My best guess at this point is that somehow using the pointer from getInterface doesn't trigger the fill method, but once the buffer is filled it is possible to read its contents. Having only started with Zig last week this is where I'm starting to get lost though. What is going on here?

I've done most testing with Zig 0.15.2 on Fedora Linux. I've also tried the current 0.16.0-dev.1441+2ea55d715 with a DebugAllocator and Io.Threaded and the result is pretty much the same, except that instead of a ReadFailed error I get a panic: programmer bug caused syscall error: INVAL. Attached is the full code for 0.15.2 and 0.16.0-dev.

stable kernel
#

just skimmed code, but it looks like the issue is that getInterface() should accept a reference to SelfReader. the way you've written returns an invalid ptr to local.

#

note that @constCast shouldn't usually be necessary in zig and is mostly used with interfacing with c code due to c's different mutability rules like fn params being mutable.

polar shadow
#

gosh I've been diving into this rabbit hole for a whole day and the solution is that simple, making it a reference does indeed work
I only added the @constCast because the compiler was complaining about const pointers but that's fixed as well now

I'm guessing then the 'workaround' isn't actually supposed to work and only does because the invalid pointer accidentally points to the correct location?

stable kernel
#

i suspect thats true about the workaround. you were likely just getting lucky somewhere and the stack happened to have not been invalidated at that point.

polar shadow
#

alrighty, thanks for the help!

stable kernel
#

i should say overwritten or reused instead of invalidated.

polar shadow
#

yea exactly, I get what you mean