Run the generated C bindings, and settle what the call axis could not (#198, step 2c) - #409
Merged
Merged
Conversation
…#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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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 acargo 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
()-returning C binding cannot report an alias rejection — it aborts. A panic cannot crossextern "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.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.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 becausecargo checkdoes 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 warningsacross all targets and features,cargo fmt --checkwith CI's config,RUSTDOCFLAGS=-D warnings cargo doc,examples/regen-check.shclean.