Interfaces and traits in C

25 points by repl a day ago on lobsters | 11 comments

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

  1. Undefined behaviour according to the C standard
  2. 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.

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:

❯ 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.

invlpg | 7 hours ago

Your information is a bit out of date. Support for type inference with the auto keyword 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, 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.

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

Comment removed by author

dfawcus | a day ago

Comment removed by author

telemachus | a day ago

This was a lot of fun, but I think Anton missed the obvious joke in the comments here.

int main(void) {
    int x = 42;
    uint8_t buf[8];
    Zeros_Read(&x, buf, sizeof(buf));  // Fuck around
}

size_t Zeros_Read(void* self, uint8_t* p, size_t len) {
    Zeros* z = (Zeros*)self;
    // ...
    z->total += len;                   // Find out
    return len;
}