cmux-app-ffi: generate cmux_motion_ffi.h with cbindgen, fail on drift
include/cmux_motion_ffi.h, the header the Mac app imports as CCmuxMotionFFI, is now generated by cbindgen from the Rust C ABI. A test fails, and prints the diff, when the committed header no longer matches what cbindgen generates.
Until now the header was hand-written next to src/motion.rs and src/hover.rs. If it disagreed with a Rust extern "C" signature or a #[repr(C)] layout, the result was undefined behavior that no Swift test catches. This comes from an ecosystem sweep that looked for mature outside tools to borrow where it's cheap, not to rewrite working code ("We always want to consider the option, consider the alternative."). The hot C-ABI paths stay on C, and iOS keeps UniFFI.
What changed
cmux-tui/crates/cmux-app-ffi/cbindgen.tomlgenerates the header fromsrc/motion.rs,src/hover.rsand cmux-motion'shover::cstructs. The Rust doc comments become the C comments.tests/c_header.rsregenerates the header and fails with a unified diff when the committed file differs.CMUX_APP_FFI_HEADER_BLESS=1 cargo test --test c_headerrewrites it.rust-workspaces.ymlalready runscargo test --lockedon this workspace whenever the crate or a path dependency (such as cmux-motion) changes, so this adds no CI job.- cbindgen is a dev-dependency pinned at
=0.29.2in the committedCargo.lock. There is nobuild.rs, so a build never rewrites a tracked file. The staticlib's own dependency versions are unchanged; the lock diff only adds cbindgen's tree and disambiguatessyn. - The regenerated header is committed. No FFI signature or
ABI_VERSIONchanged. HoverRect::ZERO/::NULLin cmux-motion getcbindgen:ignore(doc comments only). Without it, cbindgen copies them asCmuxHoverRect_*macros, andNULL'sf64_INFINITYis not valid C. The test also fails on any macro other than the include guard, since this ABI has no constants.
Same ABI
- The old and new headers declare the same 45 types and functions, compared after preprocessing and ignoring order. The only differences are ordering, comments, line wrapping and
const double from[4]becomingconst double *from(the same parameter type in C). build-app-ffi.shbuilds its exported-symbols list by greppingcmux_*(from this header. That grep gives the same 29 symbols for both headers.- The header path is unchanged:
build-app-ffi.sh(headers_in) still copiescmux-app-ffi/include/cmux_motion_ffi.hintoCCmuxMotionFFI.
Other FFI headers (not in this PR)
I tried the same recipe on the other three crates. None reproduce cleanly yet, so each would be its own follow-up:
cmux-rd-ffi: cbindgen emits anonymous typedef structs where the hand-written header has tagged ones, and stops on two conflictingNONEconstants.cmux-layout-reducer-ffi: its opaque handle types (Focus,Scroll,Mirror,Faces) would need renames in the config.cmux-terminal-client:Option<extern fn>callback typedefs need explicit config.
Testing
Observed
cargo test --locked --test c_header: passes on the head.- Changing
cmux_motion_ease'st: f64tof32insrc/motion.rsmade the test fail with the one-line-double ⦠t/+float ⦠tdiff. After reverting, it passes again. - Removing the two
cbindgen:ignorelines fails the test onunexpected macros in the header. Restoring them passes. - The header compiles with
clang -fsyntax-only -std=c11 -Wall -Wextra. cargo clippy --locked --all-targets -- -D warningsandcargo fmt --checkincmux-app-ffiare clean.prepush-gate.pyreports no new violations.
Not run
- No app build. Since the header's declarations are unchanged, the CCmuxAppFFI pin keeps its current release. The usual automated re-pin follows, because this changes files under
cmux-app-ffi.
Changelog
none
š¤ Generated with Claude Code
Agent session: agent session