jrandolf opened PR #14928 from jrandolf:map-dedupe-handles to bytecodealliance:main:
This builds on #14870, so only the last commit is new here.
Lifting a map with duplicate keys keeps the last value for each key, as the spec allows. When the values contain handles, the replaced values' handles leaked, so for example a guest resource's destructor never ran. Now they're dropped as if the embedder had dropped them.
One question I'd appreciate input on: discarded handles are queued per store rather than per call, so each drain drops everything pending. Every drain point is a place where running a destructor is fine, so I think that's OK. Per-call tracking is possible if preferred, with a fair bit more plumbing.
Closes #14812
<details>
<summary>LLM summary of the changes</summary>Lifting (
func/typed.rs)
- Whether a map's value type contains handles is computed once, when the map type is built (
TypeMap::value_contains_handlesinwasmtime-environ). Maps whose values don't contain handles are lifted as before.- Otherwise all keys are lifted first, since they can't contain handles, and
LiftMap::replaced_keysfinds which entries are followed by an equal key.BTreeMapcompares keys withOrd, andHashMapandIndexMapwithHashandEq.- Replaced values are lifted as
Vals, in order with the others, and passed toLiftContext::discard.Dropping the handles (
func/options.rs,store.rs,values.rs)
- Releasing a discarded
borrowruns no code, so it happens right away.- Dropping an
ownhandle may run a destructor in a guest, which can't happen in the middle of a lift. So those handles are queued in the store'sdiscarded_handles, anddrop_discarded_handlesdrops them:
- after post-return, for the results of a call into a component;
- before a host function runs, for its parameters (
func/host.rs);- by a task on the event loop, for results lifted there (for example with
TypedFunc::call_concurrent) and for items read from a stream or future (concurrent/func.rs,concurrent/futures_and_streams.rs).- Handles discarded elsewhere, such as while lifting the elements of a
WasmListon demand, are dropped along with those of the next such call.- Dropping every handle within a
Valwas previously only done inLinker::define_unknown_imports_as_default_values. It's now shared code invalues.rs.Test
component_model::map::duplicate_keys_with_resourceslifts a guest map with two resources under the same key intoHashMap,BTreeMapandIndexMap. It checks that the replaced resource's destructor ran by the time the call returned, that the kept resource is the last one, and that every destructor runs exactly once.</details>
jrandolf removed the review request for wasmtime-core-reviewers from PR #14928.
jrandolf requested wasmtime-core-reviewers for a review on PR #14928.
jrandolf requested pchickey for a review on PR #14928.
jrandolf requested alexcrichton for a review on PR #14928.
jrandolf removed the review request for wasmtime-default-reviewers from PR #14928.
jrandolf requested wasmtime-default-reviewers for a review on PR #14928.
:memo: alexcrichton submitted PR review:
Subjectively -- this is a hugely invasive change for what was supposed to be a minor addition to the component model. To me this is a sign we need to go back to the drawing board on something, but I don't quite know what.
jrandolf commented on PR #14928:
Yeah, I just realized this would basically be garbage collection which would need a wider design.
Would it be OK to just document the leak instead? Something like this in the ComponentType docs:
Lifting a map<K, V> keeps only the last value for each duplicate key. Any handles in the replaced values aren't dropped: they leak within the store, and a ResourceAny borrow traps when the host function returns. Use Val::Map to see every entry in order instead.
For precedent, the spec lets bindings deduplicate as long as the last value wins and says nothing about handles. JS engines and jco leave the replaced value to the GC, and wit-bindgen's Go bindings just drop it. Wasmtime already documents that a ResourceAny or FutureReader the embedder doesn't drop leaks within the store, so this would follow that. If that works for you, I'll add it to #14870 and close this.
github-actions[bot] added the label wasmtime:api on PR #14928.
alexcrichton commented on PR #14928:
I think that's possible, yeah, but I'm also still worried about the case where passing a map of borrows will trap because the host didn't remove all the borrows and there's no way to NOT trap in that situation. I talked with some others and will look to pursue https://github.com/WebAssembly/component-model/issues/751 as an alternative
:cross_mark: jrandolf closed without merge PR #14928.
jrandolf commented on PR #14928:
Closing in favor of WebAssembly/component-model#751.
Last updated: Oct 31 2026 at 23:03 UTC