Skip to content

fix(ffi): regenerate the drifted cbindgen header and gate it in CI - #277

Open
YuanYuYuan wants to merge 2 commits into
mainfrom
ci/ffi-header-fresh
Open

fix(ffi): regenerate the drifted cbindgen header and gate it in CI#277
YuanYuYuan wants to merge 2 commits into
mainfrom
ci/ffi-header-fresh

Conversation

@YuanYuYuan

@YuanYuYuan YuanYuYuan commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

crates/hiroz-go/hiroz/hiroz_ffi.h is generated by cbindgen from crates/hiroz/src/ffi/ and committed, because cgo needs it at build time. Nothing in CI regenerated it, so it drifted. The drift was not confined to documentation.

This PR regenerates the header and adds a CI gate that fails when it drifts again.

The defect

The committed header was missing the namespace_ field that CContextConfig has carried since it was added (crates/hiroz/src/ffi/context.rs:36-38; cbindgen appends the trailing underscore because namespace is a C++ keyword):

 typedef struct hiroz_context_config_t {
   ...
   bool enable_logging;
+  const char *namespace_;
 } hiroz_context_config_t;
step consequence
crates/hiroz-go/hiroz/context.go:133 declares var cfg C.hiroz_context_config_t cgo sizes it from the committed header: 80 bytes
the field list in crates/hiroz/src/ffi/context.rs Rust's CContextConfig is 88 bytes; namespace sits at offset 80
crates/hiroz/src/ffi/context.rs:84 reads cfg.namespace reads offset 80-87, past the end of the Go allocation
the value read is non-null garbage cstr_to_str dereferences it as a C string

Reached by any Go caller that sets an advanced-config option, which is what selects the hiroz_context_create_with_config path in context.go.

The 80/88 figures are from compiling a sizeof probe against each version of the header. They also follow from the x86-64 layout: enable_logging ends at offset 73, tail padding to 80, and the appended pointer occupies 80-87.

What this PR does

change file
Regenerate the header crates/hiroz-go/hiroz/hiroz_ffi.h (52 insertions, 4 deletions)
Add rust-cbindgen to commonBuildInputs flake.nix
Add a check-ffi-header command and wire it into the pipeline scripts/test-pure-rust.nu
Add an ffi-header-fresh job, mirroring python-stubs-fresh .github/workflows/ci.yml

The regenerated header adds, beyond namespace_: the KEEP_ALL_CACHE_DEPTH constant, DEPTH_RECURSIVE, ten parameter-type constants (NOT_SET through STRING_ARRAY), and corrected doc text for DEFAULT_HISTORY_DEPTH and hiroz_service_client_wait_for_service. None of these had ever reached the header.

rust-cbindgen was in no dev shell. That is the root cause: CI could not have regenerated the header even if something had asked it to. It is placed beside go, since that header is what cgo compiles against.

Two details in the check are load-bearing rather than incidental:

  • The header is deleted before regenerating. A build that generates nothing would otherwise leave the committed copy in place and pass — which is the failure mode being fixed, not a new one. With the file absent, that outcome surfaces as a D entry.
  • cbindgen's presence is asserted explicitly. crates/hiroz/build.rs:55-60 degrades a missing cbindgen to a non-fatal cargo:warning=. Without the assertion the build would succeed, write nothing, and the check would report on whatever was already in the tree.

The check also touches crates/hiroz/build.rs, so a warm target directory cannot turn it into a passing no-op.

It runs as its own Nix-based job because go-tests has no Nix environment, and cbindgen must come from the pinned dev shell. cbindgen 0.29.4 emits a CDR_HEADER_LE constant that 0.29.3 does not, so an unpinned cbindgen would itself read as drift. The version used is printed in the log, so that case is diagnosable rather than mysterious.

Evidence

The header on main is stale. Running the new check against it fails:

$ nu scripts/test-pure-rust.nu check-ffi-header
cbindgen: cbindgen 0.29.3
 M crates/hiroz-go/hiroz/hiroz_ffi.h
[... 52-insertion / 4-deletion diff, including the namespace_ field ...]
Test failed: check-ffi-header
generated FFI header is stale -- run `cargo build -p hiroz --features ffi` and commit crates/hiroz-go/hiroz/hiroz_ffi.h
rc=1

Three inputs, so the detector is known to detect rather than merely never to fire:

input result
stale header (main as-is) fail — names the drift and prints the diff
regenerated header (this PR, e20f37a9) pass
cbindgen absent from PATH fail — "would pass without verifying anything"; the header is not left deleted, because the assertion precedes the rm

Breaking changes

what changes who is affected before → after action
sizeof(hiroz_context_config_t) C and Go consumers of the FFI header 80 → 88 bytes rebuild against the new header

Not affected: the Rust API, and the offsets of every pre-existing member — the field is appended, so source compiled against the new header is source-compatible.

⚠️ Appending does not preserve ABI. A caller still linked against the 80-byte definition passes an 80-byte allocation to a library that reads namespace_ at offset 80 — the out-of-bounds read described above. The doc comment inherited from the Rust source says "to preserve ABI compatibility"; that is too strong. The accurate statement is offset compatibility plus a mandatory rebuild. The comment is left as-is in this PR.

cgo rebuilds automatically for the in-tree Go bindings. Any out-of-tree C consumer holding a copy of the old header must regenerate it.

Coverage this does not have

  • The FFI module is still not linted. FFI module is never compiled, linted or tested; the "Go FFI tests" CI step silently skips #270 item 3 (--features ffi under -D warnings) is untouched, as are item 1 (the 22 missing_safety_doc contracts), item 2 (the serialize.rs:81 cast) and item 5 (the RawPublisher::publish_bytes regression test). This PR addresses only the two header-drift follow-ups recorded in FFI module is never compiled, linted or tested; the "Go FFI tests" CI step silently skips #270's second comment, and it does so with a dedicated Nix job rather than by installing cbindgen into go-tests.
  • Only this one generated artifact is gated. Any other generated-and-committed file remains unchecked, apart from the Python stubs already covered by check-python-stubs.
  • The Go bindings expose no namespace setter, so cfg.namespace_ stays null from in-tree Go callers. What this PR fixes is the allocation size, not missing functionality.
  • On a failed build the header is left deleted in the working tree. git checkout -- crates/hiroz-go/hiroz/hiroz_ffi.h restores it. This is stated in a comment in the check rather than handled automatically.

Relationship to #273

Both PRs edit .github/workflows/ci.yml. They touch different jobs: #273 changes go-tests and scripts/test-go.nu; this PR adds a new ffi-header-fresh job and does not modify go-tests. Neither depends on the other.

Notes

flake.nix has two extraShellHook = '''' occurrences (lines 396 and 511) that a nixfmt --check run flags. Both are pre-existing on main and outside this diff; left alone.

The committed header lacked the namespace_ field that CContextConfig has
carried since it was added. cgo sizes its struct from this file, so Go
allocated 80 bytes where the Rust side reads 88 and dereferenced
cfg.namespace past the end of the allocation on every advanced-config
ContextBuilder.Build().

Regenerated with the cbindgen the flake pins (0.29.3). Also restores 11
parameter-type constants, KEEP_ALL_CACHE_DEPTH, and corrected doc text.

Refs #270
Nothing regenerated the header in CI, so its drift from the Rust structs
was invisible. cbindgen was absent from every dev shell, and build.rs
degrades its absence to a non-fatal cargo:warning=, so enabling the ffi
feature alone would not have caught this.

Adds rust-cbindgen to commonBuildInputs and a check-ffi-header check that
deletes the header, regenerates it, and fails on any diff -- mirroring
check-python-stubs, which gates the generated Python stubs the same way.
The deletion is what makes a build that generates nothing fail instead of
reporting on the committed copy; cbindgen's absence is checked explicitly
so that path cannot pass vacuously either.

Runs as its own nix-based job because go-tests has no nix environment and
cbindgen must come from the pinned dev shell -- 0.29.4 emits a constant
0.29.3 does not, so an unpinned version would itself read as drift.

Closes item 3 of #270

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Regenerates the committed FFI header and adds CI drift detection.

Changes:

  • Updates the C header with the missing context namespace field and constants.
  • Adds cbindgen and a header freshness script.
  • Adds a dedicated CI freshness job.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
scripts/test-pure-rust.nu Adds the regeneration check.
flake.nix Adds cbindgen to build inputs.
crates/hiroz-go/hiroz/hiroz_ffi.h Regenerates the C FFI surface.
.github/workflows/ci.yml Runs the freshness check in CI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/hiroz-go/hiroz/hiroz_ffi.h
Comment thread flake.nix
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.

2 participants