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

cbindgen generates crates/hiroz-go/hiroz/hiroz_ffi.h from crates/hiroz/src/ffi/. hiroz commits the header, because cgo needs it at build time. No CI job regenerated it, so it drifted. The drift was not confined to documentation.

This PR regenerates the header. It also adds a CI gate that fails on the next drift.

Refs #270 — the two header-drift follow-ups in that issue's second comment.

The defect

The committed header was missing the namespace_ field that CContextConfig has carried since that field landed. 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
var cfg C.hiroz_context_config_t in context.go cgo sizes the struct from the committed header: 80 bytes
the field list of CContextConfig in ffi/context.rs Rust's struct is 88 bytes; namespace sits at offset 80
the read of cfg.namespace reads offsets 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

Any Go caller that sets an advanced-config option reaches this path. That option is what selects hiroz_context_create_with_config in context.go.

The 80/88 figures come from a sizeof probe compiled against each version of the header. The x86-64 layout gives the same answer: enable_logging occupies offset 72, tail padding runs to 80, 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, wired into the pipeline scripts/test-pure-rust.nu
Add an ffi-header-fresh job, mirroring python-stubs-fresh .github/workflows/ci.yml

Beyond namespace_, the regenerated header adds 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 regenerate the header, even if a job had asked it to.

Four details of the check are load-bearing rather than incidental:

detail why
the check deletes the header before building otherwise a build that generates nothing leaves the committed copy in place and passes — the exact failure mode being fixed. With the file absent, that outcome surfaces as a D entry.
the check asserts cbindgen is present build.rs degrades a missing cbindgen to a non-fatal cargo:warning=. Without the assertion the build succeeds, writes nothing, and the check reports on whatever the tree already held.
the check touches crates/hiroz/build.rs a warm target directory cannot turn the check into a passing no-op
it runs as its own Nix-based job 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 reads as drift. The check prints the version it used.

Evidence

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

$ 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 exercise the check. It detects the defect; it does not merely never fire.

input result
stale header (main as-is) fail — names the drift, prints the diff
regenerated header (this PR, e20f37a9610510ba8f870d853264dfd48bc8e382) 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

tag what changes who is affected before → after action
BC1 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. cbindgen appends the field at the end, so source compiled against the new header stays source-compatible.

Warning

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. This PR leaves the comment as-is.

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

None of these block the fix. They bound what green CI proves.

tag gap
G1 The FFI module is still not linted. #270 item 3 (--features ffi under -D warnings) is untouched, as are item 1 (the 22 missing_safety_doc contracts), item 2 (the cast_slice_from_raw_parts cast in serialize.rs) and item 5 (the RawPublisher::publish_bytes regression test). Item 4 is #273's.
G2 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.
G3 The Go bindings expose no namespace setter, so cfg.namespace_ stays null from in-tree Go callers. This PR fixes the allocation size, not missing functionality.
G4 On a failed build the header is left deleted in the working tree. git checkout -- crates/hiroz-go/hiroz/hiroz_ffi.h restores it. A comment in the check states this; nothing handles it automatically.

Notes

#273 also edits .github/workflows/ci.yml. It changes go-tests and scripts/test-go.nu; this PR adds a new job and leaves go-tests alone, so neither depends on the other.

flake.nix has two extraShellHook = '''' occurrences (one, the other) that nixfmt --check flags. Both are pre-existing on main and outside this diff. Left alone.

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
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
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