feat(option-A): per-component handle tables + bridging (3/3 trio passes)#109
feat(option-A): per-component handle tables + bridging (3/3 trio passes)#109
Conversation
AI Code Review for PR #109pulseengine/meld: This pull request introduces a new feature in the Meld tool, which is a static component fusion engine for WebAssembly. The main changes are:
Overall, this feature enhances Meld's ability to handle complex WebAssembly component compositions while maintaining performance and security. This review was generated by a local AI model. It is advisory only and may contain inaccuracies. Reviewed at |
Previously reexporter_resources only listed re-exporter components that need handle tables (where callee_defines_resource=false in adapter sites). Option A's per-component-tables-with-bridging architecture also needs the DEFINER component to have its own ht so cross-component handle hand-offs can translate via Phase 3 bridging trampolines: caller_handle -> caller_ht_rep -> rep -> callee_ht_new -> callee_handle. After this change, resource_with_lists shows TWO ht entries (was 1): comp 2 (intermediate/re-exporter) — its existing exports ht comp 0 (leaf/definer) — new, allocated by Phase 1 Standard suite: 73/73, 0 regressions. resource_floats runtime still passes (no regression). resource_with_lists still traps — that's expected; Phases 2 (definer-fallback restriction) and 3 (bridging trampolines in fact.rs) still pending.
…ndles
When per-component handle tables exist on BOTH sides of a cross-
component call (post-Phase-1), the handle value space is no longer
shared — a memory-pointer handle from one component's ht refers to
that component's memory, not the other's. Without translation, the
receiving component's ht_rep loads from its own memory at the
caller's offset → garbage.
Phase 3 inserts bridging trampolines in the FACT adapter:
own<T> result transfer (callee_defines_resource=true case, lines 715+):
When both caller and callee have their own ht for the resource,
emit ResourceOwnResultTransfer { rep_func: callee.ht_rep,
new_func: caller.ht_new }. The pre-existing emit_resource_new_results
emits (call rep_func)(call new_func) which performs the bridge:
callee_handle → callee.ht_rep → rep → caller.ht_new → caller_handle.
borrow<T> param transfer (callee_defines_resource=true, lines 598+):
When BOTH the caller has its own ht AND callee has one too, emit
ResourceBorrowTransfer with callee.ht_new in the new_func slot
(was None previously). Same emit phase produces the bridge.
Gating: bridge fires ONLY when caller and callee are different
components AND caller has its own ht for the resource. Without the
caller-has-ht check, the bridge over-fires for fixtures where the
caller uses canonical [resource-rep] (no ht), corrupting handles by
double-translating canonical → memory-pointer. The check restored
resource_floats after an initial regression.
Status:
- 73/73 standard suite passes (0 regressions)
- resource_floats runtime: still PASSES
- resource_with_lists, resource-import-and-export: still trap with
Option::unwrap on None — bridges fire correctly per debug logs but
the deeper issue is upstream (likely accidental ht_drop firing on
cross-memory bridged box pointers, OR multi-memory addressing of
reps stored in one component's memory and loaded by another).
The bridging architecture is correct in principle — both fixtures
need additional investigation of where the box pointer gets freed or
mis-addressed. Tracking under issue #107.
Three layered fixes get resource_floats, resource_with_lists, and resource-import-and-export passing as runtime_test! fixtures: 1. fact.rs — discriminate the borrow lower based on whether the callee's exported function is a `[method]/[static]/[constructor]` on a locally defined resource (cabi expects REP) versus a top-level function taking borrow<T> on a `use`d resource (cabi expects HANDLE). Suppress `callee.ht_new` for the former; the rep-only path matches the canonical ABI lower of borrow<T>. Without this, the slot address minted by callee.ht_new gets passed as the rep, the export's `ThingBorrow::lift` derefs it, and Option's discriminant byte is the low byte of the stored rep (0 for typical aligned box pointers) → Option::unwrap on None. Applied symmetrically in both the 2-component (callee_defines=true) and 3-component (callee_defines=false) borrow branches. 2. merger.rs — suppress the ht_drop dtor invocation for re-exporter components. Phase 1's per-component HTs for definers store foreign reps placed there by own bridges (intermediate's HT contains leaf box pointers). The dtor (`<iface>#[dtor]<rn>`) casts every stored value as `*mut _ThingRep<LocalT>` and Box::from_raw drops it; for foreign reps this misinterprets memory and triggers re-entrant drops via the wit-bindgen Resource::drop impl, producing unbounded recursion. The standard wit-bindgen design assumes the rep is owned by the component whose ht stores it, but Phase 1 broke that invariant. 3. resolver.rs — sort reexporter_components and reexporter_resources before storing on the graph. They were collected from HashSet, whose iteration order is non-deterministic. Downstream HT allocation and the wrapper alias-fallback both make first-match decisions on this order, so the same fixture would sometimes wire `[resource-drop]` for the runner to leaf's ht_drop and sometimes intermediate's, producing the "passes manually, fails in cargo test" flakiness. Promotes resource_floats, resource_with_lists, and resource-import-and-export from fuse_only_test! to runtime_test! — closes the trio runtime failures tracked in issue #75. Standard 73-test suite still passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9149573 to
38f8660
Compare
Summary
Implements Option A from issue #107 — per-component handle tables with cross-component bridging trampolines. Builds on PR #108's path B foundation. All three trio fixtures now pass at runtime, closing issue #75.
Phase 1 (commit
0632ec2)Broaden
reexporter_resourcesto also include the resource's DEFINER component. Withresource_with_lists, this allocates 2 ht entries (was 1): one for the leaf definer, one for the intermediate re-exporter.Phase 3 (commit
8437e3f)Insert bridging trampolines in
fact.rsadapter:Phase 4 — trio close-out (commit
9149573)Three layered fixes for the remaining 2 of 3 trio runtime failures:
fact.rs— borrow-rep discriminator (both 2-comp and 3-comp branches). Method-like exports ([method]/[static]/[constructor]) on a locally-defined resource expect the REP asarg0(wit-bindgen's_export_*_cabicallsThingBorrow::lift(arg0)which derefs as*mut _ThingRep<T>). Top-level functions takingborrow<T>on aused resource expect a HANDLE (cabi callsFloat::from_handle(arg0 as u32)). Suppresscallee.ht_newfor method-like calls; emit onlycaller.ht_repso the rep flows through unchanged. Without this, the freshly-minted slot address gets passed as the rep, the deref reads 4 bytes at the slot (the just-stored rep), andOption's discriminant byte is the low byte of that rep — 0 for typical aligned box pointers →Option::unwrap on None.merger.rs— dtor suppression for re-exporterht_drop. Phase 1's per-component HTs for definers store foreign reps placed there by own bridges (intermediate's HT contains leaf box pointers). The dtor (<iface>#[dtor]<rn>) blindly casts every stored value as*mut _ThingRep<LocalT>andBox::from_rawdrops it; for foreign reps this misinterprets memory and triggers re-entrant drops via the wit-bindgenResource::dropimpl, producing unbounded recursion. Skip the dtor when the component is a re-exporter — the standard Box-pattern wit-bindgen design assumes the rep is owned by the component whose ht stores it, but Phase 1 broke that invariant.resolver.rs— sortreexporter_componentsandreexporter_resourcesbefore storing on the graph. They were collected fromHashSet, whose iteration order is non-deterministic. Downstream HT allocation and the wrapper alias-fallback both make first-match decisions on this order, so the same fixture would sometimes wire[resource-drop]for the runner to leaf'sht_dropand sometimes intermediate's, producing the "passes manually, fails in cargo test" flakiness.Status
resource_floatsruntimeresource_with_listsruntimeresource-import-and-exportruntimeAll three trio fixtures promoted from
fuse_only_test!toruntime_test!. Verified stable across 5/5 consecutivecargo testruns after the determinism fix.Related
Test plan
cargo test --release --test wit_bindgen_runtime— 73/73resource_floatsruntime passesresource_with_listsruntime passesresource-import-and-exportruntime passesBranch
feat/per-component-ht-bridginghead9149573— 3 commits on top offeat/conditional-resource-typing.