Repository navigation
Canonical definitions for weak references and finalizers #752
Description
As suggested in #525 (comment), I'm opening this issue to propose weak references and finalizers as features for the Component Model.
For context, these features would help components built in garbage collected languages (like Dart and Kotlin) reference resources from structs and arrays, registering custom callbacks to dispose them when they're no longer referenced. Even outside of the component model, weak references are commonly used on those languages to implement e.g. caches, and not having them on WASI targets prevents a full implementation of these languages.
Shouldn't this be part of core WebAssembly?
One could argue that these constructs, which are available in most popular languages using garbage collection, should be part of the core WebAssembly spec instead of the component model. However, it doesn't look like there's a current proposal to advance support for weak references or finalizers in core WebAssembly. This is also understandable, as an embedder with support for those can easily provide them as host functions without that causing a significant slowdown (that's what everyone does on the web). These custom imports should be part of the component model to share them across embedders.
Another issue is that finalizers require running callbacks, so then there's a question of when to run them (and on which thread). For example, Java uses a blocking queue that a cleaner invokes in a fresh thread, while JavaScript and Dart expect finalizers to run as event loop iterations. Encoding these expectations in WebAssembly is hard, but the component model already exposes the right primitives through waitables and its different concurrency options. So I think the Component Model is a perfect home for finalizers.
Arguably, weak references could be part of core WebAssembly. But having them in the Component Model unblocks languages from targeting it soon-ish, as opposed to the multiple years it would take to standardize this in core WebAssembly. The GC proposal includes weak references as a post-MVP proposal, but also notes that finalizers are difficult to map to WebAssembly because different languages have different expectations on how they work (something mostly solved by the component model).
From @justinfagnani in #525 (comment):
Shouldn't weakrefs and finalization registries be in Wasm GC proper, not the component API? This would be one more thing that a toolchain has to compile differently for core modules on JS vs components.
Weak references and finalizers are already based on function imports on the web, and changing imports to component imports doesn't affect a compiler much.
And a follow-up from @spotandjake in #525 (comment)
I had a similar concern, if finalization registries and or weakrefs are added to the component model, it may duplicate effort or cause duplicate apis if similar wasm proposals are made as extensions to wasm gc.
I believe there are other canon builtins that will later map onto core WebAssembly features (like e.g. stack switching). So once we have them in core WebAssembly, these canon definitions could be implemented on top of those definitions.
Weak references
I propose we add two new canonical definitions:
(canon reference.create $type (core func $create))
(canon reference.get (core func $get))validation specifies:
$createis given type(func (param (ref any)) (result (ref extern)))$getis given type(func (param (ref extern)) (result (ref null any)))- initially,
$typemust beweak. We can later support other references, like Java's soft and phantom references.
$create creates a weak reference and $get resolves it if it's still strongly reachable. A CM embedder is allowed to implement weak references with strong references, components can't assume that weak references are actually supported for their operation. For example, an embedder might choose to disable garbage collection entirely for short-lived components, and then ignore weak references.
Finalizers
Finalizers allow running callbacks asynchronously after an object has been garbage collected. I propose these canonical definitions for them:
(canon refqueue.create (core func $create))
(canon refqueue.attach (core func $attach))
(canon refqueue.detach (core func $detach))
(canon refqueue.poll (core func $poll))
(canon refqueue.drop (core func $drop))validation specifies:
$createis given the type(func (result i32)),$dropis given the type(func (param i32)).$attachis given the type(func (param i32) (param (ref any)) (param anyref) (param anyref))$detachis given the type(func (param i32) (param (ref any)))$pollis given the type(func (param i32) (param (ref $x)) (result i32)), where$xmatches(array (mut anyref))and needs to be included as a canon option (Pre-Proposal: Wasm GC Support in the Canonical ABI #525). Maybe we can have a non-batching poll instead to avoid having to specify a type.
refqueue.create returns an instance of a class ReferenceQueue(Waitable), meaning that reference queues can be added to waitable sets. refqueue.attach tracks a non-nullable object (the second parameter). I think tracking an i31ref should probably be a trap, but I need to figure that one out still. Once the parameter becomes unreachable, the third parameter is added to the reference queue and we call Waitable.set_pending_event to dispatch that as a notification. The fourth parameter is an optional detach token. When non-null, calling refqueue.detach can be called with that object undo an earlier attach call.
The second and fourth parameters to attach don't count towards those objects being accessible to the program. Again, this should be defined in a way that allowed embedders to never actually add anything to the reference queue. If an object is registered on multiple reference queues and becomes unreachable, it may be enqueued on any number (including none) of them. Once a held value has been enqueued, all reference.getcalls on it must return null. I don't think we need to be stricter than that (e.g. to only set weak references to null when no user code runs like in JS).
Pseudo-JS implementation of reference queues:
class ReferenceQueue extends Waitable {
#pending = [];
#registry = new FinalizationRegistry((v) => {
this.#pending.push(v);
if (this.#pending.length === 1) this.set_pending_event(/* todo: event code for refqueues */);
});
attach(object, param, detachToken) {
this.#registry.register(target, held, token ?? undefined);
}
detach(token) {
this.#registry.unregister(token);
}
poll(target) {
const popped = this.#pending.splice(0, target.length);
popped.forEach((v, i) => { target[i] = v; });
return popped.length;
}
}I want to experiment with these builtins in jco and a Dart implementation of components, and I'll share results of that here. But feedback and discussion items on these proposals would be very welcome, thanks!
Activity
spotandjake commentedon Oct 22, 2026
I think this is a really interesting proposal, and I appreciate the work that's gone into exploring it. I'm not entirely convinced this is the right direction yet, though, and I wanted to share an alternative idea I've been thinking about.
I think the argument that these features could initially be implemented in the Component Model and later built on top of core Wasm support is fair. However, I'm not entirely convinced that the duplication is entirely justified. If weak references and finalization registries are eventually implemented in core Wasm, much of the motivation for exposing them as Component Model features seems to disappear.
I have an alternative idea, although I haven't worked through all the details of how it would fit into the Component Model, so I may be missing some important considerations.
Finalization registries seem like a useful way to support resource cleanup when GC-managed objects become unreachable. However, they also solve a much broader set of problems, and I'm wondering whether that scope is larger than what we need to address here.
My alternative would still use finalization, but in a more narrowly scoped way. The idea would be to provide a canonical function something along the lines of
ref.drop_linkthat associates a GC object's lifetime with the release of an owned resource handle.I haven't worked through the exact semantics yet. The idea is that calling
ref.drop_linkwould associate the GC reference with the owned resource handle. When the referenced object becomes unreachable, the engine would arrange for the resource handle to be dropped using the existing canonical resource-drop mechanism.The exact mechanism would depend on how ownership is represented, but I imagine this working differently depending on how values cross the component boundary.
Linear memory to linear memory:
ref.drop_linkwouldn't be needed. Consumers would manage resource lifetimes through explicit drop calls, as they do today.GC to linear memory: This use case is already supported today. The host can keep GC references in a reference pool and explicitly remove them when the linear-memory consumer calls
free. This allows the GC object to remain reachable for as long as the consumer needs it, without requiring GC-driven cleanup across the boundary.Linear memory to GC: This is where I think
ref.drop_linkwould be useful. A GC host could associate its representation of a resource with the corresponding owned resource handle. When that GC object becomes unreachable, the engine could arrange for the handle to be dropped, without requiring the host language to implement its own finalization registry for this purpose.GC to GC: This could combine the two approaches, depending on which side owns the resource and which side is responsible for keeping the relevant object alive.
This still relies on something conceptually similar to a finalization registry internally, but it would expose a much smaller API surface tailored to resource lifetime management at component boundaries, rather than providing a general-purpose finalization mechanism.
Additionally, if in the future GC objects have canonical representations that can be associated across component boundaries, it might be possible to provide a complementary operation, perhaps
ref.link_ref, for linking the lifetimes of GC objects on either side of the boundary. I haven't explored this idea enough to say whether that would be practical, but it could be an interesting extension.I'd be interested to hear whether this narrower approach is feasible, or whether there are reasons why exposing general-purpose weak references and finalization registries at the Component Model level would be preferable.
justinfagnani commentedon Oct 22, 2026
WeakRefs, ephemerons, and finalizers are definitely needed in Wasm GC, but I don't see why they should be part of the component model and more part of Wasm GC proper.
What I mean is that a compiler has to fork its emit based on wether its targeting JS or the component model and import either JS helpers or use component builtins. To-Wasm compilers already have to do too much forking based on the target, so I would like to not add more to that pile.
Plus, that's only JS and the component model. Core modules not targeting a specific environment would still have no WeakRefs available.
Yes, there has to be a proposal, but you're making a proposal for the component model, why not make one for WebAssembly? I don't see why it should inherently take less time, except for possibly a smaller group to get consensus on or a lower bar for inclusion, but assuming or relying on those doesn't seem ideal path towards standardization to me.
The scheduling issues are still present, no matter where the feature lives. If the finalizer scheduling doesn't work for Java here, then what? Why is that acceptable here, but not in core WebAssembly?
I would have to dig into this space a lot more to provide a more informed answer to scheduling in core, but there are multiple possible answers, some of which at least have been discussed before:
With a fast
finalizers_pendingcheck, it seems like the guest can schedule the finalizers however they want.lukewagner commentedon Oct 23, 2026
I appreciate the argument for not wanting to duplicate core wasm functionality. That being said, because the CM is a "layer up" from core wasm, it's not terrible if the CM has built-ins that are later redefined in terms of core wasm instructions, and indeed this is what the Threads SG proposed the CM do a while back with
thread.spawn_refet al. The reason was that while thread-spawning should get added to core wasm in the future, for practical resourcing/prioritization purposes, it won't get added any time soon and thus a CM built-in is a good stopgap. Similarly, CM 0.3 concurrency is specified in terms of future hypothetical stack-switching instructions. I think the same argument might apply here: I haven't seen any proposals or heard of any imminent plans to propose finalization/weak-ref primitives, which means these are in all likelihood years away and so defining CM built-ins that can be polyfilled viaFinalizationRegistery/WeakReftoday seems like a practical stopgap.fitzgen commentedon Oct 23, 2026
I don't think we need callbacks for this. Could instead probably represent this with streams instead, which removes one more thing to standardize from the critical path.