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:
- 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.
- Run on
native/include/cinder_native.h(which includeswebgpu.handwgpu.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 barepassbodies. - 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); onestruct <Lib>that owns theOwnedDLHandleand holds every resolved function pointer as a field, so pointers cannot outlive the library (see the ASAP-destruction note indocs/MOJO_NOTES.md); every C pointer asOptional[Pointer[T, MutUntrackedOrigin]](8 bytes, verified); nullable callback fields asOptional[Pointer[NoneType, ...]]becauseOptional[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: printssizeof,alignof,offsetofand every enum andstatic constflag value with the platform's C compiler; CMake runs it intonative/build/<type>/layout/<lib>.txt.tests/ffi/test_layout_<lib>.mojo: a TestSuite that reads that file and asserts the same numbers fromsize_of,align_of,reflect[T].field_offsetand 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/gfxand 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/.