#Getting a segfault in implementing an interface

1 messages · Page 1 of 1 (latest)

stiff violet
#

I'm trying to implement a simple interface (for learning purposes) named Shape1 which hopefully allow me to iterate over different shapes generically and execute their area() methods:

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 simply segfaults. I'm not sure what's wrong with it. Maybe because I'm taking &Vtable {...} in init. Even if so, I don't know how to do the same without using an allocator (and it seems other people manage to do the same "fat pointer" interfaces without using any allocators). Please, help! 🥲

#

Accessing shape is what segfaults but I don't know why. Shapes seem ok and residing on the stack.

quartz mesa
#
.vtab = &VTab{ .areaFn = f }

You're taking a stack address and trying to store it in the struct, using it later.

stiff violet
#

Yes, the problem was it. Basically, the reason other people were able to work around it, ie. not using the allocator for creating/returning vtable, is because they relied on the zig ability to make values static when it is possible. For example, if I do this:

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 = &.{ // HERE vtab will refer to the static address 
                    // becase init() is generate uniquely for every `obj` type
            .draw = impl.draw,
            .move = impl.move,
        },
    };
}

or this:

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

    // HERE interface implementation returned from struct itself
    fn shape1(self: *Square) Shape1 {
        return Shape1{
            .ptr = self,
            .vtab = &.{ // HERE address is static because... `area` is also static?
                .areaFn = area,
            },
        };
    }
};

Vtable gets static address and I don't get any segfaults.

Essentially, my question now is how to predict when zig lift up values and make them static across program execution?

empty carbon
#

(impl.draw, impl.move, and arena, are all constants)