Skip to content

ffi: cut the cost of crossing the ABI - #60

Merged
Kinflou merged 2 commits into
mainfrom
ffi/crossing-cost
Oct 26, 2026
Merged

Kinflou merged 2 commits into
mainfrom
ffi/crossing-cost

Conversation

@Kinflou

@Kinflou Kinflou commented Oct 26, 2026

Copy link
Copy Markdown
Member

Stacked on #59; it needs #59's benches as its base. Retarget it to main before #59 is merged with --delete-branch.

Cuts what the C ABI costs on top of the core. Every ABI call is now cheaper and none costs more. No refusal changes, and the header doesn't change.

What changed

  • The overlap check (disjoint!) runs on every call. It was a loop over a stack array, about 360 of the 520 instructions encode_request added on top of the core.
    • It now expands at compile time to the pairs themselves: each written region against every region after it, never two reads, with no loop or branch.
    • The rule is the same. disjoint_is_the_plain_rule compares it exhaustively with the rule written out plainly:
      • every three regions over small addresses, null, and addresses near the top of the address space;
      • lengths up to usize::MAX;
      • every split of the three into written and read.
    • Two planted off-by-ones (< written as <=, and the reads dropped from the recursive arm) each fail that test.
  • span_in checks once that a span's end fits u32, which covers both the offset and the length, instead of converting them one by one.
    • It can't fail where it didn't before: a frame ends at most at PREFIX_LEN + COMLINE_MAX_FRAME_CEILING == u32::MAX.
  • comline_handshake_check checks peer_out for overlap only when it's given, since it's the only memory the call writes.
  • SliceBuf::{new, written, overflowed, clear} are #[inline], so the ABI and other crates no longer call out for them. The core's benches are unchanged.
  • New bench pair handshake-check-out: handshake_check with a peer_out.

Instruction counts (base #59 → this, measured locally at the same paths)

Call Before → after
encode-request/datagram@2/message/16 965 → 742 (−23.1%)
encode-request/jsonrpc@1/message/16 1147 → 924 (−19.4%)
encode-request/*/65536 −0.34%
feed/datagram@2/* 319–327 → 294–299 (−7.7% to −8.6%)
feed/jsonrpc@1/message/16 1728 → 1703 (−1.4%)
encode-reply/* −1 instruction each
name-hash 199 → 198
handshake-encode 785 → 772 (−1.7%)
handshake-check 152 → 138 (−9.2%)
handshake-check-out 213 → 210 (−1.4%)

All 104 FFI benches: 0 regressed. All 58 core benches: no change.

Checked locally

  • cargo test --workspace, plus core with --no-default-features --features alloc,jsonrpc.
  • Clippy, rustfmt, and the wasm32 build.
  • The header regenerates unchanged (cbindgen 0.29.2).
  • The C harness (static, dynamic, ASan + UBSan) and the Node harness against wasm32: 65/65 cases each.
  • Miri: cargo +nightly miri test -p comline-runtime-ffi --features test-hooks.
  • ffi_feed: 5 min, 1.36M runs, no findings.
  • ffi_vs_core: 5 min, 1.15M runs, no findings.

Left alone: the about 87 instructions of UTF-8 validation of a request's name. The ABI promises to refuse an invalid name under every framing.

The overlap check every call runs was a loop over a stack array: about
360 of the 520 instructions `encode_request` added on top of the core.
`disjoint!` now expands to the pairs themselves at compile time, each
written region against every region after it, never two reads, with no
loop or branch. The rule it checks is the same, and a test compares it
exhaustively with the rule written out plainly, over small addresses,
null, and lengths up to the top of the address space.

- `span_in` checks once that a span's end fits u32, which covers both
  casts, instead of converting the offset and the length one by one. A
  frame ends at most at PREFIX_LEN + COMLINE_MAX_FRAME_CEILING ==
  u32::MAX, so the check never fails on a reachable state.
- `comline_handshake_check` checks `peer_out` for overlap only when it is
  given: it's the only memory the call writes.
- `SliceBuf::{new, written, overflowed, clear}` are `#[inline]`, so other
  crates (the ABI among them) don't call out for them.
- A `handshake-check-out` bench pair measures `handshake_check` with a
  `peer_out`.

Instruction counts, against the base at the same paths: every ABI bench
is lower, none higher; the core benches are unchanged.
@Kinflou
Kinflou changed the base branch from ffi/bench to main October 26, 2026 14:58
@Kinflou
Kinflou merged commit cb41d31 into main Oct 26, 2026
24 checks passed
@Kinflou
Kinflou deleted the ffi/crossing-cost branch October 26, 2026 15:03
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