Zig is moving away from the embedded struct style of interfaces towards a separate interface-on-demand style.
The old style caused some bad performance because when an implementation function changed state, the compiler assumed that the embedded interface struct might have changed, so it couldn't optimize as well.
Let's redo this old example in the new style. We'll have an interface that picks a number, and implement it two different ways.
The usage code:
const std = @import("std");
pub fn foo(iface: Interface) void {
var i: usize = 1;
while (i < 4) : (i += 1) {
const p = iface.pick();
std.debug.print("foo {d}: {d}\n", .{ i, p });
}
}
Now we can copy interface structs so foo(iface: *Interface) becomes foo(iface: Interface).
Define the new-style interface named Interface that has a pick method:
const Interface = struct {
// pointer to the implementing struct (we don't know the type)
impl: *c_void,
// can call directly: iface.pickFn(iface.impl)
pickFn: fn (*c_void) i32,
// allows calling: iface.pick()
pub fn pick(iface: *const Interface) i32 {
return iface.pickFn(iface.impl);
}
};
Now we include a pointer to the implementing struct, and pass that to pickFn instead of a pointer to the interface.
Any type can implement this interface, so we have to use *c_void so impl can point to anything.
Our implementation that picks randomly:
const PickRandom = struct {
// specific to PickRandom
r: std.rand.DefaultPrng,
fn init() PickRandom {
return .{
.r = std.rand.DefaultPrng.init(0),
};
}
// implement the interface
fn interface(self: *PickRandom) Interface {
return .{
.impl = @ptrCast(*c_void, self),
.pickFn = myPick,
};
}
fn myPick(self_void: *c_void) i32 {
// cast typeless impl pointer to ourselves
var self = @ptrCast(*PickRandom, @alignCast(@alignOf(PickRandom), self_void));
// old random interface
return self.r.random.intRangeAtMost(i32, 10, 20);
// new random interface
//return self.r.random().intRangeAtMost(i32, 10, 20);
}
};
We no longer embed an Interface struct.
Now we call interface() to return an Interface struct that points back to this instance of PickRandom. We use @ptrCast to remove (erase) the type.
The Interface struct returned has a (type erased) struct pointer to this instance of PickRandom and a function pointer pickFn pointing to our function myPick. The interface will pass the struct pointer into myPick.
In myPick we know that self_void is a pointer to our struct, but we have to reconstruct the type. In addition to the @ptrCast we need to reconstruct the pointer alignment as well.
Without @alignCast zig complains that *c_void has alignment 1 while *PickRandom has alignment 8. That's on my x64 machine.
Here's the implementation that picks sequentially:
const PickSeq = struct {
x: i32,
fn init() PickSeq {
return .{
.x = 100,
};
}
fn interface(self: *PickSeq) Interface {
return .{
.impl = @ptrCast(*c_void, self),
.pickFn = myPick,
};
}
fn myPick(self_void: *c_void) i32 {
// cast typeless impl pointer to ourselves
var self = @ptrCast(*PickSeq, @alignCast(@alignOf(PickSeq), self_void));
self.x += 1;
return self.x;
}
};
Now we'll use both implementations:
pub fn main() !void {
var pick_random = PickRandom.init();
foo(pick_random.interface());
std.debug.print("main: {d}\n", .{pick_random.interface().pick()});
var pick_seq = PickSeq.init();
const pick_seq_iface = pick_seq.interface();
foo(pick_seq_iface);
std.debug.print("main: {d}\n", .{pick_seq_iface.pick()});
}
We can either get and call the interface together pick_random.interface().pick() or keep a copy of the interface for reuse. You can also have multiple copies of the interface pointing to the same implementation struct.
New vs. Old
In the old style you could easily trip up by copying the embedded interface struct. In the new style that's no longer a problem. The interface struct can be copied and passed to functions without worry.
Is this how all zig interfaces are moving? As I write this the Random interface has been changed and merged. But a similar change to the Allocator interface is raising other issues.
Oldest comments (12)
Great post, thank you!
Reading some examples in the standard library it seems that the
myPickfunction can also accept a correctly typed pointer and thatc_voidwill be compatible even without cast.Example:
github.com/ziglang/zig/blob/master...
Good point. It looks like you can do the pointer cast in the interface:
github.com/ziglang/zig/blob/master...
I'll have to look at that and maybe write a followup about it. Thanks!
Great article, it's a good time to revisit previous experiments like github.com/alexnask/interface.zig
Does anyone else find the new style easier to reason about?
For me the new style is a much better fit for my mental model of interfaces and impls. Not entirely sure why. Somehow the @fieldParentPtr pattern never felt intuitive, even after I'd worked with it a little bit.
I agree. It feels more like a straight-up function pointer. Also it's easier for me to understand how you can separate the interface struct, copy and store it around in different data structures. As long as the impl struct stays put!
Two things:
1) There's some stray
foo(pick_random.interface());andfoo(pick_seq_iface);in themain()example which refer to a function that's not defined.2) This "stinks" a bit because of the widespread use of
*c_void, which is documented to be used only for C ABI compatibility. One way you can avoid this is to define aconst AnyInterface = opaque {};and then replace the various*c_voidwith*AnyInterface. In theory this could also be justanyopaquein the future.foo() is defined at the very top of the post.
I'm not sure how to feel about using
*c_void. Does usingopaquechange the code/cast in a good way?Oh, my bad! I don't know how I missed the definition of
foo().As for
*c_voidvsopaque {}, AFAIK they're functionally identical, butopaque {}allows you to define a strong type alias whereas*c_voidwill accept just about any pointer. In retrospect, it probably doesn't matter very much in this specific case, but it seems good form anyway.*c_voidis going to be renamedanyopaquegithub.com/ziglang/zig/issues/323
I know, but that just means it'll accept any "opaque" type, as I clarified in zig.news/jmc/comment/51.
This is relatively simple. Thanks。
I hope the official can support closures and interfaces.