Created
June 23, 2025 05:41
-
-
Save nvlled/8b30a7007c5ee461a7bf107f763b4e84 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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