Skip to content

Instantly share code, notes, and snippets.

@lovely-error
Last active January 15, 2024 12:48
Show Gist options
  • Save lovely-error/929a37e435a5103a74f8d0218f813de4 to your computer and use it in GitHub Desktop.
Save lovely-error/929a37e435a5103a74f8d0218f813de4 to your computer and use it in GitHub Desktop.
Easified type erasure in zig
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