Speaking as someone familiar with C and Rust (not so much Go!), although there's a parallel here to Rust's Traits, this actually is much closer to dyn Trait in Rust, which uses vtables and runtime polymorphism, rather than "regular" Traits in Rust, which are monomorphized versions of similar interface constraints, much closer to C++'s templates (or concepts, I'm hand waving here).
This isn't necessarily a negative, sometimes you actually prefer vtables and runtime polymorphism for various reasons like flexibility, or code size reasons. Just wanted to add some flavor for folks that aren't as familiar with Rust, that this isn't exactly how things usually work, as "regular" Trait usage is much more common than dyn Trait usage, which you have to explicitly opt-in to.
I think it's best to avoid this kind of "re-inventing OOP in C" thing, even though it can be tempting when coming from other languages. Regardless, some notes:
- It's UB to alias one struct pointer with that of a different struct type, even if the two structs have the same first few members. Clang and GCC both exploit this for optimizations, although you can configure them not to.
- Casting function pointers is also problematic, although I think that one is more of a portability issue.
- If you want to "downcast" from a "base" struct to an "inheriting" struct, you can use the `container_of` macro, which is robust against member re-arrangement and supports multiple inheritance:
- Interfaces in other languages exist to add type safety to dynamic dispatch. You get none of that in C, though, due to the casting you have to perform regardless. Code which just "does the obvious thing" using void pointers will be much simpler, making it better IMO despite the lack of type "safety":
I've wound up just putting the protocol state in a struct and making the "conforming" action to have that struct in the conforming object with a standardized field name. Then just use a macro to get the protocol pointer and pass it to the protocol's implementation functions.
But I really, really wish we could have a lightweight protocol/trait feature in C. It would remove a large source of unsafe code that has to cast back and forth between void *.
This isn't necessarily a negative, sometimes you actually prefer vtables and runtime polymorphism for various reasons like flexibility, or code size reasons. Just wanted to add some flavor for folks that aren't as familiar with Rust, that this isn't exactly how things usually work, as "regular" Trait usage is much more common than dyn Trait usage, which you have to explicitly opt-in to.