#How to fill out a v-table with functions from a struct?

1 messages ยท Page 1 of 1 (latest)

sour rapids
#

I have something like this, and a struct that implements the methods, and I'd like to pass the struct to a function expecting the IFoo vtable interface. What's the syntax for doing this ๐Ÿค”

const IHeapPageVTbl = struct {
    const Self = @This();
    getFreeSpace: fn (self: *Self) u16,
    getNumRecords: fn (self: *Self) u16,
};

const HeapPageVTbl = struct {
    const Self = @This();
    fn getFreeSpace(self: *Self) usize {
       // code
    }
}

pub fn takesIHeapPageVTbl(comptime vtbl: *IHeapPageVTbl) void {}

comptime var vtbl = HeapPageVTbl{};
takesIHeapPageVTbl(.{
    .getFreeSpace = vtbl.getFreeSpace,
});
#

Like in C you might do this:

struct IHeapPageVTbl {
    void (*destroy)(struct HeapPage* self);
    void (*insert)(struct HeapPage* self, const char* record, uint32_t record_length);
    void (*remove)(struct HeapPage* self, uint32_t slot_index);
    void (*update)(struct HeapPage* self, uint32_t slot_index, const char* record, uint32_t record_length);
    void (*get)(struct HeapPage* self, uint32_t slot_index, char* record, uint32_t record_length);
};

struct HeapPage* HeapPage_create(uint64_t lsn)
void HeapPage_destroy(struct HeapPage* self)
void HeapPage_insert(struct HeapPage* self, const char* record, uint32_t record_length)
void HeapPage_remove(struct HeapPage* self, uint32_t slot_index)
void HeapPage_update(struct HeapPage* self, uint32_t slot_index, const char* record, uint32_t record_length)
void HeapPage_get(struct HeapPage* self, uint32_t slot_index, char* record, uint32_t record_length)

struct IHeapPageVTbl HeapPage_vtbl = {
    .destroy = HeapPage_destroy,
    .insert = HeapPage_insert,
    .remove = HeapPage_remove,
    .update = HeapPage_update,
    .get = HeapPage_get,
};
brave pilot
#

There's no magic here to help you, so you'd do it identically to C.
Though, in Zig, there's something of a convention of giving the implemention a member function that returns an instance of the interface.
This is what Allocator does with gpa.allocator(), etc.

sour rapids
#

Ah, I think my newbie-ness might be showing a bit -- attempting to hand the vtable struct references to the functions doesn't seem to compile in Zig

#
src/main.zig:135:29: error: expected struct or union; found '*main.IHeapPageVTbl'
        .getFreeSpace = vtbl.getFreeSpace,
                        ~~~~^~~~~~~~~~~~~

src/main.zig:135:25: error: expected struct or union; found '*main.IHeapPageVTbl'
        .getFreeSpace = &vtbl.getFreeSpace,
                        ^~~~~~~~~~~~~~~~~~
#

Where that's comptime var vtbl = HeapPageVTbl{};

brave pilot
#

Right - that won't because those functions are being accessed incorrectly. You want to access it through the type and not the instance; HeapPageVTbl.getFreeSpace.

#

Declarations inside a type are not part of an instance; they are just namespaced within the struct type.

#

This is a demonstration of that:

const S = struct {
    const init = S_init;
};

fn S_init() S {
    return .{};
}
sour rapids
#

Oh I see what you're saying

brave pilot
#

Oh, but also, you want *const fn, not fn.

#

The latter is a comptime-only value, like type.

#

Whereas the former is a function pointer.

sour rapids
#

Right, just realizing with this the way it's written I'd not be able to pass anything else

brave pilot
#

Yeah - that's the other problem ๐Ÿ˜„
Allocator gets around this by having an init function helper.
See, the actual function pointer in the vtable for that takes an *anyopaque, for exactly this reason.
The init function takes function pointers to use in the vtable which take *Self, and it makes some functions that take the *anyopaque, cast it to *Self, and then calls the functions you passed to it.

#

(Self would be the thing that 'implements' the interface.)

sour rapids
#

Ah this is a fair bit more complicated-sounding than how it works in C, though it's probably safer
I ought to look at how Allocator works I suppose

brave pilot
#

It's actually the same, AFAICT.
It's just that the functions are wrapped so that you don't need to do the pointer-cast inside the function itself, because that's done in the wrapper instead.

#
void HeapPage_remove(struct HeapPage* self, uint32_t slot_index);

void gen_remove(void *self, uint32_t slot) {
    struct HeapPage *ptr = (struct HeapPage *)self;
    HeapPage_remove(ptr, slot);
}

...

struct IHeapPageVTbl HeapPage_vtbl = {
    // ...
    .remove = gen_remove,
    // ...
};
#

(See lib/std/mem/Allocator.zig:69 to see the Zig code that makes these intermediate functions.)

#

But keep in mind that you don't HAVE to do this intermediate function thing. It's only for the convenience of not having to cast in each vtable proc.

sour rapids
#

Oh okay, I actually got it working/compiled now. Thank you!! ๐Ÿ™

brave pilot
#

Outstanding ๐Ÿฆพ ๐Ÿ˜„

#

Happy to help o7

sour rapids
#
const IHeapPageVTbl = struct {
    const Self = @This();
    getFreeSpace: *const fn (self: *Self) u16,
    getNumRecords: *const fn (self: *Self) u16,
    getRecord: *const fn (self: *Self, record_index: u16) []u8,
    insertRecord: *const fn (self: *Self, record: []const u8) ?u16,
    deleteRecord: *const fn (self: *Self, record_index: u16) void,
};

pub fn HeapPageImpl(comptime vtbl: *const IHeapPageVTbl) type {
    return struct {
        const Self = @This();

        vtbl: *const IHeapPageVTbl,
        page: HeapPage,

        pub fn init() Self {
            return Self{
                .vtbl = vtbl,
                .page = HeapPage{},
            };
        }

        pub fn getFreeSpace(self: *Self) usize {
            return self.vtbl.getFreeSpace(self);
        }

        pub fn getNumRecords(self: *Self) usize {
            return self.vtbl.getNumRecords(self);
        }

        pub fn insertRecord(self: *Self, record: []const u8) ?usize {
            return self.vtbl.insertRecord(self, record);
        }
    };
}
#
const HeapPageImplExample = HeapPageImpl(.{
    .getFreeSpace = HeapPageVTbl.getFreeSpace,
    .getNumRecords = HeapPageVTbl.getNumRecords,
    .insertRecord = HeapPageVTbl.insertRecord,
    .getRecord = HeapPageVTbl.getRecord,
    .deleteRecord = HeapPageVTbl.deleteRecord,
});
#

I think this is right? (the actual HeapPageVTble implementation is not shown, it's pretty big lol)

#

but it compiles

brave pilot
#

That's an interesting way of doing it; that's not how it's commonly done ๐Ÿ˜

sour rapids
#

oh LOL, what's the common idiom

brave pilot
#
const Arena = struct {
    buf: []u8,
    used: usize,

    fn init(buf: []u8) Arena {
        return .{
            .buf = buf,
            .used = 0,
        };
    }

    fn allocator(arena: *Arena) Allocator {
        return Allocator.init(arena, alloc, resize, free);
    }

    fn reset(arena: *Arena) void {
        arena.used = 0;
    }

    fn alloc(arena: *Arena, len: usize, ptr_align: u29, len_align: u29, ret_addr: usize) Error![]u8 {
        if (arena.used + len > arena.buf.len) return error.OutOfMemory;

        const ptr = arena.buf[arena.used..][0..len]; // TODO: alignment
        arena.used += len;
        return ptr;
    }

    fn resize(arena: *Arena, buf: []u8, buf_align: u29, new_len: usize, len_align: u29, ret_addr: usize) ?usize {
        if (@ptrToInt(buf.ptr) != @ptrToInt(&arena.buf[arena.used - buf.len]) return null;
        arena.used -= buf.len;
        arena.used += new_len;
        return new_len;
    }
    
    fn free(arena: *Arena, buf: []u8, buf_align: u29, ret_addr: usize) void {
        // NOTE: nothing, since it's an arena
    }
}
sour rapids
#

Ah so everything gets a pointer to itself as a first argument instead?

brave pilot
#

Well - that's the same as what you've got with the self: *Self.
Allocator.init makes the wrappers I spoke of earlier though of course --- the actual vtable in this case takes void*s as the first argument, which you would otherwise cast to *Arena inside the alloc, resize and free functions in this example, if it didn't make those wrappers.

brave pilot
sour rapids
#

Thanks for taking the time to explain all of this to me, still trying to wrap my head around all this Ziggi-ness ๐Ÿ˜…

brave pilot
#

Happy to help o7 ๐Ÿ˜„
I would recommend just reading the code; it's fairly approachable, even if comptime stuff can make it slightly more busy.
This is what Allocator.init looks like, in entirety, for example:

#
pub fn init(
    pointer: anytype,
    comptime allocFn: fn (ptr: @TypeOf(pointer), len: usize, ptr_align: u29, len_align: u29, ret_addr: usize) Error![]u8,
    comptime resizeFn: fn (ptr: @TypeOf(pointer), buf: []u8, buf_align: u29, new_len: usize, len_align: u29, ret_addr: usize) ?usize,
    comptime freeFn: fn (ptr: @TypeOf(pointer), buf: []u8, buf_align: u29, ret_addr: usize) void,
) Allocator {
    const Ptr = @TypeOf(pointer);
    const ptr_info = @typeInfo(Ptr);

    assert(ptr_info == .Pointer); // Must be a pointer
    assert(ptr_info.Pointer.size == .One); // Must be a single-item pointer

    const alignment = ptr_info.Pointer.alignment;

    const gen = struct {
        fn allocImpl(ptr: *anyopaque, len: usize, ptr_align: u29, len_align: u29, ret_addr: usize) Error![]u8 {
            const self = @ptrCast(Ptr, @alignCast(alignment, ptr));
            return @call(.{ .modifier = .always_inline }, allocFn, .{ self, len, ptr_align, len_align, ret_addr });
        }
        fn resizeImpl(ptr: *anyopaque, buf: []u8, buf_align: u29, new_len: usize, len_align: u29, ret_addr: usize) ?usize {
            assert(new_len != 0);
            const self = @ptrCast(Ptr, @alignCast(alignment, ptr));
            return @call(.{ .modifier = .always_inline }, resizeFn, .{ self, buf, buf_align, new_len, len_align, ret_addr });
        }
        fn freeImpl(ptr: *anyopaque, buf: []u8, buf_align: u29, ret_addr: usize) void {
            const self = @ptrCast(Ptr, @alignCast(alignment, ptr));
            @call(.{ .modifier = .always_inline }, freeFn, .{ self, buf, buf_align, ret_addr });
        }

        const vtable = VTable{
            .alloc = allocImpl,
            .resize = resizeImpl,
            .free = freeImpl,
        };
    };

    return .{
        .ptr = pointer,
        .vtable = &gen.vtable,
    };
}
#

(The pointer: anytype argument would be arena in my example.)

#

It's a nontrivial amount of stuff, but when you read each line through, it's actually pretty simple. ๐Ÿ˜„

sour rapids
#

I've noticed Zig really doesn't like when pointer alignments are different sizes lol

brave pilot
#

Right - unaligned things may be a segfault or just slow depending on the CPU architecture, so Zig wants you to be mindful of the choice ๐Ÿ˜„

#

@alignCast asserts that a pointer is, in fact, aligned as you say it is.

#

There's also *align(1) TYPE for when you're actually good with whatever it is. ๐Ÿ˜„

#

Reading and writing such unaligned pointers will cause Zig to use instructions that will work on all systems, but will likely be slower than the aligned version.

#

[It may not matter as much on x86 specifically, but still. Might only matter if the read/write straddles a cache line -- but I've not tested that myself yet.]

sour rapids