What is the current best practice for runtime polymorphism based on a vtable?
I guess this is a pretty common question, so I tried to do my homework before posting here :
First of all, I read the excellent article on interfaces based on tagged unions (Easy Interfaces with Zig 0.10.0 - Zig NEWS). This looks great and I’ll use it as much as possible.
But let’s say I actually need a vtable.
There’s a series of blogposts, I think the most up-to-date is this one: Zig Interfaces for the Uninitiated, an update - Zig NEWS
I’ve taken the code snippets from the blogpost and put them together https://github.com/lhk/zig_runtime_polymorphism/blob/main/src/blogpost.zig. But that doesn’t compile (maybe it’s because of zig 0.12.0-dev.926+3be8490d8)
So I decided to take a look at the standard library and see how you do it there.
I’ve tried to replicate the pattern of mem.Allocator
and implemented the example from the blogpost with it (an Iterator
interface and a Range
implementation).
Is this how you would implement an interface, or am I missing something?
The interface definition:
const Iterator = @This();
ptr: *anyopaque,
vtable: *const VTable,
pub const VTable = struct {
next: *const fn (ctx: *anyopaque) ?u32,
};
pub fn next(self: Iterator) ?u32 {
return self.vtable.next(self.ptr);
}
A concrete implementation:
const Iterator = @import("interface_definition.zig");
// implementation of an interface, update to https://zig.news/kilianvounckx/zig-interfaces-for-the-uninitiated-an-update-4gf1
const Range = struct {
const Self = @This();
start: u32 = 0,
end: u32,
step: u32 = 1,
pub fn next(ptr: *anyopaque) ?u32 {
const self: *Self = @ptrCast(@alignCast(ptr));
if (self.start >= self.end) return null;
const result = self.start;
self.start += self.step;
return result;
}
pub fn iterator(self: *Self) Iterator {
return .{ .ptr = self, .vtable = &.{ .next = next } };
}
};
The full code, with tests can be found here: https://github.com/lhk/zig_runtime_polymorphism/tree/main/src
Finally, I don’t understand what @ptrCast
and @alignCast
are doing.
Do they perform any runtime checks? If yes, isn’t this interface pattern inefficient, because it does the type checks every time next
is called?
If there is indeed some form of type checking, could you point me to a documentation/resource where I can learn more about how it does that?
But my intuition is that @ptrCast
and @alignCast
just tell the compiler “you’re getting a pointer to an instance of Range
. Now you know how to resolve the next
function”.
In this case, why is there a separate @alignCast
on top of the @ptrCast
? Shouldn’t it be enough to tell the compiler “this is a pointer to Range
”? How could such a pointer have an alignment which is different from the alignment of a Range
struct?
Overall, my understanding of alignment is very rudimentary. Basically I just thought that this is something to be aware of when laying out a struct, in order not to waste memory. The idea to do an @alignCast
is confusing to me, I always thought that alignment is just what it is, i.e. there’s a 1:1 mapping between data structure and alignment. Having the type of a pointer should determine the type of the data it points to, which then should also determine the alignment. Therefore, I’m confused to see both a @ptrCast
and an @alignCast
. I’d expect the alignCast to be redundant. If there’s a good writeup on this, I’d be very interested in it.