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…

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.

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.

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:

Aha, sneaky!