FFI
The redefinition:
@cimportreads the C header directly, and all of the foreign lives underc.. The dot shouts “this is C” at every contact.
The idea
Section titled “The idea”Talking to C is mandatory for a systems language: half a century of ecosystem is in C. The traditional form is to write bindings by hand (redeclare each prototype, each struct, keep that in sync) or to generate bindings with an external tool. Either way, you reconstruct what C already exposes.
How Makoto redefines it
Section titled “How Makoto redefines it”@cimport("header.h") runs at comptime, invokes an embedded C compiler, translates the entire header and
returns a module under the root-namespace c. You redeclare nothing: @cimport("stdio.h") gives you
c.stdio.printf directly. The fundamental types (compiler/platform-dependent) come from an imported runtime:
c.gcc.size_t is size_t as GCC defines it.
Everything from C stays under c., and this is on purpose: the dot shouts “here is a C contract, not
Makoto” at every point of contact, and your namespace stays clean (whoever does no FFI never sees any C). Calling C is
unsafe (each call is a black box that can violate any invariant), tamed with
assert/assume or grouped in a block; and the return validates at the edge (a pointer that may be NULL
becomes Optional, bytes that become a string go through a Result).
At the boundary, memory stays colorless: @mm(c) (the manager delegates to C’s free),
@promote(gc) (repatriates to the GC), @pin/@unpin (pins a GC object while C holds it). And the
lower sibling: inline assembly via @asm.
The reasoning
Section titled “The reasoning”The bet is “the C ecosystem for free, if you accept the safety mark”. If C is inherently
unsafe, rewriting bindings by hand buys no safety; it only adds bureaucracy and a copy that
goes out of sync. Better to read the header directly (the source of truth already exists) and spend the effort where it
matters: at the boundary, marking the danger (unsafe, validation at the edge) and the memory crossing
(@mm(c)/@promote/@pin).
The c. namespace is non-poisoning applied to FFI (ch. 01): the foreign does not pollute the global,
and the mark is the name path itself. It is the same reason unsafe is a mark and not a silent
mode: danger should be visible, and c.malloc in the middle of your code is visible.
Why not the alternatives
Section titled “Why not the alternatives”- Hand-written bindings. They reconstruct what C already exposes, go out of sync when the header changes, and
do not buy safety (C stays unsafe).
@cimportreads the source of truth. - FFI without a dedicated namespace (
extern "C"in scope, à la Zig’s mixed@import("c")). Names collide (isopenyours or C’s?) and the boundary stays invisible. Thec.disambiguates and marks. - Automatic conversion C
intMakotoi32. It would hide the cost: on some targetsintis 32 bits, on others 64, and the bug stays invisible. C’s types come explicit (c.gcc.size_t); the crossing is written. - Async/memory coloring crossing the boundary automatically. C’s memory arrives as
@mm(c)and you decide what to do (@promote,@pin), explicitly, because crossing worlds is a decision, not a coercion.
The concrete pain
Section titled “The concrete pain”The project that keeps a 2000-line bindings file in manual sync with a header that changes
every release. The Zig where you pass an i32 to a C int parameter and it works on x86 but breaks
on another target, with no clue in the code. The NULL that comes back from a C function and becomes a segfault three
calls later because nobody handled it. Each one is the poorly-marked C boundary charging the price.
The mental model
Section titled “The mental model”Point at the
.h, and C works. All of the foreign lives underc.(which shouts “this is C”); the danger isunsafeand memory crosses the worlds explicitly.
The accepted friction
Section titled “The accepted friction”@cimport requires an embedded C compiler in the toolchain (real weight, but you pay once). Each call to
C carries the obligation of unsafe + validation at the edge, more ceremony than a simple call, on
purpose, because the danger is real. And the c. in every name is verbose when you use C heavily, but
it is the mark that keeps the boundary honest.
Next: 11 · Ecosystem and tiers