4 ms·
Zig Interface Revisited
- ozgrakkurt 1y agoAlso can add compilation time as an upside to vtables. And using vtable in code feels cleaner compared to using generics
- kingstnap 1y agoYou've put the pub fn implBy(impl_obj: anytype) Logger Function in a weird place. Normal practice is for the specific implementations to have a getLogger function to create the logger. For example, the allocators in Zig have you instantiate the top level and then call .allocator() on them to get the vtable + pointer to self. It manages sanity a lot better, and the function implementations in the vtable can be private.
- ww520 1y agoI did it this way so that the implementation doesn't need to know about the interface at all. The programmers can string the interface and implementation together with, const intf = interface.implBy(implementation) This makes it possible to write an interface against any implementation objects. E.g. I can write an interface against the standard hashmap's get() and put() methods. Why would I do that? Let's say I have my custom hashmap with get() and put(), but I want to use the standard hashmap as fallback. A common interface working against my custom hashmap and the standard hashamp is possible.
- kingstnap 1y ago"The implementation doesn't need to know about the interface" Lol sure. It "doesn't need to know" it just happens to magically have the correct functions as public methods to exactly map into your vtable constructor. It's much easier to not do this. And have your implementations provide the vtable as a method. If your implementation doesn't provide a method for this you need to wrap it with an object that does. Which you've done but you've decided that wrapper object should be the interface itself which is weird.
- ww520 1y agoYou get back an interface object at the end anyway. Why force the implementation to produce it? And if you have a type implementing 5 different interfaces, like one for Comparable, one for Iteratible, etc., do you want to stick 5 interface constructors in the implementation?
- kingstnap 1y ago> Do you want to stick 5 interface constructors in implementation Yes. It's not as crazy as you might imagine, and it's more robust than the log delegator approach.
- ameixaseca 1y agoThere's a false dichotomy in this whole discussion: for instance, a third option is having the interface as a separate declaration. A simple example using traits (Rust): // logger.rs trait Logger { fn log_something(&self); } // mycustomlogger.rs struct MyCustomLogger; // logger.rs or mycustomlogger.rs impl Logger for MyCustomLogger { fn log_something(&self) { println!("Hello"); } } The actual `log_something` implementation for `MyCustomLogger` (ie, the block with the `println!` call) can be outside of the impl, as a function or inherent method, and just be called by the impl block - thus implementing similar "glue" as described in the article. The difference is that there is no strong requirement on where exactly it needs to be [1]. This would likely require Zig to support "interfaces" at the language level, though. -- [1] PS: there is the "orphan rule" for trait implementations, but I'm simplifying it here for the sake of the example EDIT: formatting
- ww520 1y agoThanks for bringing new perspective into the discussion. Your Rust code is a good example. I did take Rust for inspiration. The Zig version ‘Interface1.implBy(Impl2)’ is a rough equivalent to ‘impl Interface1 for Impl2’. It did it within the constraints of Zig.
- discreteevent 1y ago
- lenkite 1y agoWould it truly kill Zig to add native interface support ? All that ceremony is quite heavy and also somewhat error prone. If nothing else, maybe they can add some helpers in the standard lib or codegen in the standard tooling to make stuff like this easier ? So one need not manually code all the function pointer types for methods and the dispatch in the interface type . Yes, Zig is meant to be a low-level programming language, but stuff like this is basic table stakes nowadays. People will just make custom code generators after Zig 1.0 to do this exact thing - and they will likely be dozens of such custom code generators with subtle differences.
- thechao 1y agoI wonder if it's possible to not lock themselves into an ABI with a built in vtable implementation? I kinda get where they're coming from: let vtables be a user problem; but, yeah, modern compilers can do magic when they "know" the rules for built in vtables. It kinda feels like they're leaving a real performance opportunity on the ground?
- wavemode 1y agoThere's just one feature Zig lacks, which would have allowed for full automation of interface definitions. In Zig you can programmatically construct data types with compile-time programming, but such types aren't allowed to have methods: https://github.com/ziglang/zig/issues/6709 https://github.com/ziglang/zig/issues/6709 This is an intentional limitation, because the language creator worries the feature would be abused.
- hmry 1y agoI strongly dislike "this feature could be abused, so we won't add it" as reasoning for language design decisions. It just doesn't sit right with me. I think designing to avoid "misuse" (i.e. accidentally shooting yourself in the foot) is great, but avoiding "abuse" just reads as imposing your taste onto all users of your language. I don't like this, so nobody should be able to do it. But oh well, if you're using Zig (or any other language using auteur-driven development like Odin or Jai or C3) you've already signed up for only getting the features that the benevolent dictator thinks are useful. You take the good (tightly designed with no feature bloat) and the bad ("I consider this decision unlikely to be reversed").
- int_19h 1y agoThis is like the inverse of the normal C++ vtable. Normally the whole point of having a separate vtable (as opposed to just a bunch of function pointers directly in the struct) is so that you can share a single one between all instances, at the cost of one extra indirection. But here a copy of the vtable is instead attached to one individual instance, and yet it still does this one extra indirection to get to the implementation object.
- ethan_smith 1y agoThe extra indirection is a reasonable tradeoff in Zig because it enables heterogeneous collections of implementations while maintaining type safety without requiring inheritance.
- ww520 1y agoYes. You're right. It's not the typical C++ vtable. I wrote a second blog for the C++ style vtable. https://williamw520.github.io/2025/07/17/memory-efficient-zig-interface.html https://williamw520.github.io/2025/07/17/memory-efficient-zi... The first version is faster (one pointer indirection) with more memory consumed. The second version is more memory efficient sharing a single vtable for all instances, a bit slower (two pointer indirections).
- andsoitis 1y ago> That doesn’t mean polymorphism is off the table. In fact Zig has the tools to build interface-like behavior, making dynamic dispatch possible. Do all code bases use the same pattern? Or does it not matter for interoperability, e.g. between an app and the libraries it consumes?
- leagreeer 1y ago> Do all code bases use the same pattern? Unfortunately no - it's a complete free for all - which is the problem. Conventions are great to a point but language enforced constructs are better.
- justinhj 1y agoIsn't it a common refrain that a language one hasn't used much would be perfect if only had this one feature my favourite language has
- billmcneale 1y agoWhy is Zig so inconsistent in its syntax? We see: pub fn implBy Camel case (impl_obj: anytype) Snake case v_setLevel Mix of camel case and snake case anyopaque whatever that is
- ww520 1y agoFunction name in camel case. Variable and parameter in snake case.