There’s a concept I’ve given the moniker “build time” interfaces that I use quite a bit in embedded development in C/C++. A “build time” interface is:
An interface where only 1 “implementation” is ever used throughout the entirety of the program
It is primarily used for providing implementations for different architectures that the program can run on
For example, a GPIO driver that when compiled for the target chip, does what you would imagine (turn pins on and off). But, when compiled for an x86 system, just prints out “SIMULATION: GPIO ON” to terminal, or something of the like.
In C/C++ land I’ve generally implemented this with the pattern:
hardware.h:
void someFunction();
hardware_mcu.c:
void someFunction() {
// Do the actual thing
}
hardware_linux.c:
void someFunction() {
printf("SIMULATION");
}
And used the build system to compile the correct file for the given compilation target. I have a more complicated version that uses C++20 concepts and templates, but I’ll spare you. A nice blog on this subject:
Where config is a build option I’ve set in build.zig. This more or less captures the intent of a “build time” interface.
My question is, what have y’all come up with that fits this pattern? For instance, in this example I would love some way to keep from having to repeat the init() function for both variants of hardware. Bonus points if I can get functionality similar to C++20’s concepts/constraints, where nice error messages are printed should you try to implement an interface incorrectly.
So far I’ve seen:
Tagged Union style interfaces
Vtable/dynamic dispatch style interfaces
But both of these seem like overkill given there will only ever be a single concrete implementation of my interface for any given compilation target.
Look forward to seeing what y’all have come up with!
For more complicated examples, though, the strategy in the OP can also be found in the Zig standard library:
And for instances where you want to omit/add some fields based on a comptime value, another alternative is to condtionally make a field’s type void to make it take up zero bits:
I’m not quite sure I follow… Do you have an example of the “capability parameter”? I couldn’t find it in those release notes. Were you potentially referring to the bit about OS layers?
pub fn hwSpecificFunc(self: Hardware) u8 {
if (config.arch1) {
return self.something + 1;
} else {
// if config.arch1 is true (and comptime known),
// this code won't even be analyzed
return self.something + 2;
}
}
However it loses it’s ability to scale pretty quickly once you start adding a bunch of methods/architectures/more complicated functions. It’s like a “portable” C header with a thousand #ifdef ARCH1 statements sprinkled throughout. Ideally I want to limit my if(config.arch1) ... block to a single location per interface, if that makes any sense at all.
For more complicated examples, though, the strategy in the OP can also be found in the Zig standard library:
Cool, yeah I had seen this pattern before so I presume I’m barking up the right tree.
And for instances where you want to omit/add some fields based on a comptime value, another alternative is to condtionally make a field’s type void to make it take up zero bits:
This is super cool! Did not know about this trick so appreciate that
That 's, instead of relying on a global ambient API for IO, any function which wants to do IO would have to take IO as a parametr. This naturally makes IO overridable.
This is the similar pattern that Zig uses for allocators: rather than relying on global malloc&free symbols, allocating functions take an allocator as a parameter.
Not saying that this is definitely the answer here, just noticing that passing stuff explicitly seems to shape up as distinctively Zig pattern (cf. also how most std.fs hangs off an std.fs.Dir object)
EDIT: to clarify, this is not strictly speaking “capabilities”, as those, in the technical terms, need to come with some sort of security guarantee that there isnt any other way to do IO besides the explicitly passed paramer, and capability people tend to be so very picky about their definitions. But the code shape is similar — if you want to do $thing, you need to have a $thing-doer parameter.
Ahh got it, thank you for clarifying! This actually relates to the “real” goal of these “build time interfaces” which is to make hardware agnostic testing possible. To extend my toy example imagine a function:
pub fn somethingComplicated(THardware: type, hw: THardware) usize {
// Something annoying and complicated, trust me!
// Logic dependent "hardware" call
_ = hw.hwSpecificFunc();
// Even more complication!
return 42;
}
Without dependency injection allowed by the generic typed variable, the only way to test this would be to have it actually running on my board, doing god knows what (turning on pumps, motors, etc.).
At the end of the day I would love a way to bolster pure ducktyping in generics similar to C++20’s concepts, but understand there’s a decent chance it never makes it into the language. And I know I can enforce my interface with comptime functions and helpful compile errors, it just takes a LOT of boilerplate.
When to use which strategy likely just comes down to personal preference/the particular use case. For what it’s worth, here’s an example of a function that has many different platform-specific implementations using a switch:
Completely agreed. I’ll try a couple different paths and see what feels best For context, the “real” use case for this is essentially swapping out every peripheral driver that requires actual hardware interaction (setting some memory mapped IO to control a GPIO, ADC, etc.) with a “simulated” equivalent so that tests can be run on x86 for a binary intended to be cross-compiled and run on a freestanding ARM MCU.