Typing `: [1024]u8 = undefined` Less?

I wonder whether inline functions can sort of take the place of macros? After all, fn buf isn’t really a frame on the callstack. Presumably post-inlining this could be legit and not invalid memory? I currently get a, “error: returning address of expired local variable,” if I try this. I’m unsure of whether this is a useful idea or if I should just have some more practice sessions of typing 1024 without typos :slight_smile:

inline fn buf() []u8 {
    var b: [1024]u8 = undefined;
    return &b;
}

Currently, you can, but you are not intended to be able to reference variables or stack memory outside the block it was defined in, inline functions semantically have their own scope still so are not an exception.

Inline functions are still semantically a function call, the biggest thing to know is all their parameters will still have to be evaluated before it can be called, regardless of what the function does, regardless of the fact that there won’t be a call in the generated machine code.

People tend to get bitten by those when they try to use inline functions to replace macros.

This makes sense as a rule.

Actually, I gave this some thought and aliasing the [1024]u8 type to e.g, Buf solves my issue here. Example: feat: alias for Buf · jdevries3133/libj@f07d223 · GitHub.

1 Like

since ba137783ed4152ebe8d7b98dca855c551fc924a7 (Sep 23, 2025):

$ cat test.zig
inline fn buf() []u8 {
    var b: [1024]u8 = undefined;
    return &b;
}
$ zig ast-check test.zig
test.zig:3:13: error: returning address of expired local variable 'b'
    return &b;
            ^
test.zig:2:9: note: declared runtime-known here
    var b: [1024]u8 = undefined;
        ^
12 Likes

The safety check is much appreciated & definitely helped me as I was getting started, so I think it’s a great add!

2 Likes

since ba137783ed4152ebe8d7b98dca855c551fc924a7 (Sep 23, 2025):

Woah, I totally missed this, kudos! Probably explains why there are fewer help-posts about returning stack memory on here :wink:

5 Likes

The relationship between arrays and slices takes awhile to click for new Zig enjoyers, so I’m going to explain in depth why this works, I suspect you know all this.

Why this works is that you’re returning an array: [1024]u8, and not a slice: []u8. Aliasing it to Buf is just a convenience.

A slice [] is a pointer with a length, so the result location is two words in size. An array [1024] is the memory a slice can point to, in this case 1024 bytes in size.

The original inline function was creating the array inside the function body and returning a pointer to it. This could be legal, because inline is semantic in Zig, it’s not a hint the compiler can overlook, it has effects on (in particular) comptime analysis. We had a long discussion about this awhile ago. It wouldn’t result in a dangling stack pointer because the stack is not permitted to be pushed and popped for an inline fn call.

But I think this is the correct decision. It means that the mental model of the function’s call stack is everything visible in the function definition. An inline function is not so-marked at the call site, and we want reasoning about code to be as local as possible.

This is a good illustration of why that’s fine the way it is, because your inline function can just return an array, [1024]u8, and it should do that, since that’s the type you want.

Say 1024 isn’t always the number, no problem!

pub inline fn buf(len: comptime_int) [len]u8 {
    var b: [len]u8 = undefined;
    return b;
}

Now it’s buf(1024), buf(4096), whatever.

Last note: inline is not needed for correctness of this function. I would use it, personally, because I don’t want to rely on result location analysis to ensure one buf rather than two are created. But in Zig, the length of an array is part of the type itself, the result location has room for it, you can always return an array.

Zig doesn’t (and shouldn’t[1]) allow runtime-variable amounts of stack space to be created, and how inline functions deal with dangling memory wouldn’t change that. Therefore there’s never any need: just return the memory, not a pointer to it. If and when variable amounts of memory are needed, we use the heap for that.


  1. alloca is a security nightmare, is why. ↩︎

1 Like

That doesn’t really help things, because unless you assign that array to a local variable, you cannot coerce a pointer to it into a mutable slice to be passed to writer/reader. (Note that it also doesn’t compile unless you changed var b to const b).

You’ve just made it so that instead of writing

var buffer: [1024]u8 = undefined;

It becomes:

var buffer = buf(1024);

Which isn’t much of an improvement in my opinion.

Correct.

There is a Dude quote which comes to mind…

1 Like

I think this is a helpful way to think of slices:

[]u8 == struct { len: usize, ptr: [*]u8 }

Which can be demonstrated with:

@compileLog(@sizeOf([]u8)); // prints @as(comptime_int, 16) at compile time
@compileLog(@sizeOf(struct { len: usize, ptr: [*]u8 })); // prints @as(comptime_int, 16) at compile time
@compileLog(@sizeOf([1024]u8)); // prints @as(comptime_int, 1024) at compile time
2 Likes

The goal of “typing xxx less” is to some degree going against a fundamental language design choice:

“Favor reading code over writing code.”

Trying to save a few characters just to avoid having to write a type definition, or the undefined keyword is only going to hurt you in the end if you or someone else wants to read it again in the future.

Now the only real concern that I see here is “typing 1024 without typos”, if I understand correctly you want to enforce a fixed buffer size of 1024 bytes in your project. In that case I think the correct solution is to write more, not less, by making this an actual constant, this would also make it easy to change this value if you decide to change the size:

const stackBufferSize = 1024; // or a more specific name e.g. mentioning the protocol it comes from

...
    var buf: [stackBufferSize]u8 = undefined;

Beyond that I think it’s also worth questioning, why you have so many stack buffers to begin with. This may be a sign that you are not using allocators enough. Note that in Zig the right kind of allocator can be just as fast as a stack buffer.

1 Like