Zig NEWS

loading...
Cover image for Interfaces in Zig

Interfaces in Zig

David Vanderson
Updated on ・3 min read

Zig has a unique pattern for interfaces using composition and @fieldParentPtr.

Let's make an interface that picks a number, and implement it two different ways.

First 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});
  }
}
Enter fullscreen mode Exit fullscreen mode

So we want an interface named Interface that has a pick method:

const Interface = struct {
    // can call directly: iface.pickFn(iface)
    pickFn: fn (*Interface) i32,

    // allows calling: iface.pick()
    pub fn pick(iface: *Interface) i32 {
        return iface.pickFn(iface);
    }   
};
Enter fullscreen mode Exit fullscreen mode

We could have gotten away with just the function pointer pickFn, but that would mean the usage code would have to call it iface.pickFn(iface), repeating the interface pointer. Adding pub fn pick makes it a bit nicer to use iface.pick()

Let's make an implementation that picks randomly:

const PickRandom = struct {
    // specific to PickRandom
    r: std.rand.DefaultPrng,

    // implement the interface
    interface: Interface,

    fn init() PickRandom {
        return .{
          .r = std.rand.DefaultPrng.init(0),
          // point the interface function pointer to our function
          .interface = Interface{ .pickFn = myPick },
          };  
    }   

    fn myPick(iface: *Interface) i32 {
        // compute pointer to PickRandom struct from interface member pointer
        const self = @fieldParentPtr(PickRandom, "interface", iface);
        return self.r.random.intRangeAtMost(i32, 10, 20);
    }   
};
Enter fullscreen mode Exit fullscreen mode

For implementation we embed an Interface instance into our struct. In init we set the function pointer so that iface.pick() will call myPick(iface). Since myPick is called with a pointer to PickRandom.interface, in order to access other members of PickRandom we use @fieldParentPtr to get a pointer to the containing struct. More about this later.

Here's an implementation that picks sequentially:

const PickSeq = struct {
    x: i32,

    interface: Interface,

    fn init() PickSeq {
        return .{
          .x = 100,
          .interface = Interface{ .pickFn = myPick },
          };
    }

    fn myPick(iface: *Interface) i32 {
        const self = @fieldParentPtr(PickSeq, "interface", iface);
        self.x += 1;
        return self.x;
    }
};
Enter fullscreen mode Exit fullscreen mode

This implementation has different member data, but follows the same pattern of composing the interface inside the struct and setting the function pointer to its own myPick.

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();
    foo(&pick_seq.interface);
    std.debug.print("main: {d}\n", .{pick_seq.interface.pick()});

    // example of how copying an interface is wrong
    var junk: [100]u8 = [_]u8{200} ** 100;
    var iface_copy = pick_seq.interface;
    // this will output junk numbers
    foo(&iface_copy);
}

Enter fullscreen mode Exit fullscreen mode

We can either call the interface from the enclosing struct pick_random.interface.pick() or get a pointer to the interface foo(&pick_random.interface) and use that. More about the junk stuff later.

Outputs:

$ zig run interface.zig 
foo 1: 11
foo 2: 17
foo 3: 11
main: 16
foo 1: 101
foo 2: 102
foo 3: 103
main: 104
foo 1: 119169025
foo 2: 119169026
foo 3: 119169027
Enter fullscreen mode Exit fullscreen mode

The first 4 lines are from pick_random, then next 4 lines are from pick_seq.

The last 3 lines are the junk. The junk is different each run for me. What's going on? See below.

@fieldParentPtr

@fieldParentPtr(PickRandom, "interface", iface)

This zig builtin solves the problem "Given a pointer to a member of a struct, give me a pointer to the struct." We run into this problem inside myPick - it gets a pointer to pick_random.interface and we want a pointer to pick_random.

Here's a picture of what's going on in memory:

pick_random-------------  <-- we want a pointer to this
| pick_random.r        |     
| |           |        |     
| |-----------|        |     
| pick_random.interface|  <-- we get a pointer to this
| |                   ||
| |-------------------||
|----------------------|
Enter fullscreen mode Exit fullscreen mode

Zig knows the memory layout and can calculate how much it needs to subtract from a member pointer to get to the beginning of a struct. We tell @fieldParentPtr the struct type (PickRandom), the name of the member ("interface"), and the pointer we have (iface). It returns a pointer to the enclosing struct (pick_random).

Copying an interface can lead to junk

Here's the problem line:

var iface_copy = pick_seq.interface;
Enter fullscreen mode Exit fullscreen mode

This copies pick_seq.interface. The copy will have a valid function pointer, but the copy no longer lives inside a PickSeq struct. So now memory looks like this:

junk  <-- @fieldParentPtr produces a pointer to this
junk
junk
iface_copy|  <-- myPick gets a pointer to this
|         |
|---------|
Enter fullscreen mode Exit fullscreen mode

So inside myPick, self now points at junk (or other structs/vars), causing it to reference (and overwrite) other things in memory. This can cause almost anything to happen including garbage output and crashes, so be careful about copying interface members.

// bad
// var iface_copy = pick_seq.interface;

// good - only copying the pointer
var iface_ptr_copy = &pick_seq.interface;

// bad, function call may copy
// foo(pick_seq.interface);

// good
foo(&pick_seq.interface);
Enter fullscreen mode Exit fullscreen mode

Discussion (5)

Collapse
kristoff profile image
Loris Cro

Great job, thanks for taking the time to write this explanation! For an extra bit of context, depending on one's needs, there are also other ways of implementing interfaces as explained in this talk by Alex Naskos:

youtube.com/watch?v=AHc4x1uXBQE

Collapse
jackji profile image
jack • Edited

Nice explanation!
Though the problem of the style is parent object must be valid as long as it’s interface is being used, which is easy to break if you “accidentally” allocated object on stack in some factory and returns pointer to it’s inner-interface, so heap allocation must be used to avoid that.
There’s a nice general purpose interface lib worth checking out: github.com/alexnask/interface.zig

EDIT: Turns out the zig-dev team have noticed the issue, a proposal has been accepted:
github.com/ziglang/zig/issues/7769

Collapse
david_vanderson profile image
David Vanderson Author

Yes you are right about having to be careful. Thanks for the link!

Collapse
andrewrk profile image
Andrew Kelley

Note that pinned structs will make the "junk" example a compile error :)

Collapse
david_vanderson profile image
David Vanderson Author

Yes! Thanks for adding that, I should have included it.