Who owns the bytes when returning structs?

You can return a struct from a function.

Documentation - The Zig Programming Language

I like this choice, and think it’s something some C compiler do (maybe inconsistently if my memory serves and compilers haven’t updated since I was last actively using C)

What are the exact rules here on returning structs?

I’ve segfaulted a few times in ways I didn’t expect, but I think it’s PEBKAC and just not fully understanding what the rules are. I wonder what I’ve got wrong with these assumptions:

  1. Returning a struct extends the lifetime of that struct to its calling scope
  2. Only the lifetime of a struct itself is extended, any “nested” structs will not be extended and will die. (That is, you need an allocator and pointers)
  3. This calling-scope-owns-stack-allocation semantic doesn’t extend to union or union(enum) (or does it?)
  4. It only works for returning structs and not returning pointers-to-structs or slices of structs

This phrase is about returning a type (which is compile time action), not about returning an instance of a struct during runtime.

1 Like

I had the same issues with nested structures in arraylist .
the fields defined as const u8 , when assigning with bufprint lost their data, I had to do allocprint keeping the same philosophy.
it works well but I remain skeptical maybe I got lost.

1 Like

Well it certainly seems to return “an instance of a struct” which I assume is a continuous block of memory allocated on the stack sufficient to hold the struct members, and the members are type safe accessors to regions of memory in that block. As far as I can tell this works at runtime at least for the basic case. Normally if I create a struct in a scope (like a function) then it won’t be available to parent scopes, and if I return a pointer to a struct from a scope, the block of memory is no longer protected and very quickly becomes a use-after-free garbled mess.

But the liberal use of structs in return position both in the docs and in source for Zig itself implies this is totally valid. It’s something like “tail position ownership transfer”

The exact problem I was trying to work through was returning (and defining) linked list data structures, but now I’m more interested in fleshing out my understanding of the return semantics for memory. If I come to understand it better then I’d be happy to write up something or contribute to the docs

I believe the semantics here are as if you were assigning the struct to a variable: all members are copied one by one, no guarantees about padding, etc. Similar to as if you had done a memcpy between the two structs.

2 Likes

Are you talking about this example from the doc
(the sentence occurs there only once)?

// You can return a struct from a function. This is how we do generics
// in Zig:
fn LinkedList(comptime T: type) type {
    return struct {
        pub const Node = struct {
            prev: ?*Node,
            next: ?*Node,
            data: T,
        };

        first: ?*Node,
        last:  ?*Node,
        len:   usize,
    };
}

It is completely comptime stuff, note comptime in args and type as a type of return value.
Types are values in Zig, but only during comptime. You can think of it as a sort of monomorphisation to construct generic data structures.

If you want to return an instance of a struct, just return it. :slight_smile:

const std = @import("std");
const print = std.debug.print;

const Thing = struct {
    a: u32 = undefined,
    b: u32 = undefined,
};

fn returnSomeThing() Thing {
    const t: Thing = .{.a = 3, .b = 7};
    return t;
}

pub fn main() void {
    const t = returnSomeThing();
    print("a = {}, b = {}\n", .{t.a, t.b});
}

will print

~/coding/zig-lang $ ./rs 
a = 3, b = 7
3 Likes

Maybe this sample code makes it clearer?

onst std = @import("std");

// creates and returns a type
fn FooA() type {
    return struct { a: u8, b: u8 };
}

// create a type
const FooB = struct {
    a: u8,
    b: u8,
};

// creates an instance of a type
fn makeFooB() FooB {
    return FooB{ .a = 4, .b = 2 };
}

// creates and returns a type
fn FooC() type {
    const T = struct {
        a: u8,
        b: u8,
    };

    return T;
}

pub fn main() !void {
    // foo_a, foo_b, foo_c are all instances
    const foo_a: FooA() = .{ .a = 4, .b = 2 };
    const foo_b: FooB = .{ .a = 4, .b = 2 };
    const foo_c: FooC() = .{ .a = 4, .b = 2 };

    std.debug.print("fn FooA:\t{s}\n", .{@typeName(@TypeOf(foo_a))});
    std.debug.print("struct FooB:\t{s}\n", .{@typeName(@TypeOf(foo_b))});
    std.debug.print("fn FooC;\t{s}\n", .{@typeName(@TypeOf(foo_c))});
}

// Output
// fn FooA:	main.FooA()
// struct FooB:	main.FooB
// fn FooC;	main.FooC.T
3 Likes

Ok I’m guilty of conflating things. The docs were not actually talking about returning a struct, but about returning a struct type.

@dude_the_builder I had no idea I could have type functions as type annotations… That’s absolutely wild.

I think “return a struct, it works the way you’d expect” is about the answer I expected for simple structs. I think I need a more specific example of what I’m doing and don’t understand before I should continue. If I never update this thread again, then probably the issue was me just doing something silly with pointers.

5 Likes

If you think that’s wild, wait 'til you see more of the source code in the standard library. switch expressions that return a type as the return type of a function is one little gem that comes to mind. :smile:

3 Likes

I’ve been digging around more…

I’m speechless, this language is a work of art!

Also there is sane compile-checked duck typing through anytype… wow.

4 Likes

Keep an eye out for a footgun when an inline function returns a struct. Because it’s inline, there are no copy semantics in place.

// this works as expected; returned T is allocated on the stack
fn copyStruct(comptime T: type, buffer: []u8) T {
    return @as(*T, @ptrFromInt(@intFromPtr(buffer.ptr))).*;
}

// this does not work; returned T relies on bytes in buffer
inline fn copyStruct(comptime T: type, buffer: []u8) T {
    return @as(*T, @ptrFromInt(@intFromPtr(buffer.ptr))).*;
}

This is obvious once you understand what’s going on, but took me some time to debug.

1 Like