Skip to content

Instantly share code, notes, and snippets.

@nvlled
Created June 23, 2025 05:41
Show Gist options
  • Save nvlled/8b30a7007c5ee461a7bf107f763b4e84 to your computer and use it in GitHub Desktop.
Save nvlled/8b30a7007c5ee461a7bf107f763b4e84 to your computer and use it in GitHub Desktop.
// Changes the first argument of a function type to *anyopaque
// Example:
// AnyOpaquify(fn(*Self, u8) void) will result to fn (*anyopaque, u8) void
pub fn AnyOpaquify(func: anytype) type {
const T = @TypeOf(func);
const info = @typeInfo(T).@"fn";
var P: [info.params.len]type = undefined;
for (info.params, 0..) |p, i| {
P[i] = p.type.?;
}
if (P.len == 0) {
@compileError("func must have at least one parameter");
}
switch (@typeInfo(info.params[0].type.?)) {
.pointer => {},
else => {
@compileError("first parameter must be a pointer");
},
}
const R = if (info.return_type == null) void else info.return_type.?;
const Signature = switch (P.len) {
0 => (fn () R),
1 => (fn (*anyopaque) R),
2 => (fn (*anyopaque, P[1]) R),
3 => (fn (*anyopaque, P[1], P[2]) R),
4 => (fn (*anyopaque, P[1], P[2], P[3]) R),
5 => (fn (*anyopaque, P[1], P[2], P[3], P[4]) R),
6 => (fn (*anyopaque, P[1], P[2], P[3], P[4], P[5]) R),
7 => (fn (*anyopaque, P[1], P[2], P[3], P[4], P[5], P[6]) R),
8 => (fn (*anyopaque, P[1], P[2], P[3], P[4], P[5], P[6], P[7]) R),
9 => (fn (*anyopaque, P[1], P[2], P[3], P[4], P[5], P[6], P[7], P[8]) R),
else => @compileError("too many parameters"),
};
return Signature;
}
// Casts the given function to a function that takes a first param *anyopaque
pub fn castAnyOpaqueFnArg(func: anytype) *AnyOpaquify(func) {
return @constCast(@ptrCast(&func));
}
// Checks at compile time if T is a valid struct interface, such that:
// - struct T contains a vtable pointer
// - vtable contains function pointers
// - for each function in vtable, there's a corresponding method in struct T
// - each corresponding method in struct T must be `pub inline`
pub fn TypeCheckInterface(comptime T: anytype) type {
const info = @typeInfo(T);
const struct_info = switch (info) {
else => @compileError("interface must be a struct"),
.@"struct" => |s| s,
};
const vtable = blk: {
var found = false;
inline for (struct_info.fields) |field| {
if (std.mem.eql(u8, field.name, "vtable")) {
const t = @typeInfo(field.type);
if (t != .pointer) @compileError("vtable must be a pointer");
switch (@typeInfo(t.pointer.child)) {
.@"struct" => {
found = true;
break :blk t.pointer.child;
},
else => @compileError("vtable must be a struct pointer"),
}
}
}
if (!found) {
@compileError("interface struct must have a vtable");
}
};
const vtable_info = @typeInfo(vtable);
switch (vtable_info) {
.@"struct" => |s| {
for (s.fields) |field| {
const is_fn_pointer = blk: switch (@typeInfo(field.type)) {
.pointer => |p| {
break :blk @typeInfo(p.child) == .@"fn";
},
else => break :blk false,
};
if (!is_fn_pointer) {
@compileError("vtable member `" ++ field.name ++ " ` must be function pointer");
}
if (!std.meta.hasMethod(T, field.name)) {
@compileError("vtable member `" ++ field.name ++ "` must have a corresponding public interface method");
}
if (@typeInfo(@TypeOf(@field(T, field.name))).@"fn".calling_convention != .@"inline") {
@compileError("interface method `" ++ field.name ++ "` must be inline");
}
// no need to check for method signatures here since
// something like:
// pub inline fn method(self: *const Intf) void {
// self.vtable.method(self.ptr);
// }
// ... will be caught by the compiler if the type params don't match
}
},
else => {},
}
return T;
}
test "castAnyOpqaueFnArg" {
const exampleFunc = struct {
fn _(x: *u8) u8 {
return x.* + 10;
}
}._;
const f = castAnyOpaqueFnArg(exampleFunc);
var x: u8 = 100;
const a: *anyopaque = @ptrCast(&x);
try std.testing.expect(f(a) == 110);
}
test "simple interface check" {
const X = TypeCheckInterface(struct {
ptr: *anyopaque,
// remove or rename this field will fail
vtable: *const struct {
method1: *const (fn () void),
method2: *const (fn () void),
},
// remove or rename this method1 or method2 will fail
pub inline fn method1() void {}
// if this method is not pub or inline, it will fail
pub inline fn method2() void {}
// this one has no matching vtable entry,
// so renaming, removing will not fail
fn blah() void {}
});
_ = X;
}
test "interface creation" {
const Writer = TypeCheckInterface(struct {
const Self = @This();
ptr: *anyopaque,
vtable: *const struct {
write: *(fn (*anyopaque, []const u8) usize),
},
pub inline fn write(self: Self, msg: []const u8) usize {
return self.vtable.write(self.ptr, msg);
}
});
const StdoutWriter = struct {
const Self = @This();
pub fn write(_: *Self, msg: []const u8) usize {
debug.print("write: {s}", .{msg});
return msg.len;
}
pub fn writer(self: *Self) Writer {
return .{
.ptr = self,
.vtable = &.{
.write = castAnyOpaqueFnArg(write),
},
};
}
};
const exampleFunc = struct {
fn _(w: Writer) void {
_ = w.write("test output");
}
}._;
const w = @constCast(&StdoutWriter{});
exampleFunc(w.writer());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment