I also make use of interfaces in C quite a bit, and I recently tried to compile one of my projects to
WASM and learned (the hard way) that function casting is
Undefined behaviour according to the C standard
Straight up crashes on WASM because emscripten enforces strict type equality on function calls.
This means that something like.
int do_thing(int *i) { return *i };
auto f = ((*int)(void *))do_thing;
int a = 0;
f(&a); // crashes on WASM
This made me change how I define interfaces, where I now always use void* for the self parameter.
Also, when using interfaces, LTO is pretty much required to get the compiler to inline as much as possible.
Your example code is not C.
In olden times auto f ... would have got you a block local int.
Today, clang will give you an error:
❯ cc -o temp temp.c
temp.c:6:7: error: type specifier missing, defaults to 'int'; ISO C99 and later do not support implicit int [-Wimplicit-int]
6 | auto i = f;
| ~~~~ ^
| int
1 error generated.
Fair enough -- I live mainly in embedded land and C23 is still in the future (because reasons). Thanks for the reminder to start my periodic survey of the landscape.
I feel you. I work with some slightly higher level embedded (we at least have a Linux kernel beneath us—with some exceptions), and am slowly trying to get us to a more modern C++ compiler but ABI issues are a nightmare.
I've been trying to use Zig for smaller projects at work when I can. Worth looking into if you're in that space, although still somewhat unstable at the moment.
I recently implemented an approximation of interfaces/traits in C++20 using concepts and evil amounts of the pre-processor. It implements dynamic interfaces/traits in terms fat pointers, custom vtables rather than traditional C++ virtual methods backed by compiler-generated vtables, and a simplistic custom RTTI implementation. There are some different trade-offs, and less flexibility than Rust of course, but you can do things like: guarantee dynamic dispatch (using the fat pointer type), or generically handle either of dynamic or static (by using concept auto &&).
Overall I am quite happy with the experiment. It allows me to extend closed APIs as well as fundamental types. I can declare implementations in a variety of ways (methods, friend functions, plain old functions), and I have implemented a mechanism to sort-of-but-not-quite ensure const-correctness, where the vtable tracks if all methods can operate on const or non-const, and then casts from a type-erased fat pointer back to an implementation pointer type will check, based on the destination type qualification, whether or not const is required or not.
The implementation is mostly here and an example of a simple type-parameterized interface is here, and a more elaborate non-parameterized interface is here. The implementation of casting with const-checking is here.
This may seem unimportant for I/O, but the low overhead makes the traits attractive also for operating on in-memory data. All kinds of parsers, template engines, and compression libraries are written directly for the I/O traits. Inlining is even more important for iterator traits.
lor_louis | a day ago
I also make use of interfaces in C quite a bit, and I recently tried to compile one of my projects to WASM and learned (the hard way) that function casting is
This means that something like.
This made me change how I define interfaces, where I now always use
void*for the self parameter.Also, when using interfaces, LTO is pretty much required to get the compiler to inline as much as possible.
ibookstein | 16 hours ago
The casting itself is not UB, but calling through the wrong function pointer type definitely is.
georgn | 7 hours ago
Your example code is not C. In olden times
auto f ...would have got you a block local int. Today, clang will give you an error:invlpg | 7 hours ago
Your information is a bit out of date. Support for type inference with the
autokeyword was added in C23.georgn | 4 hours ago
Fair enough -- I live mainly in embedded land and C23 is still in the future (because reasons). Thanks for the reminder to start my periodic survey of the landscape.
invlpg | 3 hours ago
I feel you. I work with some slightly higher level embedded (we at least have a Linux kernel beneath us—with some exceptions), and am slowly trying to get us to a more modern C++ compiler but ABI issues are a nightmare.
I've been trying to use Zig for smaller projects at work when I can. Worth looking into if you're in that space, although still somewhat unstable at the moment.
pag | a day ago
I recently implemented an approximation of interfaces/traits in C++20 using concepts and evil amounts of the pre-processor. It implements dynamic interfaces/traits in terms fat pointers, custom vtables rather than traditional C++ virtual methods backed by compiler-generated vtables, and a simplistic custom RTTI implementation. There are some different trade-offs, and less flexibility than Rust of course, but you can do things like: guarantee dynamic dispatch (using the fat pointer type), or generically handle either of dynamic or static (by using
concept auto &&).Overall I am quite happy with the experiment. It allows me to extend closed APIs as well as fundamental types. I can declare implementations in a variety of ways (methods,
friendfunctions, plain old functions), and I have implemented a mechanism to sort-of-but-not-quite ensureconst-correctness, where the vtable tracks if all methods can operate onconstor non-const, and then casts from a type-erased fat pointer back to an implementation pointer type will check, based on the destination type qualification, whether or notconstis required or not.The implementation is mostly here and an example of a simple type-parameterized interface is here, and a more elaborate non-parameterized interface is here. The implementation of casting with
const-checking is here.kornel | a day ago
The static vtable initialization is a nice trick.
However, this is all dynamic dispatch. In the C version, the compiler is unable to inline the calls. OTOH the Rust example optimizes out everything down to
println!("total = {}", 16).This may seem unimportant for I/O, but the low overhead makes the traits attractive also for operating on in-memory data. All kinds of parsers, template engines, and compression libraries are written directly for the I/O traits. Inlining is even more important for iterator traits.
pekkavaa | 15 hours ago
The author suggests link-time optimization for that reason. Would be good to know how much it helps in practice.
Corbin | a day ago
Previously, on Lobsters, and previously, on Lobsters, we discussed the design and implementation of Cello, a 2015 dialect of C that provides macros and runtime helpers for building this sort of fat pointer.
dfawcus | a day ago
dfawcus | a day ago
telemachus | a day ago
This was a lot of fun, but I think Anton missed the obvious joke in the comments here.