Last active
January 15, 2024 12:48
-
-
Save lovely-error/929a37e435a5103a74f8d0218f813de4 to your computer and use it in GitHub Desktop.
Easified type erasure in zig
This file contains 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
pub fn main() !void { | |
const AbstractedObject = Opaque(&.{ | |
.{"f1", fn (*anyopaque) u1}, | |
.{"f2", fn (*anyopaque, u8, u8, u8, u8) L}, | |
.{"ferry", fn (*anyopaque)error{boo}!void}, | |
}); | |
var a = std.heap.ArenaAllocator.init(std.heap.page_allocator); | |
var T = K {}; | |
var w = try AbstractedObject.make(&T, a.allocator()); | |
const v = w.invoke("f1", .{}); | |
std.debug.assert(v == T.field); | |
const val : struct {u8,u8,u8,u8} = .{0, 1, 2, 3}; | |
const ret = w.invoke("f2", val); | |
std.debug.assert(std.mem.eql(u8, &val, &ret)); | |
w.invoke("ferry", .{}) catch std.debug.assert(true); // OK, fails | |
w.dispose(a.allocator()); | |
var d : D = .{}; | |
w = try AbstractedObject.make(&d, a.allocator()); | |
const smth = w.invoke("f1", .{}); | |
std.debug.assert(smth == d.field); | |
const ret2 = w.invoke("f2", val); | |
const ret3 = .{3,2,1,0}; | |
std.debug.assert(std.mem.eql(u8, &ret2, &ret3)); | |
w.dispose(a.allocator()); | |
} | |
const L = struct {u8,u8,u8,u8}; | |
const K = struct { | |
field:u1 = 1, | |
pub fn f1(self: *@This()) u1 { return self.field; } | |
pub fn f2(self: *@This(), a: u8, b: u8, c:u8, d:u8) L { | |
std.debug.assert(self.field == 1); | |
return .{a,b,c,d}; | |
} | |
pub fn ferry() !void { | |
return error.boo; | |
} | |
}; | |
const D = struct { | |
field:u1 = 0, | |
pub fn f1(self: *@This()) u1 { return self.field; } | |
pub fn f2(self: *@This(), a: u8, b: u8, c:u8, d:u8) L { | |
std.debug.assert(self.field == 0); | |
return .{d,c,b,a}; | |
} | |
pub fn ferry() !void { | |
return error.boo; | |
} | |
}; | |
fn Opaque( | |
comptime procs: []const struct {[:0]const u8, type}, | |
) type { | |
var fields : []const std.builtin.Type.StructField = &.{}; | |
var needMut = false; | |
inline for (procs) |proc| { | |
if (proc[0].len == 0) { | |
@compileError("Empty function names are not allowed"); | |
} | |
const proc_meta = @typeInfo(proc[1]); | |
if (!(proc_meta == .Fn)) { | |
@compileError("Expected function type, got " ++ @typeName(proc_meta)); | |
} | |
const fun = proc_meta.Fn; | |
if (fun.params.len != 0) { | |
const Selfie = fun.params[0].type.?; | |
const MetaSelfie = @typeInfo(Selfie); | |
if (!(MetaSelfie == .Pointer)) { | |
const msg = | |
"First argument to a function item '" ++ proc[0] ++ "' should be a pointer, but is is instead '" ++ @typeName(Selfie) ++ "'"; | |
@compileError(msg); | |
} | |
if (MetaSelfie.Pointer.child != anyopaque) { | |
@compileError("Expected an opaque pointer as self on interface function '" ++ proc[0] ++ "', but got '" ++ @typeName(MetaSelfie.Pointer.child) ++ "' instead"); | |
} | |
if (!MetaSelfie.Pointer.is_const) needMut = true; | |
} | |
fields = fields ++ &[1]std.builtin.Type.StructField{ | |
std.builtin.Type.StructField { | |
.is_comptime = false, | |
.alignment = @alignOf(proc[1]), | |
.name = proc[0], | |
.type = *const proc[1], | |
.default_value=null | |
} | |
}; | |
} | |
const fields_ = fields; | |
const PTable = @Type(std.builtin.Type { .Struct = std.builtin.Type.Struct { | |
.is_tuple = false, | |
.decls = &.{}, | |
.layout = .Auto, | |
.fields = fields | |
}}); | |
const FSig = struct { | |
fn Validate(comptime proc_name: []const u8) type { | |
var cand : ?type = null; | |
inline for (fields_) |f| { | |
if (std.mem.eql(u8, f.name, proc_name)) { | |
cand = f.type; | |
} | |
} | |
if (cand == null) @compileError( | |
"No function named '" ++ proc_name ++ "' was register on construction of 'Opaque' object" | |
); | |
return cand.?; | |
} | |
fn MkRetTyForProc(comptime proc_name: []const u8) type { | |
const t = @This().Validate(proc_name); | |
return @typeInfo(@typeInfo(t).Pointer.child).Fn.return_type.?; | |
} | |
fn MkTailArgsTyForProc(comptime proc_name: []const u8) type { | |
const proc = @This().Validate(proc_name); | |
const proc_meta = @typeInfo(@typeInfo(proc).Pointer.child).Fn; | |
var args_ty : []const type = &.{}; | |
if (proc_meta.params.len > 1) { | |
const tpars = proc_meta.params[1..]; | |
inline for (tpars) |tpar| { | |
args_ty = args_ty ++ &[1]type { | |
tpar.type.? | |
}; | |
} | |
} | |
return std.meta.Tuple(args_ty); | |
} | |
}; | |
const needMut_ = needMut; | |
const SomeFatPtr = struct { | |
object_ptr: ?if (needMut_) *anyopaque else *const anyopaque, | |
proc_table: *const PTable, | |
pub fn make(obj: anytype, allocator: std.mem.Allocator) !@This() { | |
const T_ = @TypeOf(obj); | |
const MetaT = @typeInfo(T_); | |
if (MetaT != .Pointer) { | |
@compileError("Expected a pointer to an object, got " ++ @typeName(T_)); | |
} | |
if (MetaT.Pointer.is_const and needMut_) { | |
@compileError("Interface specified on opaque objects requires object to be referenced through mutable pointer, but it is const instead. Consider changing object binding from const to var."); | |
} | |
const T__ = MetaT.Pointer.child; | |
inline for (procs) |forv| { | |
const present_on = @hasDecl(T__, forv[0]); | |
if (!present_on) { | |
@compileError("No function '" ++ forv[0] ++ "' found on " ++ @typeName(T__)); | |
} | |
} | |
var wr : @This() = undefined; | |
wr.object_ptr = @ptrCast(obj); | |
var table_ : PTable = undefined; | |
inline for (procs) |pn| { | |
if (!@hasDecl(T__, pn[0])) { | |
@compileError("No function named '" ++ pn[0] ++ "' found on '" ++ @typeName(T__) ++ "'. Did you mark it pub?"); | |
} | |
const MetaF = @typeInfo(@TypeOf(@field(T__, pn[0]))); | |
const params = &MetaF.Fn.params; | |
if (params.len != 0) { | |
const tinfo = @typeInfo(params.*[0].type.?); | |
if (tinfo != .Pointer) { | |
@compileError("Expected first argument of function '" ++ pn[0] ++ "' on '" ++ @typeName(T__) ++ "'' to be a pointer, but it is " ++ @typeName(params.*[0].type.?) ++ " instead"); | |
} | |
if (tinfo.Pointer.child != T__) { | |
@compileError("Expected first argument of function '" ++ pn[0] ++ "' on '" ++ @typeName(T__) ++ "' to be a pointer to Self, but it points to " ++ @typeName(tinfo.Pointer.child) ++ " instead"); | |
} | |
comptime var iface_fun : ?type = null; | |
inline for (procs) |proc|{ | |
if (comptime std.mem.eql(u8, proc[0], pn[0])) { | |
iface_fun = proc[1]; break; | |
} | |
} | |
const meta_iface_fun = @typeInfo(iface_fun.?); | |
if (meta_iface_fun.Fn.params.len != 0) { | |
const meta_iface_selfie = @typeInfo(meta_iface_fun.Fn.params[0].type.?); | |
if (meta_iface_selfie.Pointer.is_const and !tinfo.Pointer.is_const) { | |
@compileError("Self argument of the function '" ++ pn[0] ++ "' defined on the interface is const, but on object it has different mutability"); | |
} | |
if (!meta_iface_selfie.Pointer.is_const and tinfo.Pointer.is_const) { | |
@compileError("Self argument of the function '" ++ pn[0] ++ "' defined on the interface is mutable, but on object '" ++ @typeName(T__) ++ "'' it has different mutability"); | |
} | |
} | |
if (params.len != meta_iface_fun.Fn.params.len) { | |
@compileError("Argument count mismatch for function '" ++ pn[0] ++ "'"); | |
} | |
comptime var ix = 1; | |
inline while (true) { | |
if (ix == params.len) break; | |
const obj_param = params.*[ix].type.?; | |
const iface_param = meta_iface_fun.Fn.params[ix].type.?; | |
if (iface_param != obj_param) { | |
@compileError("Argument type mismatch on function '" ++ pn[0] ++ "'. Interface has '" ++ @typeName(iface_param) ++ "', object has '" ++ @typeName(obj_param) ++ "'"); | |
} | |
ix += 1; | |
} | |
if (MetaF.Fn.return_type.? != meta_iface_fun.Fn.return_type.?) { | |
@compileError("Return type mismatch on function '" ++ pn[0] ++ "'. Interface has '" ++ @typeName(meta_iface_fun.Fn.return_type.?) ++ "', object has '" ++ @typeName(MetaF.Fn.return_type.?) ++ "'"); | |
} | |
} | |
// todo: try harder to find possible sig mismatches | |
@field(table_, pn[0]) = @ptrCast(&@field(T__, pn[0])); | |
} | |
const table = try allocator.create(PTable); | |
table.* = table_; | |
wr.proc_table = table; | |
return wr; | |
} | |
pub fn invoke( | |
self: if (needMut_) *@This() else *const @This(), | |
comptime proc_name: []const u8, | |
args: FSig.MkTailArgsTyForProc(proc_name) | |
) FSig.MkRetTyForProc(proc_name) { | |
const f = @field(self.proc_table, proc_name); | |
const coe_args = .{self.object_ptr.?} ++ args; | |
return @call(.auto, f, coe_args); | |
} | |
pub fn dispose( | |
self: *@This(), | |
allocator: std.mem.Allocator | |
) void { | |
allocator.destroy(self.proc_table); | |
self.object_ptr = null; | |
} | |
}; | |
return SomeFatPtr; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment