Skip to content

Instantly share code, notes, and snippets.

@boomlinde
Forked from T1nk3r1/interface.zig
Created January 12, 2024 13:34
Show Gist options
  • Save boomlinde/e157735ec612784f91e14fe36518bfcf to your computer and use it in GitHub Desktop.
Save boomlinde/e157735ec612784f91e14fe36518bfcf to your computer and use it in GitHub Desktop.
const std = @import("std");
// Returns an instance of Interface wrapped around the implementation
// at ptr
pub fn as(comptime Interface: type, ptr: anytype) Interface {
return .{
.ptr = @ptrCast(@alignCast(ptr)),
.vtable = vtable(Interface.VTable, @TypeOf(ptr.*)),
};
}
// Given a VTable and an implementation type Impl, return
// a pointer to a static populated VTable corresponding to the implementation
pub fn vtable(comptime VTable: type, comptime Impl: type) *const VTable {
const fields = switch (@typeInfo(VTable)) {
.Struct => |f| f.fields,
else => @compileError("Can't populate non-struct vtable"),
};
const VTableContainer = struct {
const vt = blk: {
var out: VTable = undefined;
for (fields) |field| {
const method = @field(Impl, field.name);
if (!sigEql(field.type, @TypeOf(method)))
@compileError("Invalid method signature, expected: " ++
@typeName(field.type) ++
", found: " ++
@typeName(@TypeOf(method)));
const func_ptr: *const anyopaque = @ptrCast(&method);
@field(out, field.name) = @ptrCast(func_ptr);
}
break :blk out;
};
};
return &VTableContainer.vt;
}
fn sigEql(comptime func0: type, comptime func1: type) bool {
const info0 = getBuiltinFn(func0) orelse
@compileError("Expected function, found: " ++
@typeName(func0));
const info1 = getBuiltinFn(func1) orelse
@compileError("Expected function, found: " ++
@typeName(func0));
if (info0.params.len != info1.params.len) return false;
if (!isSelfPtr(info0.params[0].type.?) and !isSelfPtr(info1.params[0].type.?))
return false;
for (info0.params[1..], info1.params[1..]) |p0, p1| {
if (p0.type != p1.type) return false;
}
return true;
}
inline fn isSelfPtr(comptime T: type) bool {
return switch (@typeInfo(T)) {
.Pointer => |ptr| if (ptr.size != .One) false else true,
else => false,
};
}
inline fn getBuiltinFn(comptime T: type) ?std.builtin.Type.Fn {
return switch (@typeInfo(T)) {
.Fn => |f| f,
.Pointer => |ptr| switch (@typeInfo(ptr.child)) {
.Fn => |f| f,
else => null,
},
else => null,
};
}
test "Example" {
const Greeter = struct {
// Still need to declare a type erased VTable manually
pub const VTable = struct {
hello: *const fn (*anyopaque, i32) []const u8,
};
ptr: *anyopaque,
vtable: *const VTable,
pub fn init(ptr: anytype) @This() {
// No need to manually wrap implementation methods
return as(@This(), ptr);
}
// Still need to wrap the VTable functions to expose as methods
// on the interface
pub fn hello(self: @This(), i: i32) []const u8 {
return self.vtable.hello(self.ptr, i);
}
};
const ExampleGreeter = struct {
buf: []u8,
pub fn hello(self: *const @This(), i: i32) []const u8 {
return std.fmt.bufPrint(self.buf, "Hello, {}", .{i}) catch "Hello";
}
};
const OtherExampleGreeter = struct {
buf: []u8,
pub fn hello(self: *const @This(), i: i32) []const u8 {
return std.fmt.bufPrint(self.buf, "Hello, {}", .{-i}) catch "Hello";
}
};
var buf: [20]u8 = undefined;
var impl1 = ExampleGreeter{ .buf = &buf };
var impl2 = OtherExampleGreeter{ .buf = &buf };
const g1 = Greeter.init(&impl1);
const g2 = Greeter.init(&impl2);
try std.testing.expect(Greeter == @TypeOf(g1));
try std.testing.expect(Greeter == @TypeOf(g2));
try std.testing.expectEqualStrings("Hello, 5", g1.hello(5));
try std.testing.expectEqualStrings("Hello, -5", g2.hello(5));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment