Load bearing debug print

I’m running into issues with @fieldParentPtr again and I would like to understand what is happening. Basically I’m trying to make a small wrapper for terminal methods, so I have a struct with some std.Io like so:

stdin: std.fs.File,
stdout: std.fs.File,

input_buffer: [4096]u8 = undefined,
fs_reader: std.fs.File.Reader = undefined,
reader: *std.Io.Reader = undefined,
output_buffer: [4096]u8 = undefined,
fs_writer: std.fs.File.Writer = undefined,
writer: *std.Io.Writer = undefined,

pub fn init() !@This() {
    var self: @This = .{
        // ...
        .stdin = std.fs.File.stdin(),
        .stdout = std.fs.File.stdout()
    };
        
    // ...

    self.fs_reader = self.stdin.reader(&self.input_buffer);
    self.reader = &self.fs_reader.interface;
    self.fs_writer = self.stdout.writer(&self.output_buffer);
    self.writer = &self.fs_writer.interface;

    return self;
}

Now, when I try to use self.writer, either inside of declarations on the wrapper or elsewhere, sometimes it works but often I will get either a segfault or a “General protection exception (no address available)”, which immediately makes me think that somewhere along the line the pointer to the interface is getting dropped or copied somehow which is breaking the parent lookup. So, I go through each level in the call stack and add

std.debug.print("{*}\n", .{ wrapper.writer });

and the pointer looks good all the way up the stack, and then I get to that init method above, and I put

std.debug.print("{*}\n", .{ self.writer });

before returning, and suddenly all the problems dissapear. I remove all other debug statements, and everything still just works, but then if I comment out that one debug in the init function, things break again. What is going on here? I’m very confused.

I believe your issue is returning pointers to stack allocated memory. You first create a stack allocated struct with var self = ... and then pass pointers to arrays in that stack allocated struct to the writer and reader constructors of your files.
When you return from the function, this stack allocated variable goes out of scope, and the buffers with it. Your other data is copied to the resulting variable, but it copies the pointers to the stack allocated buffers that are now out of scope. Eventually that stack space is used for some other storage and you memory issues that you are seeing.

For this situation, I usually require the space to already exist and be passed into the init function:

pub fn init(self: *@This()) void {
    self.stdin = std.fs.File.stdin();
    self.stdout = std.fs.File.stdout();
    self.fs_reader = self.stdin.reader(&self.input_buffer);
    self.reader = &self.fs_reader.interface;
    self.fs_writer = self.stdout.writer(&self.output_buffer);
    self.writer = &self.fs_writer.interface;
}

And then it will be used like the following:

    const term: TermIO = undefined;
    term.init();
    defer term.deinit();

This means that the buffer will live for the same lifetime as the struct.

4 Likes

Okay, yes that does appear to have been the issue! I refactored init to take the buffers as arguments instead of them being struct fields, and things look to be working now. I think it did not occur to me that an array would be a pointer, I assumed it was part of the struct itself. Thanks!

So the array is part of the struct itself, but the Reader/Writer contain pointers to the buffer slices. That’s the catch here is the internal usage of pointers for the reader and writers.

Those arrays are indeed directly part of the struct self (not pointers), and the data they contain does outlive the stack frame of init, in the sense that it gets copied (like any other struct field) to the memory location of the returned struct. But its memory location changes. The pointers you passed to the reader/writer constructors are just fixed windows into memory; they can’t see when they block of data they originally encompassed gets copied somewhere else.

1 Like

reader and writer are the only fields you need, all the others are redundant. This also solves your problem, but you figured that out already.

If you need the File.Reader/Writer for what ever reason, you then don’t need to store pointers to the interfaces, as you can get them on demand.

1 Like

Interesting… In the source it seems that std.fs.File.reader/writer is returning a struct containing the interface, so I figured I had to keep track of that struct for the lifetime of the interface or else @fieldParentPtr would fail. If I change the init to do something like this

    self.reader = @constCast(&(self.stdin.reader(input_buffer).interface));
    self.writer = @constCast(&(self.stdout.writer(output_buffer).interface));

then it does still work, but I would think the struct returned by the reader/writer function would go out of scope here? Why does this still work?

Your assumption is correct, it continues to work through (bad)luck, its undefined behaviour.

I should have been more explicit, I meant to obtain the reader/writer from the caller.

It’s generally good to dump memory/resource management on the caller (the entire point of Allocator and Reader/Writer interfaces).
It allows the caller to be more flexible and simplifies your code, additionally would allow your code to be tested by using different reader/writer (assuming you don’t need File.Reader/Writer’s, which requires upcoming std.Io interface to test, unless you don’t mind using your real file system)

You do have to keep a struct alive for as long as you want to keep accessing it via a pointer to its internals, but returning the struct from the same function where you constructed the pointer doesn’t have that effect.