#Generating an interface helper function

1 messages · Page 1 of 1 (latest)

keen forge
#

Ive heard it said that "[runtime polymorphism should be the responsibility of libraries]."
I understand how the common interface pattern work in the standard library and have implemented it a couple times myself. given how much code im repeating when i do this, im curious to explore having some type of helper function to create these interface.

heres a snippet for an example interface im working with:

const Interface = @This();
const std = @import("std");

ptr: *anyopaque,
_foo: *const fn (*anyopaque) void,

inline fn check_decl(comptime T: type, fn_name: []u8) void {
    const pointed_type = @typeInfo(T).Pointer.child;
    const valid = @hasDecl(pointed_type, fn_name);
    if (!valid)
        @compileError(@typeName(pointed_type) + " does not properly implement " + fn_name);
}

// this fn is generated at compile time because of anytype, which is a VALUE not a type.
pub fn init(implementor: anytype) Interface {
    const ImplementorPointer = @TypeOf(implementor);
    const type_info = @typeInfo(ImplementorPointer);
    
    // General checks
    if (type_info != .Pointer)
        @compileError("Cannot initialize interface for non-pointer type");
    if (type_info.Pointer.size != .One)
        @compileError("Cannot initialize interface for multi-item pointer");
    if (type_info.Pointer.is_const)
        @compileError("Cannot initialize interface for non-mutable pointer");
    if (@typeInfo(type_info.Pointer.child) != .Struct)
        @compileError("Cannot initialize interface for pointer to non-struct");

    // Check this type is valid as Interface (duck typing)
    check_decl(ImplementorPointer, "foo");

    const gen = struct {
        pub fn _foo(impl: *anyopaque) void {
            const self: ImplementorPointer = @ptrCast(@alignCast(impl));
            type_info.Pointer.child.foo(self);
        }
    };

    return .{
        .ptr = implementor,
        ._foo = gen._foo,
    };
}

pub fn foo(self: *Interface) void {
    self._foo(self.ptr);
}

Id like to in some way, be able to have a generator function which creates structs at compile time for a given known interface. in mild pseudocode, something like this:

// a function which takes the requirements of the interface and creates
// a respective struct with an init fn and the proxy functions
fn create_interface(implementation_requirements) Interface {
  return struct {
    ptr: *anyopaque,
    _[fn_name]: *[fn type]
    pub fn init() { ... }
    pub fn [fn_name] (self, [params]) [return type] {
      return self._[fn_name]([params])
    }
  }
}

at first glance, there are many problems with making such a helper function. To me, chiefly among them are:

  • creating a struct with fields of dynamic name.
  • using a comptime loop of some effect to add fields to a struct.
  • creating a struct with dynamic decls
  • having those decls have comptime dynamic parameters and return types

Im looking for help if anyone has a good place to start, or information about why this isnt currently, or wont ever be supported.

hoary badger
#

you can't do 3/4

#

but you can fake it by making a comptime field

#

it's not as ergonomic tho

#

1/2 are doable with @Type(), would you be interested in that without 3/4?

umbral bough
#

A while ago I wanted to do a similar thing and wrote this ugly thing: https://hst.sh/ewipedefes.kotlin
You use it like this:

const Entity = Interface(.{
    .update = fn () void,
});

var player: struct {
    fn update(_: *@This()) void {
        std.debug.print("lol\n", .{});
    }
} = .{};

const entity = Entity.init(&player);
entity.call(.update, .{});
hoary badger
#

that actually doesn't look too bad

keen forge
#

@umbral bough wow, yeah that is definitely helpful. a great place for me to start digging in. theres a lot here for me to take in so i dont have much more useful to say other than thank you!

@hoary badger I hadnt realized the @Type() function existed! very cool, and used as well in scss' code. If i understand correctly, you saying 1/2 are possible but not 3/4, you are referring to a system where I could create the interface structs, but they wouldnt be able to have the proxy function, so i would be left with saying entity.update(entity.ptr, ...) in your example?

keen forge
#

so im playing around more with the constructed types using @Type, and if i understand the creation of the vtable correctly, could i not use the same pattern to generate wrapper functions on the interface?

#

(that is the loop on line 71 of the above paste link)

south frost
#

struct functions are part of struct decls, which you can't generate

keen forge
#

scratch that, ive realised that the vtable is not filled in until runtime, whereas init is compile time known, and is explicitly written

south frost
#
pub const StructField = struct {
    name: [:0]const u8,
    type: type,
    default_value: ?*const anyopaque,
    is_comptime: bool,
    alignment: comptime_int,
};
pub const Declaration = struct {
    name: [:0]const u8,
};
#

there's only name in Declaration

keen forge
#

yup. this is the code ive been referencing and completely missed it haha

south frost
#

I mean I don't know about compiler internals, but I think that code indicates how declarations work. To a struct, decls are only names that are lookup-able after a dot, and otherwise hidden

#

Their code/data is not stored in the struct itself

#

So you can't generate it through editing Type

#

Somebody correct me if I get this wrong😭

keen forge
#

that does make me wonder if theres some way that i can in a comptime block or something create a function and pass it off to the compiler to say "hey this function exists in that lookup table" but at that point im being a little ambitious.

south frost
#

you can't dynamically create functions, I think

keen forge
#

This totally seems to be the case, yes

ornate yarrow
south frost
ornate yarrow
#

Yes.

keen forge
#

understanding that we cant generate the functions, im realizing now that it seems we cant even type check them at compile time. I absolutely can ensure for example that an implementor is of a valid type, and has decls by a name, but due to the lack of additional information, there seems to be no way to check that the params / return type of the decls are aligned.

silver oriole
#

you can access the arguments and return type of a function with @typeInfo

keen forge
#

for decls?

#

as above std.builtin.Type.Declaration only provides a name, thus cannot be further inspected for consistency against the set of interface functions.

#

please let me know if im mistaken

umbral bough
#

@field() works on both types and structs if its a type it accesses the decl

keen forge
#

gooot it! perfect, thank you