Skip to content

Run the generated C bindings, and settle what the call axis could not (#198, step 2c) - #409

Merged
milyin merged 2 commits into
shape-coveragefrom
shape-matrix-runtime
Aug 13, 2026
Merged

Run the generated C bindings, and settle what the call axis could not (#198, step 2c)#409
milyin merged 2 commits into
shape-coveragefrom
shape-matrix-runtime

Conversation

@milyin

@milyin milyin commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Step 2c of #198 — the runtime, C side — on shape-coverage (#402).

The questions the call axis had to defer

Step 4 added call shapes but made no claim that the alias guard works, because that is a claim about running code and I would not assert it against emitted text. Three questions were left standing:

  • does the guard fire on a call naming one resource twice?
  • does it spare a call that does not?
  • does it run before ownership moves?

All three now hold, and they are answered by running the binding — with no toolchain at all. The C target's extern "C" wrappers are ordinary Rust functions, so calling one with the pointers a C caller would pass is a cargo test. No C compiler, no linker, no CI change.

The third is the one worth stating precisely: the aliased call is rejected and the resource is reclaimed afterwards — which would be a double free if either converter had run. That is what makes it a statement about ordering rather than about failing.

The cases are a discrimination set, not a wish list: a guard that fired on everything would pass the first and fail the second; one that fired on nothing would do the reverse. Neither can pass both.

Four things it took to get there, each a finding

  • A ()-returning C binding cannot report an alias rejection — it aborts. A panic cannot cross extern "C", so under .panic() the guard is fatal by construction and unobservable in-process. The cases use fallible call shapes, whose rejection routes to the error out-param the way a real C API reports anything. Both spellings are now in the corpus, so the difference is visible rather than rediscovered.
  • A fallible fixture must actually return — a body that panicked would abort on the accepted path, for the same reason.
  • The C error channel requires From<String> on the declared error type. The binding's own messages — an alias rejection, a null handle — are converted through it. A requirement of the channel, not of any one shape.
  • The rejection's explanation reaches the caller intact. The first case asserts on it: the aliasing message survives conversion into the error type and back out through the accessor. An earlier fixture discarded it and the caller received error 1 — which is exactly the kind of thing only running the code finds.

Mechanics

Each case is an integration test, so cargo compiles it as its own crate. Not a style choice: every case exports the same #[no_mangle] symbols (probe, handle_drop), and one crate holding two of them does not link. The compile stage never hit this because cargo check does not generate code.

The receipt is the test harness's own per-test result line. A case that fails to compile or never runs is not run — never a pass — and nothing maps a case to "presumed fine".

Exit: answers move, additively

4 new cells (the two fallible call shapes); no pre-existing cell changed.

What is left

Kotlin/JNI has no equivalent shortcut: its wrappers are entered from a JVM. The branch map now splits that out as 2d — the Kotlin compiler and the JVM runtime — and it is the first step in this sequence that is not self-contained.

Checks

cargo test -p shape-matrix (18 tests), clippy -D warnings across all targets and features, cargo fmt --check with CI's config, RUSTDOCFLAGS=-D warnings cargo doc, examples/regen-check.sh clean.

milyin added 2 commits August 14, 2026 01:43
…#198, step 2c for C)

Every stage so far asked whether something could be *produced* — a plan, Rust
that compiles, a header that declares. The call axis raised three questions none
of them can answer: does the alias guard **fire** on a call naming one resource
twice, does it **spare** a call that does not, and does it run *before* ownership
moves. Those are claims about running code.

The C target needs no toolchain to answer them. Its `extern "C"` wrappers are
ordinary Rust functions, so calling one with the pointers a C caller would pass
is a `cargo test` — no C compiler, no linker, no CI change. All three hold.

The third is the one worth stating precisely: the aliased call is rejected **and
the resource is reclaimed afterwards**, which would be a double free if either
converter had run. That is what makes it a statement about ordering rather than
about failing.

Cases are a discrimination set, not a wish list. A guard that fired on
everything would pass the first and fail the second; one that fired on nothing
would do the reverse. Neither can pass both.

Four things this needed, each of which is a finding in its own right:

* **A `()`-returning C binding cannot report an alias rejection — it aborts.**
  A panic cannot cross `extern "C"`, so under `.panic()` the guard is fatal by
  construction and unobservable in-process. The cases therefore use fallible
  call shapes, whose rejection routes to the error out-param the way a real C
  API reports anything. Both spellings are now in the corpus, so the difference
  is visible rather than discovered again.
* **A fallible fixture must actually return.** A body that panicked would abort
  on the *accepted* path for the same reason.
* **The C error channel requires `From<String>` on the declared error type** —
  the binding's own messages (an alias rejection, a null handle) are converted
  through it. A requirement of the channel, not of any one shape.
* **The rejection's explanation reaches the caller intact**, which the first
  case now asserts on: the aliasing message survives conversion into the error
  type and back out through the accessor. An earlier fixture discarded it and
  the caller received `error 1`.

Each case is an **integration test**, so cargo compiles it as its own crate.
Not a style choice: every case exports the same `#[no_mangle]` symbols, and one
crate holding two of them does not link. The compile stage never hit this,
because `cargo check` does not generate code.

The receipt is the test harness's own per-test result line. A case that fails to
compile or never runs is `not run`, never a pass, and no table maps a case to
"presumed fine".

Kotlin/JNI has no equivalent: its wrappers are entered from a JVM, and reaching
them needs one.

**Exit: answers move**, additively — 4 new cells for the two fallible call
shapes, no pre-existing cell changed.

Part of #198, tracked by #399.
@milyin
milyin merged commit 4971a79 into shape-coverage Aug 13, 2026
6 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.

1 participant