Repository navigation
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.
simolus3 commentedon Oct 24, 2026
Thanks for the responses! I'll start by experimenting with this in jco. To address existing questions and feedback:
For Dart, I want to use finalizers to reference owned resources, and I think
ref.drop_linkis enough for that. However, I also want to use them for things that don't use CM resources, e.g. to have ownership of a RustBox<T>from a GC struct. Porting Dart to the Component Model means I need to port Dart strings and regular expressions. With finalizers, I could back those objects with an existing Rust regex crate compiled to WebAssembly. These are an implementation detail within a single component, so not an owned resource.I can't reliably free those native objects without finalizers. So, I'd have to implement regular expression parsing and evaluation over
array mut i16references. I currently have a vibe-coded prototype for this, but I strongly dislike this approach. Being able to use a proper existing (linear-memory based) library would be a relief.Core modules without a specific environment also don't have strings, number formatting, or println. These not being mandated is that makes wasm so ubiquitous.
Finalization registries need an asynchronous notification mechanism, which the Component Model already has. All async event sources in CM can be polled by blocking a thread, used in stack-switching coroutines, or invoke a callback on a single-threaded component via the stackless async ABI. By re-using waitables, this proposal should work for all styles of async programming already supported by the CM.
I designed my proposals for finalizers with a potential Java guest in mind (pass the
Referenceobject as a third parameter torefqueue.attachand the object it references as a second, then userefqueue.pollwithout an async flag to block the current thread until a reference has been finalized, matching Java semantics). Again, this shows the flexibility of the Component Model as the same builtin also works for Dart/JS guests not using blocking calls.As far as I know, core WebAssembly has no scheduling at all, only synchronous calls to exported functions.
For components with an event loop, there also needs to be away to indicate that a freed reference should trigger a new event loop iteration (like it does on JS when a finalization registry is used). So a guest can't just poll when it feels like it, there needs to be an active notification as well if nothing else is going on. Again, the Component Model already has these async primitives figured out for a variety of guest languages.
Exactly, that's why I think this belongs to the Component Model :) I believe streams wouldn't work directly, because we probably need a
stream<anyref>to surface metadata associated with tracked references that have been GCed. I think we should use waitables for this to let components decide how to poll for finalization events.justinfagnani commentedon Oct 24, 2026
@simolus3 Given your use case includes same-module cleanup of non-GC resources, are you saying that things like Dart's regex support wouldn't work in standard core modules? Only components?
simolus3 commentedon Oct 24, 2026
In a way, nothing "works" in core modules, there always needs to be an embedder instantiating modules with host functions. Dart has a generic wasm target that imports basically everything, including functions for regular expressions. This allows re-using a regex implementation from the embedder (wasmtime example) instead of bloating every Dart module. The Dart SDK knows nothing about the component model and doesn't treat it as a special target, I have my own tooling generating a component instantiating that module with required imports (some implemented from scratch in Dart,
some in Rust, some imported and lowered via the CM). I'd just like to implement more with existing Rust crates instead of from scratch.
rossberg commentedon Oct 24, 2026
Like with threads, I also think that this should preferably be solved in the core language, as these features have nothing to do with components as such, and are independently relevant core computational features.
Is there reason to expect that it's gonna be easier or quicker to work out weak refs for components? No progress has been made in the core because (a) it is hard and (b) nobody volunteered to work on it. But I don't see either hurdle depending on the core vs components question.
I fear that the more shortcuts we take by conveniently pushing random stuff to the component level, the less pressure there will be to provide these features in an adequate and well-specified manner that's usable by everybody everywhere. The result might be a two-class ecosystem.
(One argument for deferring thread creation primitives in the core has been the self-absorbed stranglehold that the Web platform still has on Wasm. But if we're now pushing components into the Web anyway, then this strategy might become rather moot I would think.)
simolus3 commentedon Oct 24, 2026
Related to (b) is that that it's already not a high-pressure issue because
WeakReferenceis just an import away on the web.Which then leaves the Component Model (where we can't have custom imports) as the only target that needs to have support for this for WASI to become a target for GC languages.
I think the benefits of the Component Model are mainly relevant for finalizers. Implementing finalizers requires an implementation of weak references internally as the token/detach token is weakly held. So we might as well include it, but I'd be happy to only include finalizers here if we want that for purity reasons.
rossberg commentedon Oct 24, 2026
That strikes me as a non-sequitur. As, say, a compiler writer, I certainly do not want to depend on an open-ended set of different platform/ecosystem-dependent mechanisms that may or may not be well-behaved, compatible with each other, or exist at all. For core language/runtime machinery, I want one that works everywhere.
justinfagnani commentedon Oct 24, 2026
I don't think it's true that plain core modules necessarily need their own custom imports. A module that only exports functions to compute and access data (ie, string/array/object reading helpers) can do a lot. Things like custom descriptors should make plain core modules even more usable out-of-the-box.
But even for embeddings that provide custom imports for the module to work, leaving weakrefs to the embedder instead of the runtime seems very tough to manage. If you have a Wasm GC runtime, how do you know you can get access to its GC in such a way as to forward a finalizer API into the core module?
Plus, this method of working around making proposals to core seems very problematic to me. It implies that core is not useful and components are the only real way to run Wasm. If that is true, then only the combined Wasm + component model matters as a standard, and why would there be two tiers for proposals where one is considered slower and harder, and the other is faster and easier?
I would be happy to try to contribute as much as I can to a core weakref + finalizers proposal, though I am not an ideal champion. The main sticking point for finalizers seems to be how the runtime notifies a module when object becomes unreachable.
I may be naive (I'm sure), but this doesn't seem so insurmountable to me. Very roughly: the module registers a wake-up function with the runtime. This function is called when there are new unreachable objects, but only when the module is idle: its call stack is unwound, it's not waiting on a JSPI or any other blocking call. When woken the module schedules handling of the finalization records in the queue.
justinfagnani commentedon Oct 24, 2026
edit: the original comment I'm replying to seems like it was deleted:
This is roughly true of GC in general, if you consider memory use and performance as part of your determinism guarantees. But yes, finalizers do (potentially) come with more non-determinism, yet many languages have them and tend to note that the behavior is non-deterministic, up to and including that the finalizers might not run at all. IMO, Wasm GC should still support those languages.
spotandjake commentedon Oct 24, 2026
@justinfagnani
Sorry about that I was trying to reformat it and add some more information.
While GC itself is non-deterministic, guaranteeing deterministic performance or memory usage is impractical without standardizing the exact GC algorithm. My concern is instead observable non-determinism within the sandbox. Currently, Wasm GC's non-determinism exists outside the bytecode, with no way to observe when or if collection occurs from within the sandbox itself. Adding finalizers or weak references would make that behaviour observable, potentially undermining Wasm's determinism and sandboxing guarantees (e.g., allowing engine fingerprinting through GC behaviour).
This isn't necessarily a showstopper, as Wasm does permit some forms of non-determinism Nondeterminism.md, however, finalizers and weak references seem rather different than the rest of the list so far.
I partially agree. My larger concern is the complexity of the Component Model as a whole. Adding features to CM to get them through sooner risks duplicating behaviour that could eventually exist in core Wasm, increasing complexity long-term.
That said, I think communicating drops between components is fundamentally a component-level problem. I'd just prefer a narrower, more scoped proposal addressing that use case, leaving other concerns such as finalization within a given module to core Wasm. Weak references seem to fall more into the latter category.