I’m getting a grasp of interfaces, and am deeply unwilling to have to write self = @ptrCast(@alignCast(ptr)) all over the place. There MUST be a better way, no?
( See also: https://www.youtube.com/watch?v=2Q8gB2OXB2E )
( See also (idea of casting the function): Vtable interfaces and the role of @ptrCast and @alignCast - #11 by efjimm )
So I watched the video above that neatly describes Zig interfaces, specifically std.mem.Allocator, and thought to myself:
“If I can cast *anyopaque to *T, then surely I can cast the function that receives *anyopaque to one that receives *T. And if I can cast the function, then surely I can cast the whole VTable containing the functions.”
Thus:
const std = @import("std");
const Allocator = std.mem.Allocator;
const print = std.debug.print;
pub fn main(init: std.process.Init) !void {
var workers: [2]WorkerInterface = undefined;
workers[0] = try .init(init.gpa, Repeater, .{ .repeat_count = 3 });
workers[1] = try .init(init.gpa, Tagger, .{ .tag = "bold" });
defer for (workers) |worker| worker.deinit();
for (workers) |worker| {
worker.doStringStuff("Hello");
print("=> {d}\n", .{worker.doNumberStuff(123)});
}
}
pub const WorkerInterface = struct {
ptr: *anyopaque,
vtable: *const VTable(anyopaque),
fn VTable(T: type) type {
return struct {
// VTable methods defined by init():
getGPA: *const fn (*T) Allocator,
selfDestroy: *const fn (*T) void,
// VTable methods defined by interface implementations:
deinit: *const fn (*T) void,
doStringStuff: *const fn (*T, []const u8) void,
doNumberStuff: *const fn (*T, usize) usize,
};
}
pub fn init(gpa: Allocator, T: type, params: T.Params) !@This() {
const ptr = try gpa.create(T);
ptr.params = params;
ptr.gpa = gpa;
try ptr.init();
const vtable = try gpa.create(VTable(T));
// VTable methods defined by init():
const DefinedByInterface = struct {
fn getGPA(t: *T) Allocator {
return t.gpa;
}
fn selfDestroy(t: *T) void {
t.gpa.destroy(t);
}
};
vtable.getGPA = DefinedByInterface.getGPA;
vtable.selfDestroy = DefinedByInterface.selfDestroy;
// VTable methods defined by interface implementations:
vtable.deinit = T.deinit;
vtable.doStringStuff = T.doStringStuff;
vtable.doNumberStuff = T.doNumberStuff;
return .{
.ptr = ptr,
.vtable = @ptrCast(vtable),
};
}
pub fn deinit(self: @This()) void {
const gpa = self.vtable.getGPA(self.ptr);
self.vtable.deinit(self.ptr);
self.vtable.selfDestroy(self.ptr);
gpa.destroy(self.vtable);
}
pub fn doStringStuff(self: @This(), string: []const u8) void {
self.vtable.doStringStuff(self.ptr, string);
}
pub fn doNumberStuff(self: @This(), number: usize) usize {
return self.vtable.doNumberStuff(self.ptr, number);
}
};
pub const Repeater = struct {
pub const Params = struct { repeat_count: usize };
params: Params,
gpa: Allocator,
pub fn init(_: *@This()) !void {}
pub fn deinit(_: *@This()) void {}
pub fn doStringStuff(self: *@This(), string: []const u8) void {
for (0..self.params.repeat_count) |_| print("{s}\n", .{string});
}
pub fn doNumberStuff(self: *@This(), number: usize) usize {
for (0..self.params.repeat_count) |_| print("{d} ", .{number});
print("\n", .{});
return number * self.params.repeat_count;
}
};
pub const Tagger = struct {
pub const Params = struct { tag: []const u8 };
params: Params,
gpa: Allocator,
pub fn init(_: *@This()) !void {}
pub fn deinit(_: *@This()) void {}
pub fn doStringStuff(self: *@This(), string: []const u8) void {
print(
"<{s} type=\"string\">{s}</{s}>\n",
.{ self.params.tag, string, self.params.tag },
);
}
pub fn doNumberStuff(self: *@This(), number: usize) usize {
print(
"<{s} type=\"number\">{d}</{s}>\n",
.{ self.params.tag, number, self.params.tag },
);
return number;
}
};
Is this sound?