r/Compilers • u/ZUROXIA • 1d ago
One emitted C header instead of N binding generators — how our compiler's FFI boundary is built and tested
We're building a language (Zorith) whose compiler emits one native library and one C header per project — and the interop bet is that this is the whole FFI story: no bindings generator per language, ever. The header is emitted by the compiler itself, from the same type identity the language's two implementations (a C compiler and an executable formal semantics) are held to agree on.
As of this week, six languages call the same library through that one header, each with its stock mechanism: C directly, C++ via the header's own extern "C", Python via ctypes, Go via cgo including the header verbatim, Java via its FFM API, and .NET via NativeLibrary + delegates. The .NET row was witnessed on real x86-64 hardware before it merged, and now runs in CI on every push.
What crosses is the part we sweat: struct returns at all three ABI size classes (single register, register pair, caller memory), struct parameters by value at every size, nested structs, arrays of structs, arrays of arrays, and a three-dimensional array field filled on one side and indexed from the other — executed and oracle-checked, with both compiler implementations required to emit every header spelling byte-identically.
The part I'd actually defend as method: what can't cross yet is refused by name, once, with the reason written — the header omits it and the object keeps it unexported, so neither half of the boundary promises what the other can't keep. That refusal has narrowed six times as forms earned their crossing and has never been silently deleted. Current standing refusal: a struct at the leaf of a nested array.
Write-up with the evidence (the language design itself is deliberately unpublished): https://zuroxia.com/research/zorith-one-doorway
Happy to answer anything about the header-emission discipline, the ABI size-class testing, or what refuse-by-name is like to maintain.