Video about Zig interfaces

Hello guys, I made a video about zig interfaces, I tried to make it beginner friendly. Any thoughts?

https://www.youtube.com/watch?v=2Q8gB2OXB2E

12 Likes

Hey, great explanation and visuals! I’m sending this to my friend who’s currently learning Zig. The Italian brainrot at the end caught me off guard though haha

2 Likes

Thinking of doing an interface, and this comes in a perfect time. I will watch and review the video later.

1 Like

Thank you! I (almost) always say something stupid at the end of my videos ^^

1 Like

Watched the video. It is really nice and clear to follow the steps, but I have some questions to the video:

Seems I am not sure about how to create these objects. If I want to create a triangle with the shape interface, if that means I have to create a triangle struct first, obtain that pointer, and creating the shape interface struct with inject the triangle pointer? Or there is actually a simpler way?

This one is a bit more open ended: If I have multiple functions in an interface, is that means I have to include all the functions in the gen struct, while every opaque function need to do a pointer and align cast independently. When the interface grows, seems like the casting will be highly repetitive. I am thinking if it is possible to reduce the number of casting in each of the function in the gen struct.

2 Likes

What the video teaches is a vtable, a type of a runtime interface (dynamic dispatch).

I think the example in the video is slightly bad, because you typically don’t want to use vtable to encode something like shapes. In fact you want to avoid dynamic dispatch all together.

Anyhow here’s different kind of “interfaces” you can do in zig:

Comptime interface

This is basically anytype in nutshell. Anytype can only be used as function arguments and can take in anything. There exists userland compile time interface implementations that try to make this more typed:
https://github.com/permutationlock/zimpl
https://github.com/Dok8tavo/Interfacil
https://github.com/nilslice/zig-interface

Note that anytype in interfaces is most likely going to be less relevant in future if https://github.com/ziglang/zig/issues/23367 gets implemented, which would allow zig to guarantee optimizations for things like vtables.

pub fn draw(shape: anytype, opts: DrawOptions) void {
   shape.draw(opts); // compiler errors if shape doesn't have the function
}

Vtable

This is std.mem.Allocator, std.io.AnyWriter, std.io.AnyReader, std.Random.
Vtables are extensible and can be implemented by a third party code.
The video already covers this so I’ll just drop the sample code.

const Shape = struct {
   ptr: *anyopaque,
   vtable: struct {
      draw: *const fn (opq: *anyopaque, opts: DrawOptions) void,
   },
};

const Circle = struct {
   pos: Vec2d,
   r: f32,
   
   pub fn draw(opq: *anyopaque, opts: DrawOptions) void {
         const self: *const @This() = @ptrCast(@alignCast(opq));
         // ...
   }
  
   pub fn shape(self: *const @This()) Shape {
       return .{
            .ptr = self,
            .vtable = .{
               .draw = draw,
            },
       };
   }
};

Union

Unions can represent a runtime static interface. You can use a pointer members where the union size will always be the same, but requires a stable pointer that you can de-reference:

const Shape = union (enum) {
  circle: *Circle,
  triangle: *Triangle,

  pub fn draw(self: @This(), opts: DrawOptions) void {
      switch (self) {
          inline else |shape| shape.draw(opts),
      }
  }
};

Alternatively it can store the members as values, but the union size will be the size of the largest member:

const Shape = union (enum) {
  circle: Circle,
  triangle: Triangle,

  pub fn draw(self: @This(), opts: DrawOptions) void {
      switch (self) {
          inline else |shape| shape.draw(opts),
      }
  }
};

Intrusive interface

Using @fieldParentPtr you can retieve a parent struct of a field. This lets you do intrusive interfaces that are extensible similar to vtables.

const Shape = struct {
    draw: *const fn (shape: *const Shape, opts: DrawOptions) void,
};

const Sphere = struct  {
   pos: Vec2d,
   r: f32,

   shape: Shape = .{ .draw = draw },
   
   pub fn draw(shape: *const Shape, opts: DrawOptions) void {
         const self = @fieldParentPtr(@This(), "shape", shape);
         // ...
   }
};

While I understand the video code is for example sake. For actual code where you need to deal with shapes (lets say collision code), you really want to std.MultiArrayList instead and batch every shape combination separately.

16 Likes

Thank you for your reponse.

You are right, I realize that it was a very bad example.
I should’ve taken the example of making a library where dynamic dispatch is needed.
I thought it could be helpful for Java dev, for example, to have a relatable example, but it’s probably going to lead them to use this “runtime interface”, instead of a better solution.

Will try to clarify it with a pinned comment, and maybe make a followup video with better options, like “don’t make interfaces in zig, do this instead” or something.

Still have a lot to learn, and need to get rid of the Java OOP brainrot! I will do better :slight_smile:

3 Likes

I don’t feel that “bad” is the appropriate adjective, perhaps “not optimal” would be a more fair description, especially given the purpose of the video is to offer a beginner-friendly overview of one concept, not provide code that results in the most optimal machine instructions possible. Your example is perfectly acceptable for the former, the issue is that Zig has multiple ways to create interfaces, each with drawbacks and strengths, and each having their appropriate use-cases.

The Vtable form you explained was explained well, and is perhaps the easiest to wrap your head around when coming from a high-level language like Java, C#, or Go, and one’s understanding of “interface” may be very different when approached with that mindset. As it uses dynamic dispatch, it is generally going to be the least optimal of the bunch, but is easy to understand and implement.

I personally enjoyed the video, and would encourage you to make more on the topic, perhaps exploring other methods of creating interfaces, such as those that were previously mentioned.

9 Likes

Thank you! I will definitely do more videos about this, and zig in general!

3 Likes

Nicely done! Good pacing, visuals, and nice small additions of humor.