I’m still relatively new to Zig, but I’ve not seen this interface pattern before. I’ve not actually used it in practice, but am interested to hear opinions on whether this is a good approach or not.
I see the advantages as follows:
- All of the logic is in the interface, nothing in the implementers.
- Implementers can take a self param of their own type, rather than anyopaque and casting it.
Disadvantages:
- Possibly an extra function call? Maybe it gets inlined?
- Slightly more awkward syntax than calling .allocator() or .reader()
See below for an implementation of an allocator-style interface.
MyInterface implements the generic alloc and free vtable functions as would be typical for a zig fat pointer interface.
My addition is an init function that creates the functions to cast the *anyopaque to the correct type, rather than have the implementing function do it. (There is probably a better name than init for this).
To create an interface off an implementation:
var imp: MyImpl = .init();
var i = MyInterface.init(MyImpl, &imp);
The full proof of concept is below. It’s a small change, but I think it could be good approach to a Reader-style interface, which can wrap any struct that provides a read() implementation.
Interested to hear people’s thoughts.
const MyInterface = struct {
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
alloc: *const fn (*anyopaque, len: usize) []u8,
free: *const fn (*anyopaque, mem: []u8) void,
};
pub fn alloc(i: MyInterface, len: usize) []u8 {
return i.vtable.alloc(i.ptr, len);
}
pub fn free(i: MyInterface, mem: []u8) void {
return i.vtable.free(i.ptr, mem);
}
pub fn init(comptime T: type, instance: *T) MyInterface {
if (std.meta.hasFn(T, "alloc") and
std.meta.hasFn(T, "free"))
{
return .{
.ptr = instance,
.vtable = &.{
.alloc = struct {
fn alloc_trampoline(ptr: *anyopaque, len: usize) []u8 {
const self: *T = @ptrCast(@alignCast(ptr));
return self.alloc(len);
}
}.alloc_trampoline,
.free = struct {
fn free_trampoline(ptr: *anyopaque, mem: []u8) void {
const self: *T = @ptrCast(@alignCast(ptr));
return self.free(mem);
}
}.free_trampoline,
},
};
} else {
@compileError(std.fmt.comptimePrint("{s} must supply alloc(self, len) and free(self, mem)", .{@typeName(T)}));
}
}
};
pub const MyImpl = struct {
gpa: std.heap.DebugAllocator(.{}),
pub fn init() MyImpl {
const gpa: std.heap.DebugAllocator(.{}) = .init;
return .{
.gpa = gpa,
};
}
pub fn alloc(self: *MyImpl, len: usize) []u8 {
std.debug.print("alloc\n", .{});
return self.gpa.allocator().alloc(u8, len) catch unreachable;
}
pub fn free(self: *MyImpl, mem: []u8) void {
std.debug.print("free\n", .{});
return self.gpa.allocator().free(mem);
}
};
pub fn main() !void {
std.debug.print("start.\n", .{});
var imp: MyImpl = .init();
var i = MyInterface.init(MyImpl, &imp);
const a = i.alloc(10);
i.free(a);
std.debug.print("done.\n", .{});
}
const std = @import("std");