Skip to content

Generate a C header from Rust with rust_cbindgen - #54

Merged
jackmarsh merged 4 commits into
mainfrom
issue-25-cbindgen
Aug 24, 2026
Merged

Generate a C header from Rust with rust_cbindgen#54
jackmarsh merged 4 commits into
mainfrom
issue-25-cbindgen

Conversation

@jackmarsh

Copy link
Copy Markdown
Collaborator

Closes #25.

The gap

rust_bindgen makes Rust bindings from a C header. The reverse had no rule, so C calling into a staticlib hand-wrote the declarations:

int ffi_add(int a, int b);   // test/cc_interop/main.c, checked by nothing

The rule

The same shape as rust_bindgen, which is what #25 asked for: cbindgen is a published crate, declared by rust_repo and aliased through a CbindgenTool config knob, so the supply chain stays in the graph.

cbindgen parses the source rather than compiling it, so the rule needs no toolchain and no dependencies: a crate's extern "C" surface is decided by its own text. That makes this rule considerably simpler than rust_bindgen, which needs libclang.

Supports lang (c / c++ / cython, picking the extension), a cbindgen.toml, extra flags, and srcs for a crate whose root declares modules.

How you know it works

test/cc_interop generates its header now rather than carrying one, and main.c includes it instead of declaring ffi_add by hand. plz run //test/cc_interop:uses_rust still prints rust says 42.

Adding a parameter on the Rust side:

test/cc_interop/main.c:6:30: error: too few arguments to function 'ffi_add'

Before, that compiled and crashed at run time. That is the property #25 asked for.

A bug this surfaced, fixed in its own commit

lock --add cbindgen broke an unrelated test with a confusing TOML parse error. cbindgen wants toml 0.9; please_rust asks for toml 0.8. Both were declared correctly as separate buckets, but:

versions.sort_by(|a, b| b.0.cmp(&a.0));   // newest first
let new_name = if rank == 0 { norm.clone() } else { ... };

The bare label went to whatever was newest. So //third_party/crates:toml became 0.9.12, and please_rust, which names that label, silently moved a major version. Any first-party rule depending on a crate by its bare name is exposed to this the moment a new major arrives as an indirect dependency of something unrelated.

A root is what somebody declared and what rules depend on by name, so it now keeps the bare label. Newest still wins between two roots and when nothing is a root. Covered by a test.

192 tests pass, clippy clean, fmt clean.

Next: #26 wasm-bindgen, then both exercised in rust-corpus.

A crate declared at two versions gets one bare label and one versioned
one, and the bare label went to the newest. So a new major arriving as
an indirect dependency of something unrelated takes the label, and every
first-party rule naming //third_party/crates:<crate> moves a major
version with nobody saying so.

Adding cbindgen, which wants toml 0.9, did exactly that: please_rust
asks for toml 0.8, the bare toml label became 0.9.12, and the tool
linked against it. The symptom was one unrelated test failing to parse a
lockfile, pointing nowhere near the cause.

A root is what somebody declared and what rules depend on by name, so it
keeps the bare label. Newest still wins between two roots, and when
nothing is a root.
rust_bindgen makes Rust bindings from a C header. The reverse had no
rule, so C calling into a staticlib hand-wrote the declarations and
nothing checked them against the Rust definitions.

The same shape as rust_bindgen: cbindgen is a published crate, declared
by rust_repo and aliased through CbindgenTool, so the supply chain stays
in the graph. cbindgen parses the source rather than compiling it, so
the rule needs no toolchain and no dependencies.

test/cc_interop generates its header now rather than carrying one, and
main.c includes it instead of declaring ffi_add by hand. Adding a
parameter on the Rust side turns into

    main.c:6:30: error: too few arguments to function 'ffi_add'

where before it compiled and crashed at run time.
The table claimed rules_rust supported Rust to C headers. It does not.
Its extensions are bindgen, mdbook, prost, pyo3 and wasm_bindgen, and a
code search over the repository returns no hits for cbindgen at all,
against 23 for rust_bindgen and 41 for wasm_bindgen, so the search works
and the absence is real.

Cargo's row is right as it stands: cbindgen is called from build.rs as a
library, generated by the crate being built rather than as an artifact
another rule can depend on.
@jackmarsh
jackmarsh merged commit cfad011 into main Aug 24, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cbindgen rule: Rust to C headers

1 participant