Memory Initialization and Writer Interface

Hello everyone!
I’ve just started to explore the language, and writing simple programs I’ve stumbled upon the writer interface copy issue. Now, I have this program in which even if I’m obtaining a pointer to the interface, the output is not the expected one.

const std = @import("std");

const StdOutHelper = struct {
    buf: [1024]u8 = undefined,
    stdout: std.fs.File = undefined,
    writer: std.fs.File.Writer = undefined,

    pub fn init() StdOutHelper {
        var this = StdOutHelper {};

        this.stdout = std.fs.File.stdout(); 
        this.writer = this.stdout.writer(&this.buf);

        return this; 
    }

    pub fn getWriter(this: *StdOutHelper) *std.Io.Writer {
        return &(this.writer.interface);
    }
};

pub fn main() !void {
    var stdout_helper = StdOutHelper.init();
    var writer = stdout_helper.getWriter(); 

    try writer.print("Hello world", .{});
    try writer.flush();
}

Out: o world instead of Hello world

Instead, I obtain the expected output if I create the writer in the getWriter method:

const std = @import("std");

const StdOutHelper = struct {
    buf: [1024]u8 = undefined,
    stdout: std.fs.File = undefined,
    writer: std.fs.File.Writer = undefined,

    pub fn init() StdOutHelper {
        var this = StdOutHelper {};

        this.stdout = std.fs.File.stdout(); 

        return this; 
    }

    pub fn getWriter(this: *StdOutHelper) *std.Io.Writer {
        this.writer = this.stdout.writer(&this.buf);
        return &(this.writer.interface);
    }
};

pub fn main() !void {
    var stdout_helper = StdOutHelper.init();
    var writer = stdout_helper.getWriter(); 

    try writer.print("Hello world", .{});
    try writer.flush();
}

Said so, I’m not understanding a lot:

  • I’m creating a writer on the stack with a std library function, and I’m copying the whole struct as a field of the StdOutHelper, that lives on the stack too when created in the main function.
  • I’m obtaining the interface to that copy in a second moment, and it’s not working as expected, so I must deduct that the whole writer struct is not copyable? Why if I copy it in the same function it starts to work?
  • Which is the guideline to follow to understand when something is copyable or not? How can I understand if something is meant to “survive” the stack and it’s a copiable struct holding pointers to heap allocated stuff, or everything is meant to be consumed before the stack frame expiration?

I understand there is an ongoing process of designing the language, as a Zig newbie I’m struggling to understand if there is already some well defined pattern/guideline/direction in this sense.

In your first version, the order of field initialization is wrong.
You are using this.stdout before initializing it.

4 Likes

they also were using a pointer to stack memory for the buf.

It’s fine to copy the writer implementation as long as you make sure any existing pointers to the interface are updated/aren’t used anymore. Stack memory only becomes invalid when the function that owns that memory returns.

if i understand correctly, pure chance, you’ve entered undefined/illegal behaviour, your lucky if it breaks when developing instead of in ‘production’ where it can do real damage.

Documentation should tell you, otherwise you can look at the source.

If you pass pointers to things that do outlive the current function then those pointers cant point to stack memory.

if you see @fieldParentPtr in the source, then the pointer passed to that must point to a field of the type its casting to (which is infered from the location its being assigned to eg const foo: *Foo = @fieldParentPtr(bar))


Lets improve what you had incrementally

first never use undefined as a default value, it should always be used explicitly, otherwise you could get all sorts of nasty behaviour when you forget about it.

Default values should not be used when its possible to create invalid state by only partially setting fields, which is the case here.

You probably don’t want to create a new writer every time you call getWriter, doing so does affect existing pointers to the interface since they will be using the implementation in the field, which is being overwritten each time.

to express that the writer may or may not exist is the purpose of optionals

const std = @import("std");

const StdOutHelper = struct {
    // no undefined, no default values.
    buf: [1024]u8,
    stdout: std.fs.File,
    writer: ?std.fs.File.Writer, // its optional now

    pub fn init() StdOutHelper {
        return .{
            // this is ok, since you cant partially call `init`.
            .buf = undefined,
            .stdout = .stdout(),
            .writer = null,
        }; 
    }

    pub fn getWriter(this: *StdOutHelper) *std.Io.Writer {
        if (this.writer) |*writer| { // |*name| makes it a pointer instead of a copy
            return &writer.interface
        } else {
            this.writer = this.stdout.writer(&this.buf);
            // there is `.?` syntax, but i dont remember how to make sure it doesnt copy.
            // which would be bad as the writer would then point to stack memory.
            // using `unreachable` is correct as we just set it to a non null value
            return if (this.writer) |*writer| &writer.interface else unreachable;
    }
};

pub fn main() !void {
    var stdout_helper = StdOutHelper.init();
    var writer = stdout_helper.getWriter();

    try writer.print("Hello world", .{});
    try writer.flush();
}

btw, there isnt any reason to store stdout because the writer takes a copy, not a pointer.

In addition to the above, if possible its good to make state implicit instead of a real value, such as our optional.

You do this by creating the writer in init so that there is no time the writer doesn’t exist (after init), like you originally tried. The problem is the buffer is one of our fields, and we need to take a pointer to it that lasts beyond init.

The solution is to have the caller provide it for us:

const std = @import("std");

const StdOutHelper = struct {
    buf: [1024]u8,
    // no stdout, its unecissary.
    writer: std.fs.File.Writer,

    pub fn init(this: *StdOutHelper) void {
        this.writer = std.fs.File.stdout().writer(&this.buf);
    }

    pub fn getWriter(this: *StdOutHelper) *std.Io.Writer {
        return &this.writer.interface;
    }
};

pub fn main() !void {
    var stdout_helper: StdOutHelper = undefined; // this is fine since `init` is called after
    stdout_helper.init();
    var writer = stdout_helper.getWriter();

    try writer.print("Hello world", .{});
    try writer.flush();
}

lastly, when you know ahead of time if a file is a stream you should use the [reader/writer]Streaming functions to set the mode of the file reader/writer to streaming. It does detect and changes modes when necessary but that results in a wasted syscall and the first real read/write doing nothing.

also want to make it clear, you shouldnt be using a helper for something so simple, it actually makes it harder to reason about since the buffer is ‘hidden’ in the type.

5 Likes

Sorry about that, what I posted earlier was a bad copy/paste from the original source. I’ve corrected it now to avoid any confusion about the unexpected behavior.

This was indeed an issue, thanks! I hadn’t considered that. I also really appreciate your breakdown of my code and the incremental improvements you suggested.

That said, there are still a few things that leave me feeling a bit confused or uneasy. I’m not trying to be overly critical—it’s just that I don’t have any preconceived notions about the language, so I’m trying to share honest feedback about what feels unclear or inconsistent to me without any “experience BIAS”.

The problem is that the documentation doesn’t seem to cover these details, and while digging into the source is always an option, it often feels inefficient and time-consuming. Maybe it’s just me, but I haven’t found a centralized resource or series of articles that outline best practices for Zig. I’m also unsure whether the community has reached any kind of consensus on them. You’ve pointed out a few, like “never use undefined for default values,” which is helpful—but it seems like the only way to learn these things is by making mistakes while coding and asking for help here :slightly_smiling_face:

For example, you showed me two different implementations of the helper: one using an init() function to construct a new object, and another that initializes struct fields directly. Which approach is considered best practice? Should I expect different styles depending on the library or even within the standard library itself?

If I don’t use a helper (which I created just to experiment with the language), what’s the recommended way to reuse a writer? Should I instantiate it at the entry point of my program along with the buffer, and then pass it to every function that needs to write to stdout? Should I manually flush it each time? Is it considered bad practice to create a helper function that writes and flushes when needed as a transparent operation for the caller?

I have all these questions swirling in my head right now, and I’m not sure if it’s simply because Zig is still evolving and the discussion is ongoing—or if there’s a deeper philosophy behind it. It feels like Zig intentionally embraces a kind of “anarchic” flexibility, where you’re encouraged to “squeeze the bit” and optimize at a granular level, making rigid conventions less desirable.

To draw a parallel: in C++, there’s the “rule of five” when it comes to designing object ownership. You’re not strictly required to follow it, but if you don’t, you’re generally seen as someone who’s breaking best practices. With Zig, I’m still trying to understand whether similar conventions exist—or if the language deliberately avoids them to preserve its low-level control and expressive freedom.

unfortunately pretty much, there are some in the lang ref, and other guides.
not useing undefined as default values is an extention of faulty default values in the lang ref.

for the most part you can get there with some introspection about what something does, it’s intended use, and potential issues when doing things differently.

the first approach is preferable. The second approach is when you need a field to reference another field.

your helper doesnt reuse a writer, it creates one from stdout with a hidden buffer. with and without the helper you would be creating it at the start of your program and passing it to functions.

you flush when you are done, when you need it to output any buffered data. you will rarely ever flush then use it again.

small helpers are generally discouraged as they usually make the code harder to reason about, the exception is when they make it easier to reason about.

its a bit of all that, plus your unfamiliarity, if you know c, and its best practices, this should be familiar though not entirely the same.

zig zen gives a breif list of points you should be aware of

 * Communicate intent precisely.
 * Edge cases matter.
 * Favor reading code over writing code.
 * Only one obvious way to do things.
 * Runtime crashes are better than bugs.
 * Compile errors are better than runtime crashes.
 * Incremental improvements.
 * Avoid local maximums.
 * Reduce the amount one must remember.
 * Focus on code rather than style.
 * Resource allocation may fail; resource deallocation must succeed.
 * Memory is a resource.
 * Together we serve the users.

You should pass the pointer to the interface if you intend to reuse the same writer everywhere. Something like this:

fn my_function(writer: *std.Io.Writer) !void {
    try writer.print("Hello!\n", .{});
    try writer.flush();
}
fn main() !void {
    var stdout_buffer: [1024]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const stdout = &stdout_writer.interface;
    try my_function(stdout);
}
1 Like

I think one case where you flush multiple times is in interactive programs, like if you write something that asks the user for input and then responds, for example in a read-eval-print-loop / repl.

There you flush every time when you are done printing, then you wait for the new user input and repeat. So that is one of the cases where you need to flush many times, so that the user sees the full response and you can keep using the same writer, that is later flushed again.

There also could be cases where readers / writers are used for communication, in such situations it also could make sense to call flush multiple times, but it depends on what you are reading/writing.

Inserting a call to flush after everything you write is generally a bad idea, because it defeats the buffering and then you are back to making way more syscalls than necessary.

First priority should be to make sure that flush gets called at the end so no data gets stuck in the buffer at the end of your program.

After that you can reason about your program behavior and figure out whether there are any places in your program where nothing will be written for a while (for example at the end of a frame in a game, or when the program is waiting for something to happen) and then it could make sense to add a call to flush before you start waiting (for the next frame or some input).

4 Likes

That makes sense. Initially, I thought manual flushing was necessary to prevent leaks from a full buffer, which led me to consider creating a reusable function to encapsulate the flushing logic. However, after reading another discussion, I realized the behavior is more akin to printf—with automatic flushing and a final flush to clear any remaining data.

Regardless, the idea behind the somewhat over-engineered helper “object” was simply to experiment with the language and deepen my understanding of lifetime and ownership concepts.

I started exploring Zig because I wanted to port a side project from C++ to C, aiming for a more lightweight and fun procedural approach to working with Vulkan APIs… without the overhead of OOP. Zig caught my attention as a modern alternative to C, so I decided to give it a try. What I didn’t expect was to encounter interfaces and polymorphism even for something as basic as printing to stdout.

Having worked as a C++ game developer (though it’s been over a decade), I’ve always kept the two paradigms separate: C++ for OOP and polymorphism, and C for procedural code—often wrapped in C++ when I needed to bridge the two worlds. Zig’s “middle ground” approach—where full polymorphism isn’t officially supported, yet subtly implemented for building the standard library—feels a bit contradictory, especially when viewed through the lens of Zig Zen points 1 and 4.

I believe my confusion stems from not having worked with large-scale “pure C” codebases, which makes it harder to appreciate the underlying design motivations.

1 Like

im not sure what you are talking about. zig doesn’t claim to not have polymorphism.

zig does have parametric polymorphism (generics), however it is done through procedural code as types are just values.

In any language with pointer casting you can make interfaces which is polymorphism.

I’m not on 15.1 yet (that’s my project for this evening), so I’m not going to try and sketch this out as code, but I wanted to point out that inline has some well-defined semantic consequences in Zig, and one of those is that a new stack frame is not created.

As the documentation puts it:

Adding the inline keyword to a function definition makes that function become semantically inlined at the callsite. This is not a hint to be possibly observed by optimization passes, but has implications on the types and values involved in the function call.

In a certain sense, they’re more like macros than they are like functions. In any case, it’s legal to create stack values inside an inline fn and “return” them, because all the action happens inside the parent stack frame, due to semantic inlining.

At minimum I would be confused and surprised if that didn’t work. I try to avoid strong statements about what code will do when I’ve neither written it nor run a test on it…

3 Likes

It makes semantic sense to consider the inline function to still have its own scope, currently data can live beyond scopes within the stack frame, but I recall that not being intended or slated to change (it was in an issue or pr, I can’t recall which one).

Which is to say, using inline fn is not a stable solution, assuming it considered a scope by the language which is a safe assumption.

I disagree, using inline fn with the behavior it currently has makes more sense to me, sure if you want to you can avoid it or use it based on your own rules, but I think inline has its uses here and there and I don’t think it makes sense to use it in a way based on some pr that may or may not be merged.

So unless that pr is basically about to land in master (or at least confirmed to be planned), this seems like unnecessary speculative future proofing, which I dislike and would rather wait for the actual change to happen. If it doesn’t happen or some other pr happens, your mental model of what is going on may already be trained to use the language in a way that doesn’t match what is actually going on.

I think it is easier to always try to get as close as possible to what is actually going on and then make decisions from there, instead of trying to come up with simplifying rules, that then give you problems once you hit some corner case.

2 Likes

There was a rather long discussion about scope-based vs function-frame based memory lifetimes, here on Ziggit. I don’t recall anything like a definitive issue on ziglang pertaining to that.

I favor frame-based, clearly. Especially since it looks like we’re getting async back in some form, this will be easier to reason about, and that discussion turned up several useful patterns which scope-based memory lifetimes would prevent, without, in my opinion, any compensating benefit.

Frame-based is also how the language works at the moment.

Broadly I agree with @Sze that we should use the language we have, at least until an issue has been marked ‘accepted’ and is being actively worked on.

If inline functions stop working for this purpose, the code can always be inlined manually, it’s not an especially challenging refactor to perform. But I do see that as a good argument for not forcing this to happen.

1 Like

I’m not arguing for one way or the other, I just recall someone on an issue or pr stating, I think in response to some code using memory past the end of a scope, that the intended behaviour for the language was scope based memory lifetimes instead of frame based.

If that is the intended behaviour, then it isn’t stable to use inline fn to solve this problem. They also could have been mistaken, or someone random who shouldnt be making statements about zig. But I don’t think I would remember it if it was someone random.

This from mlugg?

3 Likes

that is what i was thinking of, thanks for finding it.

They way I interpret the behavior of inline, this is not a reason that inline wouldn’t work:

And:

Adding the inline keyword to a function definition makes that function become semantically inlined at the callsite.

Basically I don’t see how this would change anything about inline working the way it is now. What was a local variable within the inline function will just become a local variable at the callsite. There is no reason stated anywhere that using inline would introduce a local scope that wasn’t written in the inline function.

I guess we are down to discussing what inline means and how it is implemented, if it works more like a macro replacement than I wouldn’t expect any scope introduction for the call itself.

inline fn is orthogonal to what closes and releases stack storage (frame or scope).

The problem is that when you inline a function, you very explicitly eschew time aliasing stack memory which means that you create neither a new frame nor a new block and place your allocations in the parent context.

I’m on record as preferring block scope for stack allocations, but frame scope doesn’t magically make the problem go away, either.

And, to be fair, I’m not that opposed to frame scope. The primary problem with frame scope is that Zig makes it problematic to create functions inside functions which would allow you to control when memory can get released. Whereas, creating a new scope via { } is super straightforward in Zig.

As an embedded programmer, bounding my stack memory usage is very, very, very important.

3 Likes

One more confirmation from mlugg today that return &local; in an inline fn is to be considered illegal: https://github.com/ziglang/zig/issues/25312#issuecomment-3324791040

1 Like