Segfaults on implementing an interface

Hi everyone! I’m trying to implement a simple interface (for learning purposes) named Shape1 which will hopefully allow me to iterate over different shapes generically and execute shapes’ area() methods accordingly:

const Circle = struct {
    radius: f32,
    fn area(self: *anyopaque) f32 {
        const s: *const Circle = @ptrCast(@alignCast(self));
        return std.math.pi * s.radius * s.radius;
    }
};

const Square = struct {
    side: f32,
    fn area(self: *anyopaque) f32 {
        const s: *const Square = @ptrCast(@alignCast(self));
        return s.side * s.side;
    }
};

const Shape1 = struct {
    ptr: *anyopaque,
    vtab: *const VTab,

    const VTab = struct {
        areaFn: *const fn (ptr: *anyopaque) f32,
    };

    fn area(self: *Shape1) f32 {
        return self.vtab.areaFn(self.ptr);
    }

    fn init(ptr: *anyopaque, f: *const fn (ptr: *anyopaque) f32) Shape1 {
        return .{
            .ptr = ptr,
            .vtab = &VTab{ .areaFn = f },
        };
    }
};

pub fn main() !void {
    var sq = Square{ .side = 2 };
    var cr = Circle{ .radius = 2 };
    var shapes = [_]Shape1{
        Shape1.init(&sq, Square.area),
        Shape1.init(&cr, Circle.area),
    };
    for (&shapes) |*shape| {
        std.log.debug("{d}", .{shape.area()});
    }
}

First, I was trying to use ptr: *const anyopaque and it succeeded. Then, I switched to non-const pointers and now code segfaults. Stepping in the debugger showed it happens as soon as I access shape in the loop. Tbh, I’m not sure why. All the things reside on the stack and the accessing them shouldn’t be a problem.

I tried to get rid of &Vtable {...} in init by simply inlining areaFn into Shape1 itself. And it worked! Segfault disappeared. However, in the repo explaining different approaches to implementing interface (here), the author uses &VTab{} in the same init() successfully, without using any allocator.

What do I do wrong?

You’re sharing one vtable between all the individual types. This won’t work because that single vtable is getting its functions switched out.

You need to have a method that returns a shape on each type so they can each have their own vtable.

In init VTab{ .areaFn = f } is allocated in stack, its lifetime ends when exiting init but you get a pointer to that memory that its contents are replaced.

One way to solve this is to move the construction of vtable in each shape where f is constant and the VTab allocation can be static.

A concrete example maybe? I’m not sure I’m getting you both :slight_smile:

In init VTab{ .areaFn = f } is allocated in stack, its lifetime ends when exiting init but you get a pointer to that memory that its contents are replaced.

Then, the original code here is wrong?

Sure, I’ll let @dimdin speak to the stack lifetime issue, I’ll draft up a godbolt example here to show you what I mean… uno momento…

1 Like
const std = @import("std");

const VTab = struct {
    areaFn: *const fn (ptr: *anyopaque) f32,
};

const Circle = struct {
    radius: f32,
    fn area(self: *anyopaque) f32 {
        const s: *const Circle = @ptrCast(@alignCast(self));
        return std.math.pi * s.radius * s.radius;
    }
    pub fn vtable() *const VTab {
        return &VTab{ .areaFn = Circle.area };
    }
};

const Square = struct {
    side: f32,
    fn area(self: *anyopaque) f32 {
        const s: *const Square = @ptrCast(@alignCast(self));
        return s.side * s.side;
    }
    pub fn vtable() *const VTab {
        return &VTab{ .areaFn = Square.area };
    }
};

const Shape1 = struct {
    ptr: *anyopaque,
    vtab: *const VTab,

    fn area(self: *Shape1) f32 {
        return self.vtab.areaFn(self.ptr);
    }

    fn init(ptr: *anyopaque, vtab: *const VTab) Shape1 {
        return .{
            .ptr = ptr,
            .vtab = vtab,
        };
    }
};

pub fn main() !void {
    var sq = Square{ .side = 2 };
    var cr = Circle{ .radius = 2 };
    var shapes = [_]Shape1{
        Shape1.init(&sq, Square.vtable()),
        Shape1.init(&cr, Circle.vtable()),
    };
    for (&shapes) |*shape| {
        std.log.debug("{d}", .{shape.area()});
    }
}

EDIT:
Why this works?

pub fn vtable() *const VTab {
        return &VTab{ .areaFn = Square.area };
}

Since the members of VTab are all constant, allocation is static. We get a VTab pointer to the static space that does not change.

Why this does not work?

fn init(ptr: *anyopaque, f: *const fn (ptr: *anyopaque) f32) Shape1 {
    return .{
        .ptr = ptr,
        .vtab = &VTab{ .areaFn = f },
    };
}

Here VTab is not constant, f comes in runtime. Since VTab is allocated locally, it is allocated in stack.
When init finishes the stack space is reused by other functions and its value becomes corrupted.

Discussion: Feel confused with lifetime of anonymous struct

I don’t know… It looks ugly tbh, like an ad-hoc or something and I’m still not sure why this works and the other one didn’t. Why returning return &VTab{ .areaFn = Square.area } in the shape structs itself magically makes VTab static? Also, I would appreaciate to hear some comments on the original code I’m learning from (GitHub - yglcode/zig_interfaces: Interfaces in ziglang) because it seems to work fine:

const std = @import("std");

const Point = struct {
    x: i32 = 0,
    y: i32 = 0,
    pub fn move(self: *Point, dx: i32, dy: i32) void {
        self.x += dx;
        self.y += dy;
    }
    pub fn draw(self: *Point) void {
        std.debug.print("point@<{d},{d}>\n", .{ self.x, self.y });
    }
};

const Shape2 = struct {
    ptr: *anyopaque,
    vtab: *const VTab,
    const VTab = struct {
        draw: *const fn (ptr: *anyopaque) void,
        move: *const fn (ptr: *anyopaque, dx: i32, dy: i32) void,
    };

    pub fn draw(self: Shape2) void {
        self.vtab.draw(self.ptr);
    }
    pub fn move(self: Shape2, dx: i32, dy: i32) void {
        self.vtab.move(self.ptr, dx, dy);
    }

    pub fn init(obj: anytype) Shape2 {
        const Ptr = @TypeOf(obj);
        const impl = struct {
            fn draw(ptr: *anyopaque) void {
                const self: Ptr = @ptrCast(@alignCast(ptr));
                self.draw();
            }
            fn move(ptr: *anyopaque, dx: i32, dy: i32) void {
                const self: Ptr = @ptrCast(@alignCast(ptr));
                self.move(dx, dy);
            }
        };
        return .{
            .ptr = obj,
            .vtab = &.{
                .draw = impl.draw,
                .move = impl.move,
            },
        };
    }
};

pub fn main() !void {
    var p1 = Point{ .x = 1, .y = 2 };
    var p2 = Point{ .x = 3, .y = 4 };
    var shapes = [_]Shape2{
        Shape2.init(&p1),
        Shape2.init(&p2),
    };
    for (&shapes) |*shape| {
        shape.draw();
    }
}

UPDATE:

Aha! I see why it works in the original code above. impl struct is constant and all its members are comptime known so it seems it automatically become static, right?

@dimdin Thanks for the EDIT. It indeed explains the lifetime of VTab thing.

1 Like

My take is a little different than @dimdin’s, but I think you’ll see the similarity because we’re saying something similar. Here’s an example that I drafted - basically, same idea, just using different names. Each type has a function that returns an Interface with a vtable.

const VTable = struct {
    vcall: *const fn(*const anyopaque) i32,
};

const Interface = struct {
    ptr: *const anyopaque,
    vtable: *const VTable,
    pub fn call(self: Interface) i32 {
        return self.vtable.vcall(self.ptr);
    }
};

const Foo = struct {
    x: i32,

    pub fn call(ctx: *const anyopaque) i32 {
        const self: *const Foo = @ptrCast(@alignCast(ctx));
        return self.x;
    }

    pub fn interface(self: *const Foo) Interface {
        return Interface {
            .ptr = self,
            .vtable = &.{ .vcall = call },
        };
    }
};

const Bar = struct {
    x: i32,
    y: i32,
    pub fn call(ctx: *const anyopaque) i32 {
        const self: *const Bar = @ptrCast(@alignCast(ctx));
        return self.x + self.y;
    }

    pub fn interface(self: *const Bar) Interface {
        return Interface {
            .ptr = self,
            .vtable = &.{ .vcall = call },
        };
    }
};

fn doCall(a: Interface, b: Interface) i32 {
    return a.call() * b.call();
}

export fn something() i32 {
    const foo = Foo{ .x = 42 };
    const bar = Bar{ .x = 43, .y = 7 };

    const a = foo.interface();
    const b = bar.interface();

    const out = @call(.never_inline, doCall, .{ a, b });
    return out;    
}

I’ve done @call(.never_inline because I don’t want the compiler to see through this trivial example.

Anyway, if we look at the assembly, we can see this…

example.Foo.interface__anon_1002:
        .quad   example.Foo.call

example.Bar.interface__anon_1004:
        .quad   example.Bar.call

These anonymous values are holding our function pointers - you can see we got two of them, one for each interface function. These will get referenced when I make an interface:

example.Foo.interface:
        push    rbp
        mov     rbp, rsp
        push    rax
        mov     rax, rdi
        mov     qword ptr [rbp - 8], rsi
        mov     qword ptr [rdi], rsi
        movabs  rcx, offset example.Foo.interface__anon_1002
        mov     qword ptr [rdi + 8], rcx
        add     rsp, 8
        pop     rbp
        ret

So you can see that we’re loading in a different location somewhere else in the code - that’s where the vtables live. If you add more functions to it, you’ll see them stack up there.

Now why do we have two? We have two because each interface function has its own vtable unique to it because they are two separate functions. So we’re getting two separate vtables because we have made two different literal vtables (in the same sense as a string literal) in two separate function implementations.

In other words, you get a vtable for Foo’s and a vtable for Bar’s.

3 Likes

I had to take a break. I was upset not being able to understand it all. Such a waste! Anyway, thank you for the effort and the code above. At least your approach with interface() methods in each of the struct looks familiar with what I saw in the std lib.

The rest, including assembly, is simply beyond my brain capacity :smiling_face_with_tear: . However, I’ll try my best and explain what I supposedly understood:

These:

example.Foo.interface__anon_1002:
        .quad   example.Foo.call

example.Bar.interface__anon_1004:
        .quad   example.Bar.call

Possibly refer to these lines:

const Foo|Bar = struct {
    pub fn interface(self: *const Foo|Bar) Interface {
        return Interface {
           ...
           .vtable = &.{ .vcall = call }, 
                     ^ (Foo|Bar).interface__anon_* static structures

Which should show that…ehm…zig compiler prepared a couple of VTable instances as static data in the scope of each Foo/Bar struct.

Then, the implementation of example.Foo|Bar.interface in assembly, as I said, I simply cannot understand. I think I need a separate book for it (is there any not 800-page-long-you-will-never-read?) I assume that this asm block (with pro and epilogs trimmed) fills/initialize the Interface and store it into the addresses the rbp and rdi are pointing to. But I think you just wanted to point out that during this initialization, we can see zig uses that static data (example.(Foo|Bar).calls) mentioned above.

If my assumptions were true, then the explanation on why do we get two separate vtable instances that are static:

Now why do we have two? We have two because each interface function has its own vtable unique to it because they are two separate functions. So we’re getting two separate vtables because we have made two different literal vtables (in the same sense as a string literal) in two separate function implementations.

Starts to makes sense and taking & address of “literal values”/structs in the implementation is safe as soon as zig knows the … [and here is the key to predict when zig indeed makes things static so that we can rely on their addresses]

Since no one wanted :slight_smile: to comment why the original implementation worked relative to mine, I would ask directly:

pub fn init(obj: anytype) Shape2 {
    const Ptr = @TypeOf(obj);
    const impl = struct {
        fn draw(ptr: *anyopaque) void {
            const self: Ptr = @ptrCast(@alignCast(ptr));
            self.draw();
        }
        fn move(ptr: *anyopaque, dx: i32, dy: i32) void {
            const self: Ptr = @ptrCast(@alignCast(ptr));
            self.move(dx, dy);
        }
    };
    return .{
        .ptr = obj,
        .vtab = &.{ // THIS
            .draw = impl.draw,
            .move = impl.move,
        },
    };
}

Can I assume that the reason why the code above works is because in the place of THIS zig creates static vtable instances for every obj passed in into init()? (again, because anytype means zig would spawn a separate function for every type passed in)

You basically have it. Let me make a comparison here to string literals and I think it will tie it up nicely.

Follow this example - we have a function called foo and foo just takes a string, gets it’s length, and the subtracts something from it (again, I’m just using .never_inline so the compiler doesn’t see right through what we’re trying to do):

fn count(x: []const u8) usize {
    return x.len;
}

export fn foo(x: usize) usize {
    const string: []const u8 = "hello";
    const len = @call(.never_inline, count, .{ string });
    return len - x;
}

Okay, so where does that string live? The thing is, it’s a literal string. It gets lifted out of the function and put somewhere else… that looks like this:

__anon_1460:
        .asciz  "hello"

So when we go to __anon_1460 we find our string. It doesn’t live in the bounds of foo.

Now, where is our vtable literal? Here:

&.{ .vcall = call }

It’s just a value - it doesn’t have a name, and yet we’re taking an address to it? How is that possible and not invalid? Well, it’s because Zig treats this like the string literal and it’s been lifted into “static” data.

EDIT: It’s also important that the instantiation can be treated like a constant. If you can parameterize it on only runtime known data, you’ll get the OP’s original problem.

What’s important to understand here is each function has it’s own unique vtable. This happens twice. Once for foo and once for bar. Hence, we have two vtables that live in a memory location that any instance of Foo or Bar can call:

const x: Foo...
const y: Foo...
// both get a pointer to the same vtable
const a = x.interface();
const b = y.interface();
``

What happens if zig will see it through? Optimize to the point where the call is unnecessary because we already know the length of “hello” and we can replace it with literal value inplace?

Will something change if we put the name? I thought the whole point was the analysis of the content/values that forms this struct.

Funny enough, it didn’t make a difference in this case - it’s just a habit of mine to stop the compiler from being smarter than the example I’m trying to make. I had to stay in debug mode.

Here’s all the assembly we get. I compile on release fast to remove debug symbols to find things more easily…

foo:
        mov     eax, 5
        sub     rax, rdi
        ret

The string doesn’t even exist in the code anymore.

1 Like

EDIT: Again, it’s also important that the instantiation can be treated like a constant. If you can parameterize it on only runtime known data, you’ll get the OP’s original problem.

It’s the fact that we’re taking a pointer to it and it can be treated like a constant - how can you take a pointer to it if it doesn’t exist somewhere? Since it has to exist somewhere, it’s gets lifted out and given an anonymous name.

Think of it this way - what’s a slice? It’s a pointer and a length. It’s pointing to something and that happens to be a string. A slice isn’t the string, it’s a pointer to it.

If I have an array of u8, then there’s no need to lift it anywhere. That memory is right where we’re at.

Same as this - const vtable = .{ .call = foo }. We’ve declared that we are going to keep the memory local and call it vtable.

This however const vtable: *const Vtable = &{ .call = foo } is not a vtable, it’s a pointer to one. So where does the vtable itself live?

2 Likes

I think I’m missing the main point in your explanation. It feels like it is not about taking an address of something that may not exist (and zig being so smart to automatically lift it up to somewhere else) but the point is when zig can lift up. My conclusion is that the compiler can make thing static (ie. we can refer to it anywhere in the code) as soon as it is not runtime. My current list is:

  • String literals
  • Number literals
  • Comptime known structs (otherwise, if for example 1 out of 10 fields of a struct happens to be runtime, then the whole struct cannot become static).
  • Function declarations
  • Globals?

So let’s go back to your original code for a second and look at a bit more assembly (sorry to keep doing this to you, but it’s worth it).

Let’s just focus on that init function.

    fn init(ptr: *anyopaque, f: *const fn (ptr: *anyopaque) f32) Shape1 {
        return .{
            .ptr = ptr,
            .vtab = &VTab{ .areaFn = f },
        };
    }

Here comes the assembly…

example.Shape1.init:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 32
        mov     rax, rdi
        mov     qword ptr [rbp - 32], rsi
        mov     qword ptr [rbp - 24], rdx
        mov     qword ptr [rdi], rsi
        mov     qword ptr [rbp - 16], rdx
        mov     rcx, qword ptr [rbp - 16]
        mov     qword ptr [rbp - 8], rcx
        lea     rcx, [rbp - 8]
        mov     qword ptr [rdi + 8], rcx
        add     rsp, 32
        pop     rbp
        ret

Notice something missing? Where’s the anonymous variable? Doesn’t exist. I’m going to take a stab here, but it looks like what has happened is that since it’s parameterizable by an unknown function at runtime, it can’t be treated like a constant. If it was, you wouldn’t be able to assign a new function to it every time you called init. So it gets created on the stack and dies when you leave.

That’s what @dimdin was saying with:

The difference here is that in the implementations where you take a parameter and assign it to that vtable, it can’t be constant and can’t be lifted out of the function.

The reason this works:

    pub fn init(obj: anytype) Shape2 {

Is because for every object type you pass to this function, you get a unique function to that type. It gets deduced and stamped out by the compiler. Then down here:

        return .{
            .ptr = obj,
            .vtab = &.{
                .draw = impl.draw,
                .move = impl.move,
            },

You only ever get one possible function for each impl that you’ve created. It’s always the same for any obj you pass in because you get unique types and only one function implementation provided per type. Because of that, the compiler knows that it’s always going to be the same, so it just makes one and makes it constant.

I’m speculating about how the compiler is thinking here, but effectively the result is the same. So I’ll append to my statement earlier…

It’s not just that it’s a pointer to a literal, it’s also that it can be treated like a constant.

1 Like

The difference here is that in the implementations where you take a parameter and assign it to that vtable, it can’t be constant and can’t be lifted out of the function.

Yes, understood! Thank you. Honestly I realized it the first time you and @dimdin mentioned it. What I didn’t understand is the rules when compiler is be able to “lift” things out in general, so that I can predict the values “static-ability”.

I think these are some glimpses of these rules:

It’s not just that it’s a pointer to a literal, it’s also that it can be treated like a constant.

Because of that, the compiler knows that it’s always going to be the same, so it just makes one and makes it constant.

–––

(sorry to keep doing this to you, but it’s worth it).

:smiley:

1 Like