I had a late night idea of how could one make using vtable based interfaces easier, as in nearly automatic.
So, I decided to just write this late night thought out until it compiled (can very much be improved in a few ways):
const std = @import("std");
const Interface = struct {
object: *anyopaque,
greet: *const fn (*anyopaque, struct{*std.Io.Writer}) std.Io.Writer.Error!void,
pub fn call_greet(self: Interface, writer: *std.Io.Writer) std.Io.Writer.Error!void {
return self.greet(self.object, .{writer});
}
};
const Greeter = struct {
name: []const u8,
pub fn greet(self: Greeter, writer: *std.Io.Writer) std.Io.Writer.Error!void {
try writer.print("Hello, {s}\n", .{self.name});
}
};
const Fareweller = struct {
name: []const u8,
// yes, this works without the pub here
fn greet(self: Fareweller, writer: *std.Io.Writer) std.Io.Writer.Error!void {
try writer.print("Bye, {s}\n", .{self.name});
}
};
fn adapt(I: type, O: type, object: *O) I {
const fields = std.meta.fields(I);
var interface: I = undefined;
inline for (fields) |f| {
if (comptime std.mem.eql(u8, f.name, "object")) continue;
const ptr_info = @typeInfo(f.type);
const info = @typeInfo(ptr_info.pointer.child);
const params = info.@"fn".params;
const Tuple = params[1].type.?;
const oldArgsFields = @typeInfo(Tuple).@"struct".fields;
comptime var argsTypes: [oldArgsFields.len+1]type = undefined;
comptime {
for (argsTypes[1..], oldArgsFields) |*t, arg_field| {
t.* = arg_field.type;
}
}
argsTypes[0] = O;
const ArgsTuple = @Tuple(&argsTypes);
const Helper = struct {
pub fn call(o: *anyopaque, param: Tuple) info.@"fn".return_type.? {
const Args = ArgsTuple;
var args: Args = undefined;
args[0] = @as(*O, @alignCast(@ptrCast(o))).*;
inline for (0..param.len) |i| {
args[i+1] = param[i];
}
return @call(.auto, @field(O, f.name), args);
}
};
@field(interface, f.name) = &Helper.call;
}
interface.object = object;
return interface;
}
fn do(writer: *std.Io.Writer, objects: []const Interface) std.Io.Writer.Error!void {
for (objects) |o| {
try o.call_greet(writer);
}
}
pub fn main(init: std.process.Init) !void {
var buffer: [1024]u8 = undefined;
var stdout_file: std.Io.File.Writer = .init(.stdout(), init.io, &buffer);
const stdout = &stdout_file.interface;
var greeter: Greeter = .{
.name = "World",
};
var fareweller: Fareweller = .{
.name = "World",
};
try do(stdout, &.{
adapt(Interface, Greeter, &greeter),
adapt(Interface, Fareweller, &fareweller),
});
try stdout.flush();
}
So, how much does anyone here love or hate this? Or maybe both?
I am very much in the last camp.
I love it since Greeter, Fareweller and Interface don’t need to know of each other.
I hate it since this is starting to become comptime abuse imo.