Programmatically creating an interface table for a set of implementation fxns

i would like to automatically “reify” a struct type whose fields are pointers to functions implemented within another container… said another way, i’m trying to write a generic function that encapsulates the “function-pointer-table” design pattern commonly used when implementing abstract interfaces…

here’s my current attempt:

const std = @import("std");

const print = std.log.debug;

const Impl = struct {
    pub fn foo(x: u32) void {
        print("foo({d})", .{x});
    }
    pub fn bar() void {
        print("bar", .{});
    }
};

fn ItabFrom(T: type) type {
    const ti = @typeInfo(T);
    comptime var fld_list: []const std.builtin.Type.StructField = &.{};
    inline for (ti.Struct.decls) |d| {
        const dval = @field(T, d.name);
        const dti = @typeInfo(@TypeOf(dval));
        if (dti == .Fn) {
            const FT = @Type(.{ .Pointer = .{
                .size = .One,
                .is_const = true,
                .is_volatile = false,
                .alignment = 1,
                .address_space = .generic,
                .child = @TypeOf(dval),
                .is_allowzero = false,
                .sentinel = null,
            } });
            const fld = std.builtin.Type.StructField{
                .name = d.name,
                .type = FT,
                .default_value = dval,
                .is_comptime = false,
                .alignment = 0,
            };
            fld_list = fld_list ++ ([_]std.builtin.Type.StructField{fld})[0..];
        }
    }
    const IT = @Type(.{ .Struct = .{
        .layout = .auto,
        .fields = fld_list,
        .decls = &.{},
        .is_tuple = false,
        .backing_integer = null,
    } });
    return IT;
}

pub fn main() void {
    const IT = ItabFrom(Impl);
    print("size = {d}", .{@sizeOf(IT)});
}

but here’s the output i receive when compiling this program (under 0.12.0):

src\main.zig:42:16: error: comptime dereference requires 'fn (u32) void' to have a well-defined layout
    const IT = @Type(.{ .Struct = .{
               ^~~~~
src\main.zig:53:24: note: called from here
    const IT = ItabFrom(Impl);
               ~~~~~~~~^~~~~~

my goal is for this synthesized struct to effectively match the following explicitly defined struct (which i could tediously write by hand):

const Impl = struct {
    foo: *const fn (u32) void = &Impl.foo,
    bar: *const fn () void = &Impl.bar,
};

my use-case is somewhat simpler than the “classic vtable”, in that my functions do NOT have a self parameter; no need to carry around an opaque ptr in the interface struct…

1 Like

Not sure if this is what you’re looking for, but I got a lot of value out of the article Interfaces, or: Runtime Polymorphism in Zig - specifically, I think this snippet from it might be useful to you:

/// Iterators can be iterated with while loops. The next
/// function returns null when the iterator is exhausted.
pub fn Iterator(comptime T: type) type {
    return struct {
        const Self = @This();
        nextFn: fn (self: *Self) ?T,
        resetFn: fn (self: *Self) void,
        pub fn next(self: *Self) ?T {
            return self.nextFn(self);
        }
        pub fn reset(self: *Self) void {
            return self.resetFn(self);
        }
    };
}

Like you were looking for, there is no anyopaque pointer needed, just function pointers. Sorry if I’m totally off-base here though!

Edited: See my updated answer - Programmatically creating an interface table for a set of implementation fxns - #5 by AndrewCodeDev

The problem here looks like the *const F instead of just F. You can get past this as direct function types because it looks like the pointer-to-function is expecting well defined layout. You can do this:

const std = @import("std");

const print = std.log.debug;

const Impl = struct {
    pub fn foo(x: u32) void {
        print("foo({d})", .{x});
    }
    pub fn bar() void {
        print("bar", .{});
    }
};

fn ItabFrom(T: type) type {
    comptime {
        const TI = @typeInfo(T);
    
        const decls = TI.Struct.decls;
    
        var fld_list: [decls.len]std.builtin.Type.StructField = undefined;
    
        for (decls, 0..) |decl, i| {
    
            const func = @field(T, decl.name);
            
            fld_list[i] = std.builtin.Type.StructField{
                .name = decl.name,
                .type = @TypeOf(func),
                .default_value = func,
                .is_comptime = false,
                .alignment = 0,
            };
        }
    
        const freeze = fld_list;
    
        return @Type(.{ .Struct = .{
            .layout = .auto,
            .fields = freeze[0..],
            .decls = &.{},
            .is_tuple = false,
            .backing_integer = null,
        }});
    }
}

pub fn main() void {
    const itab = ItabFrom(Impl){};
    itab.bar();
}

Essentially, we’re transforming declarations into member fields and using those instead. At this level, I’m not sure what advantage you’d get by having a *const F as oppsed to the direct function type. Either way, when you remove the pointer qualifier, you’ll see that it works.

Now, what you may be trying to do is construct a generic interface from a set of struct decls. I don’t see why that can’t be done using the same technique. At some level, I just throw up my hands and say “where’s the duck when you need him?”

Okay, @biosbob, this actually works for the *const F - I do this by distinguishing between reification and initialization here and I do that through another indirection. In short, I don’t see a way to do it through direct reification, but if you can stomach assignments via an inline for then this should do it for you:

const std = @import("std");

const print = std.log.debug;

const Impl = struct {
    pub fn foo(x: u32) void {
        print("foo({d})", .{x});
    }
    pub fn bar() void {
        print("bar", .{});
    }
};

fn ItabType(T: type) type {
    comptime {
        const TI = @typeInfo(T);
    
        const decls = TI.Struct.decls;
    
        var fld_list: [decls.len]std.builtin.Type.StructField = undefined;
    
        for (decls, 0..) |decl, i| {
    
            const func = @field(T, decl.name);
            
            fld_list[i] = std.builtin.Type.StructField{
                .name = decl.name,
                .type = *const @TypeOf(func),
                .default_value = null,
                .is_comptime = false,
                .alignment = 0,
            };
        }
    
        const freeze = fld_list;
    
        return @Type(.{ .Struct = .{
            .layout = .auto,
            .fields = freeze[0..],
            .decls = &.{},
            .is_tuple = false,
            .backing_integer = null,
        }});
    }
}

pub fn initItab(comptime impl: type) ItabType(impl) {

    var itab: ItabType(impl) = undefined;

    inline for (comptime std.meta.declarations(impl)) |decl| {
        @field(itab, decl.name) = @field(impl, decl.name);
    }

    return itab;
}

pub fn main() void {
    const itab = initItab(Impl);
    itab.bar();
}

What’s even funnier here is that this also works using a comptime keyword on the init function itself:

pub fn main() void {
    const itab = comptime initItab(Impl);
    itab.bar();
}

So… maybe you can actually get both constraints satisfied at once here? Odd. I think someone should look a little deeper into the discrepancy here.

4 Likes

no problem with separating reification from initialization… my objective is to minimize the about of information required by my “newbie” users to specify an interface… the more i create artificats like a vtable inside my framework, the better…

what is VERY important is the generated vtable is usable as runtime; to that end, having @sizeOf yield an non-zero answer was my test…

in my use-case, these generate vtables are used in my framework’s “upstream meta-program” – where efficiency is much less of an issue… if the compiler can figure out that this inline initialized table in fact is constant, that’s upside… as a practical matter, there is no “monkey-patching” happening at runtime; i’m simply call implementation functions through an interface object…

Yeah, with that last method, I’m getting a reported size of 16 bytes when marked with and without comptime. That’s true for debug and release-fast. It looks like that indirection may do the trick then. I’d still like to know more about the discrepancy with reification here. Either way, hope that helps.

1 Like

I suddenly realized what the issue here is while chewing on some dried squid: you’re missing one level of indirection. default_value needs to be a pointer to a function pointer:

const std = @import("std");

const print = std.log.debug;

const Impl = struct {
    pub fn foo(x: u32) void {
        print("foo({d})", .{x});
    }
    pub fn bar() void {
        print("bar", .{});
    }
};

fn ItabType(T: type) type {
    comptime {
        const TI = @typeInfo(T);

        const decls = TI.Struct.decls;

        var fld_list: [decls.len]std.builtin.Type.StructField = undefined;

        for (decls, 0..) |decl, i| {
            const func = @field(T, decl.name);
            const func_ptr = &func;

            fld_list[i] = std.builtin.Type.StructField{
                .name = decl.name,
                .type = *const @TypeOf(func),
                .default_value = @ptrCast(&func_ptr),
                .is_comptime = false,
                .alignment = 0,
            };
        }

        const freeze = fld_list;

        return @Type(.{ .Struct = .{
            .layout = .auto,
            .fields = freeze[0..],
            .decls = &.{},
            .is_tuple = false,
            .backing_integer = null,
        } });
    }
}

pub fn main() void {
    const itab: ItabType(Impl) = .{};
    itab.bar();
}

:squid::squid::squid:

2 Likes

Aha, sneaky!

1 Like