DocsDecisions

ADR-0005: Generating the Mojo bindings

Status: Accepted, 2026-11-08 (decided by spike 9; supersedes the "Proposed" version of the same day)

Context #

The mission names mojo-bindgen (libclang based) as the bindings generator and requires the output to be reviewed and committed, with a C program and a generated Mojo test cross-checking every struct layout and enum value. It also forbids Mojo nightlies.

Evidence gathered in spike 9 (2026-11-08, pinned toolchain) #

mojo-bindgen 0.3.4 from PyPI (MIT) was installed in an isolated pixi environment (pixi run -e bindgen). Findings:

  1. It requires Python 3.14 and has no Mojo dependency at all, so the "generator pins a nightly" concern from the README's unreleased 0.4.0 notes does not apply to the released package.
  2. Run on native/include/cinder_native.h (which includes webgpu.h and wgpu.h) it produced 10,616 lines that do not parse under Mojo 1.2.0: a macro body (} )) is emitted as code, and the output targets nightly idioms that 1.2.0 rejects or has removed: InlineArray (removed in 1.1.0), UnsafePointer (372 uses), StringSlice (618 uses), ImmutOpaquePointer, private stdlib internals (_DLHandle, _Global, _find_dylib), 25 bare pass bodies.
  3. Its layout sidecar embeds libclang's numbers as constants computed on the generating machine, so it cannot detect a difference between the Linux and macOS C ABIs at test time.

Decision #

Cinder uses its own generator, tools/bindgen/cinder_bindgen.py (about 600 lines of Python over libclang, run from the isolated bindgen pixi environment), which emits from one parse:

  • cinder/ffi/<lib>.mojo: Mojo 1.2 idioms only (Pointer, Array, comptime, __deinit__-free plain-data structs); one struct <Lib> that owns the OwnedDLHandle and holds every resolved function pointer as a field, so pointers cannot outlive the library (see the ASAP-destruction note in docs/MOJO_NOTES.md); every C pointer as Optional[Pointer[T, MutUntrackedOrigin]] (8 bytes, verified); nullable callback fields as Optional[Pointer[NoneType, ...]] because Optional[def(...) thin abi("C")] is 16 bytes; unions, bitfield records and anonymous members as aligned byte blobs; variadic functions listed in a comment, never stubbed.
  • native/layout/<lib>_layout.c: prints sizeof, alignof, offsetof and every enum and static const flag value with the platform's C compiler; CMake runs it into native/build/<type>/layout/<lib>.txt.
  • tests/ffi/test_layout_<lib>.mojo: a TestSuite that reads that file and asserts the same numbers from size_of, align_of, reflect[T].field_offset and the generated comptime constants.

mojo-bindgen stays available in the bindgen environment for comparison, but nothing in the build depends on it.

Consequences #

  • A generator we control, small enough to review, that fails loudly on anything it cannot bind.
  • The layout test is platform-correct by construction: Linux and macOS each compile and run their own printer.
  • Constness of C pointers is not expressed in the raw layer; the safe layer (cinder/gfx and friends) owns it. This is deliberate: Optional[Pointer] has no implicit mutable-to-immutable conversion.
  • Deviation from the mission's named tool, recorded here with the evidence above.

Amendment (2026-11-08): compile-time cost #

The first generator output kept the symbol names in a comptime Array[StaticString, N]. With the umbrella header (4,463 functions) the Mojo 1.2.0 compiler exhausted 13 GB on any program that instantiated the library struct; a 2,200-entry array failed the same way. The names are now one newline-joined StringLiteral split at load time, after which the full module compiles in about 7 s with 0.3 GB peak. Reproducer and numbers: bugs/comptime_array_literal_oom/.

View as MarkdownSource: docs/adr/0005-bindings-generation.md