Problem initialising field of type std.io.AnyWriter

allocator: std.mem.Allocator,
stream: std.net.Stream = undefined,
data: std.ArrayListUnmanaged(u8) = .empty,
writer: std.io.AnyWriter = undefined,

pub fn init(allocator: std.mem.Allocator, stream: std.net.Stream, select_data_writer: bool) !@This() {
    // 1 - This works
    return .{
        .allocator = allocator,
        .stream = stream,
        .writer = stream.writer().any(),
    };

    // 2 - This does not work:
    // writer.print(...) -> error.NotOpenForWriting
    var to_return: @This() = .{
        .allocator = allocator,
        .stream = stream,
    };

    to_return.writer = stream.writer().any();
    // or
    to_return.writer = to_return.stream.writer().any();
    return to_return;

    // 3 - This works even less:
    // writer.print(...) -> seg fault
    to_return.writer = if (select_data_writer) to_return.data.writer(allocator).any() else stream.writer().any();
    return to_return;
}

I found a couple of hacks to make it work by calling
to_return.writer = if (select_data_writer) to_return.data.writer(allocator).any() else stream.writer().any();
later on in the code.
But can it be done in the init function?

AnyWriter stores a pointer. Method 1 is not correct because you’re returning a pointer to the stack-local stream which becomes invalid after the function returns. Method 2 and 3 are incorrect for the same reason.

If your writer is always going to be stream.writer().any() and not anything else, just don’t store it. You could still make a convenience function to get it.

If your writer could be something else, just have it be passed in.

3 Likes

Thanks for the explanation. I now understand why method 1 worked but shouldn’t have and why it worked when defining writer later on in the code.

I have several function writing to that self.writer, which should sometimes point to the stream writer when no more processing is required, while sometimes to the ArrayListUnmanaged writer or any other type of writer for further processing before sending it to the stream.

Is there a way to save this pointer directly in the init() function or should I just give up trying to save it in a field? My current workaround is to save the select_data_writer and use const writer = if (self.select_data_writer) self.data.writer(self.allocator).any() else self.stream.writer().any(); at the beginning of those function instead of writing to self.writer.

If the possible writers it may point to are all known, you could use an enum to store which writer is currently being used, and then a convenience function could select the appropriate writer and return an AnyWriter.

fn writer(self: *const @This()) AnyWriter {
    switch (self.current_writing_mode) {
        .arraylist => return self.data.writer().any(),
        .stream => return self.stream.writer().any(),
        // ...
    }
}

Having a struct point within itself is tricky and it’s best to avoid if you can.

Thanks a lot, I’ll try to go that road and remember your advice for the future :+1: