Repository navigation
Pre-Proposal: Wasm GC Support in the Canonical ABI #525
Description
Pre-Proposal: Wasm GC Support in the Canonical ABI
This issue proposes extensions to the Component Model's Canonical ABI for Wasm GC support and describes some of the motivation for particular choices. I am in the process of implementing these extensions in wasm-tools and wasmtime. My goals are to kick off discussion of how best to integrate GC and the canonical ABI, build consensus, and eventually get these extensions merged into the component model spec itself.
Emoji
Most important things first, I propose that we use the 🛸 emoji (an unidentified flying object1) to represent the Wasm GC extension to the component model in the Explainer.md.
The Problem of Choosing Which Core Type to Lower Into
With the introduction of Wasm GC into the component model, a component type can lower to many different core types.
Consider this record:
record point {
x: f32,
y: f32,
}
The first choice is whether to lower this to linear memory or GC, so we've already entered the world of component-to-core type lowering being one-to-many, and therefore we must give components some way of choosing which version is desired.
But let's assume we've somehow opted into using GC instead of linear memory. A naive translation of the point record into a core Wasm GC struct type might look something like this:
(type $point (struct (field f32) (field f32)))Making the fields immutable means that these objects can be passed between components without copying. But if one component's "at rest" representation of a point is mutable, this is forcing the component itself to do a copy. And if both sides need mutability, then both sides are forced to copy, and we end up with two copies instead of the single copy we would otherwise get when using the linear memory version of the canonical ABI!
Alternatively, we could make the fields mutable:
(type $point (struct (field (mut f32)) (field (mut f32))))But now if our two components don't require mutability, the engine cannot pass these objects between them without copying because it cannot know that they won't actually be mutated after they're sent across the component boundary (at least, not without a prohibitively expensive global program analysis).
Things get even worse when we consider rec groups. To recap, core Wasm types are deduplicated structurally at the granularity of whole rec groups. Therefore, two otherwise-identical types that appear in structurally-different rec groups are not the same types. Repetitions of otherwise-identical types in the same rec group are not the same types. In the following example, $a and $b are the same type, but they are different from $c, $d, and $e, which are all their own unique types:
;; Note: `$a` is defined within an implicit singleton `rec` group.
(type $a (struct (field i32)))
;; `$b` is the same type as `$a`, its `rec` group is just explicitly
;; written out.
(rec (type $b (struct (field i32))))
;; `$c` is a distinct type from `$a` because its `rec` group is different.
(rec (type $c (struct (field i32)))
(type (array i8)))
;; `$d` and `$e` are distinct from `$a` because their `rec` group is
;; different. They are distinct from each other because types are
;; effectively nominal *within* a `rec` group.
(rec (type $d (struct (field i32)))
(type $e (struct (field i32))))If the "at rest" representation inside one of our components is in a non-singleton rec group, then the component is forced to copy from the canonical ABI struct into its "at rest" struct that is identical other than the rec group it is defined within.
To summarize these observations:
- There are many core types that a single component type could lower into.
- Minimizing copying requires that the objects going into and coming out of the canonical ABI match the "at rest" representation inside each component.
Therefore, in order to minimize copies, the canonical ABI should (as much as possible) avoid prescribing one particular canonical GC type representation for a given component type; components should be able to choose the core GC types that component types lower to.
New Canonical Options
-
We add a
gccanonical option, which switches the canonical ABI over to GC mode from linear memory mode, when present. -
To allow lowered component functions' types to match the component's inner core module's "at rest" representation as often as possible, we add a canonical option to specify the core function type used during function lowering:
(canon lower (func $f) ... (core-type $my_core_function_type))
The
core-typeoption must be present if and only if thegcoption is present.When present, the component function's type and the given core function type are recursively traversed together and it is checked that component type can be lowered to the associated core type.
This allows components to configure which Wasm GC type lowering they desire, minimizing copies in the overall system.
Encoding
-
(core-type <idx>)canonical option:0x08 <idx:u32>` -
gccanonical option:0x09
GC Type Lowerings
We recurse over the component signature and for each argument and then each argument's type's nested fields and elements, we make sure that the corresponding part of the core signature (that was specified by the (core-type ...) canonical option) matches according to the following rules.
We tweak some of the lowerings depending on whether we are lowering into either
- a core value type (for direct arguments and returns), or
- a struct field or array element storage type (for nested types like the
u8in alist<u8>).
The lowerings are summarized in the following table, more details follow after.
| Component Type | Value Type | Storage Type |
|---|---|---|
bool |
i32 |
i8 |
s8 |
i32 |
i8 |
u8 |
i32 |
i8 |
s16 |
i32 |
i16 |
u16 |
i32 |
i16 |
s32 |
i32 |
- |
u32 |
i32 |
- |
s64 |
i64 |
- |
u64 |
i64 |
- |
f32 |
f32 |
- |
f64 |
f64 |
- |
char |
i32 |
- |
string when string-encoding=utf8 |
(ref null? (array (mut? i8))) |
- |
string when string-encoding=utf16 |
(ref null? (array (mut? i16))) |
- |
string when string-encoding=latin1+utf16 |
(ref null? (array (mut? i8))) |
- |
(record ...T) |
(ref null? (struct ...(field (mut? T')))) |
- |
(variant ...) |
(ref null? (struct) |
- |
(list T) |
(ref null? (array (mut? T'))) |
- |
(tuple ...T) |
(ref null? (struct ...(field (mut? T')))) |
- |
(flags ...) |
i32 |
- |
(enum ...) |
i32 |
- |
(option ...) |
(ref null? (struct)) |
- |
(result ...) |
(ref null? (struct)) |
- |
(own ...) |
externref |
- |
(borrow ...) |
externref |
- |
(future ...) |
externref |
- |
(stream ...) |
externref |
- |
error-context |
externref |
- |
bool and {s,u}{8,16}
These types have different lowerings depending on whether we are lowering them to a value type (arguments and results) or a storage type (a field inside another component type). When lowered to a value type, these become i32 values. When lowered to a storage type, these become i8 (bool, {s,u}8) and i16 values ({s,u}16).
Example when lowering into values:
;; Component signature.
(type $sig (func (param "x" u8)
(param "y" s16)
(param "z" bool)))
;; Core signature.
(core type $sig' (func (param i32 i32 i32)))Example when lowering into storage types:
;; Component types.
(type $tup (tuple bool s8 u16))
;; A valid core type lowering of those component types.
(core type $tup' (struct (field i8) (field i8) (field i16))){s,u}{32,64}, f{32,64}, and char
These component types are lowered to the same corresponding core type as in the linear memory version of the canonical ABI, regardless whether we are lowering into a value type or a storage type.
string
string types are lowered to references to core arrays of code points. When using the UTF-8 encoding, that means (array i8). When using the UTF-16 encoding, that means (array i16). The compact latin1 and UTF-16 hybrid encoding lowers to an array of the raw encoded bytes: (array i8).
The reference may be either nullable or non-nullable; calls will trap if the reference is null at runtime. The referenced type may be an arbitrary subtype of another array type and may be defined within an arbitrary rec group.
;; Component signature.
(type $sig (func (param "s" string)))
;; A valid core lowering, assuming UTF-8 encoding.
(core type $string' (array i8))
(core type $sig' (func (param (ref null $string'))))record and tuple
Component record and tuple types lower to references to core struct types where the component type's fields are point-wise lowered into core storage types that become the core struct's fields.
The reference may be either nullable or non-nullable; calls will trap if the reference is null at runtime. The referenced type may be an arbitrary subtype of another struct type and may be defined within an arbitrary rec group.
;; Component types.
(type $point (tuple s32 s32))
(type $triangle (record (field "x" $point)
(field "y" $point)
(field "z" $point)))
(type $sig (func (param "a" $triangle)))
;; A valid core type lowering of those component types.
(core type $point' (struct (field i32) (field i32)))
(core type $triangle' (struct (field $x' (ref $point'))
(field $y' (ref $point'))
(field $z' (ref $point'))))
(core type $sig' (func (param (ref $triangle'))))Discussion: I have not implemented width- or depth-subtyping for
records andtuples, but it is intended that GC lowering can be straightforwardly extended to allow such subtyping in the future. We will, of course, need to take care to ensure that variance is sound.
variant
Component variant types are matched against a reference to a struct type with no fields.
The reference may be either nullable or non-nullable; calls will trap if the reference is null at runtime. The referenced type may be an arbitrary subtype of another struct type and may be defined within an arbitrary rec group.
When passing a case value, the value must be an instance of a subtype of the specified core struct type. That subtype must have fields that correspond to the case's payload type lowered into a storage type, if any. All case subtypes must be defined in the same rec group, in the same order as the variant's cases.
;; Component type and signature.
(type $animal
(variant
(case $cat "cat" u32)
(case $dog "dog" u16)
(case $rabbit "rabbit" u16)))
(type $sig (func (param "animal" $animal)))
;; A valid core lowering of that component type and signature.
(core type $animal' (sub (struct)))
(core rec
(type $cat' (sub $animal' (struct (field (mut i32)))))
(type $dog' (sub $animal' (struct (field (mut i16)))))
(type $rabbit' (sub $animal' (struct (field (mut i16))))))
(core type $sig' (param (ref $animal')))Discussion: The requirement that all
casesubtypes are defined in the samerecgroup allows discriminating betweencases with structurally identical payloads.
Discussion: The way we define the exact shape of each
casetype above is less flexible than other GC lowerings, for examplerecordlowerings, where an arbitrary type in an arbitraryrecgroup can be specified and the only requirement upon the specified type is that it structurally match the component type. The corecasetypes cannot all be defined in the samerecgroup according the above rules, for example. Unlike other types, for sum types we don't have a core type in the(core-type ...)canonical option's specified core function type that the user can use to specify exactly which type a particularcaseof avariantshould be lowered into. Therefore, we define the above rules prescribing the exact types of each loweredcasevalue.An alternative approach would be to define a new canonical option that allows the user to specify the exact core types that each component
casevalue is lowered into:(case-core-type $my-component-case $my-core-type)The
$my-component-casewould name a componentvariant'scaseand$my-core-typewould name a corestructtype whose fields match the associatedcase's payload's lowering and which is a subtype of thestructtype that appears in the(core-type ...)canonical option's core function type's corresponding argument or nested type reference location.(
resultandoptiontypes would probably require defining additional canonical options as well, because they do not have name-able cases).It should be noted that this approach is still not quite as flexible as
recordlowering, because all instances of thecasevalue would always be lowered to the same core type, where as tworecords arguments in the same function call, for example, can be lowered to two different corestructtypes so long as they both structurally match. In practice, this constraint would probably not be onerous. The primary reason this approach was considered and rejected is its verbosity: every sum type in the transitive type reference closure would require specifying a(case-core-type ...)canonical option.
list<T>
Component list<T> types are lowered to a reference to a core array. The array's element type must match the lowering of T into a storage type.
The reference may be either nullable or non-nullable; calls will trap if the reference is null at runtime. The referenced type may be an arbitrary subtype of another struct type and may be defined within an arbitrary rec group.
;; Component type.
(type $bools (list bool))
(type $sig (func (param "x" $bools)))
;; A valid lowering to core types.
(core type $bools' (array (mut i8)))
(core type $sig' (func (param (ref null $bools'))))flags and enum
Component flags and enum types are lowered in the same manner as in the linear memory version of the canonical ABI, regardless whether we are lowering into a value type or a storage type.
option
Component option types are lowered to a reference to an empty core struct type. This struct type must not be final.
The reference may be either nullable or non-nullable; calls will trap if the reference is null at runtime. The referenced type may be an arbitrary subtype of another struct type and may be defined within an arbitrary rec group.
When passing a none value, the value must be an instance of a subtype of the specified core struct type which does not define any additional fields. This type must be defined as the first type in a rec group consisting of this type followed by the some value's type.
When passing a some value, the value must be an instance of a subtype of the specified struct type that has a field containing option's inner type lowered into a storage type. This type must be defined as the second type in a rec group consisting of the none value's type followed by the some value's type.
If the value is not an instance of one of the two above subtypes, then the call will trap at runtime.
;; Component types.
(type $opt (option u32))
(type $sig (func (param "x" $opt)))
;; A valid core type lowering.
(core type $opt' (sub (struct)))
(core rec
(type $opt'-none (sub $opt' (struct)))
(type $opt'-some (sub $opt' (struct (field (mut i32))))))
(core type $sig' (func (param (ref null $opt'))))Discussion: Note that we do not represent
(option T)component types with(ref null T')core types, where a null reference represents the none value and a non-null reference represents some values. There are two reasons for this:
Scalar types are heap types, so
(option u32)could not become(ref null u32)because the latter is invalid. We would have to require the definition of a(struct $s (field (mut u32)))type and then lower to(ref null $s)instead. This adds undesirable special-casing to the lowering rules.Using null references to represent none values is not composable. When dealing with
(option (option T)), if null references represented none values, then a null value is be ambiguous: does it meannoneorsome(none)? This ambiguity and composability issue could be resolved with additional special-casing, similar to the previous point, but such special-casing implies additional undesirable complexity.
result
Component result types are lowered to a reference to a core struct type without any fields. This struct type must not be final.
The reference may be either nullable or non-nullable; calls will trap if the reference is null at runtime. The referenced type may be an arbitrary subtype of another struct type and may be defined within an arbitrary rec group.
When passing a success value, the value must be an instance of a subtype of the specified struct type. This subtype must define fields corresponding to the lowering of the result's success case into a storage type, if any. This type must be defined as the first core type in a rec group consisting ofthis subtype followed by the result's error-case subtype.
When passing an error value, the value must be an instance of a subtype of the specified core struct type. This subtype must define fields corresponding to the lowering of the result's error case into a storage type, if any. This type must be defined as the second core type in a rec group consisting of the result's success-case subtype followed by this subtype.
If the value is not an instance of one of the two above subtypes, then the call will trap at runtime.
;; Component type.
(type $res (result u32 (error bool)))
(type $sig (func (param "x" $res)))
;; A valid core type lowering.
(core type $res' (sub (struct (field i8))))
(core rec
(type $res'-success (sub $res' (struct (field i8) (field i32))))
(type $res'-error (sub $res' (struct (field i8) (field i8)))))
(core type $sig' (func (param (ref null $res'))))own, borrow, future, stream, and error-context
These component types are lowered into references to core extern heap types.
The reference may be either nullable or non-nullable; calls will trap if the reference is null at runtime.
;; Component types and signature.
(type $file (resource (rep i32)))
(type $future-bool (future bool))
(type $stream-u32 (stream u32))
(type $sig (func (param "a" (own $file))
(param "b" (borrow $file))
(param "c" $future-bool)
(param "d" $stream-u32)))
;; A valid core type lowering.
(core type $sig' (func (param (ref extern)
(ref null extern)
(ref extern)
(ref null extern))))Footnotes
-
Object. Haha -- get it?? Like GC objects. GET IT!? HAHA WHY AREN'T YOU LAUGHING?!?!?!?!? ↩
Activity
fitzgen commentedon Jun 3, 2025
+cc @lukewagner @alexcrichton
fitzgen commentedon Jun 3, 2025
Realization: we could actually remove the discriminants from sum types if we prescribe that the cases are all defined in the same
recgroup.I'll prototype this and update the OP once it is working in
wasm-tools.Remove discriminants from sum types
wasmparser(CM+GC): Add support for lowering variants, options, and re…
wasmparser(CM+GC): Add support for lowering variants, options, and re…
fitzgen commentedon Jun 3, 2025
Done and done
lukewagner commentedon Jun 4, 2025
This is really exciting and I appreciate the clear writeup! Also, I love the approach of maximally-deriving the ABI from the core function type; that's an elegant solution to an otherwise gross problem. Overall, 👍 from me.
A few small questions/suggestions:
I'm probably misunderstanding the proposal, wasm-gc or both, but: for
variantet al, your examples all put the supertype in a separate rec group than the case subtypes but I would think that this doesn't expose the semantic link between "the variant type" and "the case types" that you need to let the core function type tell the whole story. E.g., iiuc, there's no "edge" in the type graph from$animal'to the rec group containing$dog'. (The edge exists in the other direction, ofc, but that's many-to-1, so you couldn't talk about "the rec group that contains subtypes of this supertype".) Instead, I would think that, since we're sortof hijacking rec groups in this scheme anyways, we'd say that variants have to refer to astructtype that is the first of its rec group and all the subsequent rec group members must be its cases; then thetypeidxused in the core function type points to a rec group that points to the cases.While you're right about the reason that
optioncan't representnonewithref.nullin general, could we say that, whenoption<T>has a non-null-allowingT,ref.nullrepresentsnone? This is ad hoc and irregular, but we are licensed to do that in the CABI when it's a significant win andoptionis very common and I expect this optimization would to 95% ofoptions in practice.result<T>(noE) andresult(noT) are also super common and I think perhaps worth optimizing. Could the former usenullforerrorand the latter return a (boolean)i32(making it no less efficient thanenum, so that no one has the perverse incentive to use anenumfor aresultfor perf reasons)?lukewagner commentedon Jun 5, 2025
Two more follow-up thoughts:
latin1+utf16string-encoding, there's the question of how to communicate the dynamic "is it Latin-1 or UTF-16?" boolean (which is packed with the length into ani32when using linear memory). One idea is that you could take a page from how variants work and have the static type be(ref null? array)and dynamically accept/produce either(ref (array i8))or(ref (array i16)).own,borrow,futureandstream(as well as waitable sets and async subtasks): one issue I just realized is that variousdropbuilt-ins trap under various conditions (ownandborrowtrap if there are any extant async import calls to which they were lent; the writable end of afuturetraps if dropped without having written a value; etc). As a general rule, we try to avoid ever allowing user code of a safe language to hit a raw wasm trap, so somehow we need to figure out how the language runtime (running as wasm using the CABI) is able to avoid hitting these traps. One option would be to extend the "drop if unreachable" semantics to be "drop if unreachable and the trapping conditions are met", but I'm thinking this will get tricky and subtle. Another option is to force the wasm guest to rootexternrefs during times when they would trap if they became unreachable, but this might end requiring the wasm guest to maintain a lot of parallel bookkeeping already maintained by the C-M, maybe adding non-trivial overhead. So I wonder if a nice third option is to leave these types the same as with linear memory (i32s that need to be dropped), but introduce new canon built-ins that provide postmortem-notification analogous-to and polyfilled-viaFinalizationRegistryand then allow the language runtime to use this primitive to Do The Right Thing itself. Such a built-in could also be used for other guest-internal purposes (such as associating a linear-memory buffer that needs to be freed with a wasm-GC object).rossberg commentedon Jun 6, 2025
A few drive-by comments:
@lukewagner:
FWIW, I don't think it is gonna be even close to 95%. Languages with parametric polymorphism typically don't or can't do such a specialisation, because it would require costly runtime type passing and dispatch. That probably includes languages like Java with its Optional type.
lukewagner commentedon Jun 6, 2025
@rossberg
Ah, interesting point. When I said "95%", I was talking about the percent of
options appearing in WIT interfaces, but also thinking about howoptions are represented "at rest" in various languages is a good for regularity.Question: if a lowered function type lowers an
optioninto a non-eqreference with an emptystructrepresentingnone, can the engine implementnonewith a single-per-type allocation (since the identity isn't observable viaeqor mutation)? If so, then thenulloptimization wouldn't even be that big of a win.rossberg commentedon Jun 6, 2025
I would represent None as an i31 ref with value zero (or any random value). It is just an instance of a variant type, where runtimes typically represent nullary constructors as different tagged ints. Which, now that I mention it, is something the variant encoding should also support.
Edit: Though null would equally work for option. What these languages generally can't do is unboxing Some.
gertvv commentedon Jun 9, 2025
This is awesome! As an outsider, the wording on variant types is confusing:
However the example defines a
(struct (field i32))which looks like a tagged union, but this is not explicitly stated. Is this the correct interpretation? And if so, can the lowering choose to use a smaller (packed) int instead, like the linear memory ABI does?fitzgen commentedon Jun 13, 2025
Ah I just forgot to update the example. There shouldn't be any
(field i32)there. Will update the OP shortly.fitzgen commentedon Jun 13, 2025
@lukewagner
Because the shape of the cases' rec group is fully defined, there doesn't need to be an explicit link: the engine/fused adapter compiler can simply (re)define the cases' rec group and then rely on type canonicalization.
This approach is slightly more flexible than keeping the base type in the same rec group as the cases, since it allows the base type to be defined in any rec group of the producer's choosing.
I'm fine with ultimately doing this kind of thing, but I am not going to do it for now, in my current prototyping, and therefore I am not going to update the OP with it either (which I will try to keep in sync with my prototyping).
As this extension settles and gets upstreamed into the spec, it should of course describe what we ultimately come to consensus on standardizing, and not what I just happen to have prototyped, but we can cross that bridge when we get to it.
Yeah, this makes sense and the OP reflects a misunderstanding on my part. Will update as soon as I fix my prototype.
Alternatively, we could continue to require explicit drop calls (as we do now for linear memory) rather than say that these things are implicitly dropped on finalization. If the
externrefbecomes unreachable without the future/stream having been explicitly dropped, then the future/stream is never dropped. Language-specific bindings generators can wrap this stuff up similar to what they would have to do now when translating to the linear memory variant of the ABI (e.g. Python would use itswithconstruct).Then, as a next step, we can introduce those canonical builtins for finalizers that allows them to Do The Right Thing.
This approach is smaller and more incremental and crucially doesn't require finalizer support in implementations initially at all.
fitzgen commentedon Jun 13, 2025
@rossberg
This gets at the philosophical question of how much duck typing we want to do. The proposal is doing some amount already with its matching of component types against many core types by their shape, but right now they always match against one shape modulo nullability, subtyping, and rec group. Allowing
i31refin various places is widening things such that we would match against multiple shapes. It seems to me like this could potentially be a slippery slope: How many shapes is enough? How do we decide whether a shape is worthy of matching against?I'm not saying I'm against adding support for
i31ref, but I do think we should consider what design principles we want to follow in this CM+GC extension.It is also possible my concerns are overblown, and this is the only additional shape we will actually want to match against.
8 remaining items
tanishiking commentedon Feb 20, 2026
Hi @fitzgen
Would you be open to sharing your prototype of the
wasmtime/wasm-toolschanges? I'd be happy to help build out the prototype and validate that the approach works well from the producer side 😃.In scala-wasm, (uses WasmGC), we currently have to copy data between GC objects and linear memory when communicating with other components, and it's slow as mentioned in #525 (comment) so this proposal would be a significant improvement for us and other GC-based languages.
fitzgen commentedon Feb 23, 2026
@tanishiking everything I prototyped has landed, I don't think I have anything in personal branches.
wasmparserhas an implementation of this pre-proposal andwasmtimehas the very beginning of an implementation.But I also haven't been pushing on this very much because (a) I've had other, high-priority things to do, and (b) the whole lazy value lowering work seems like it is going to redo a bunch of the canonical ABI and it perhaps makes sense to delay this effort to that time and/or synchronize with it.
tanishiking commentedon Feb 24, 2026
@fitzgen Thanks for update! I'll play around with the current implementations to see what'd be possible within the existing design!
Cool! I wasn't aware that lazy value lowering would be actively worked on. It would be great if the
validxabstraction in lazy value lowering were designed to support not justi32pointer/index values but also GC reference values, similar to what this proposal does :) And I absolutely agree that synchronizing with the lazy value lowering work makes sense 👍SebastienGllmt commentedon Feb 24, 2026
We have a project that also needs Wasm GC support in the component model to work effectively. I'd be happy to collaborate with anybody to get this over the finish line. My email + twitter are in my bio 🙏
simolus3 commentedon Mar 22, 2026
How would this interact with the async proposal, specifically the stackless ABI using callbacks? My understanding is that the caller passes a result pointer for an async call for that ABI. I assume a direct port of that to WasmGC might be for the caller to provide some kind of GCed "box" into which
task.returnwould write the value?lukewagner commentedon Mar 23, 2026
@simolus3 That sounds right to me.
justinfagnani commentedon Apr 22, 2026
Is there anything that interested parties can do to help this effort along?
I'm making a new language for Wasm GC with planned component support built into the compiler, so this issue is at a very critical intersection of features for me.
spotandjake commentedon Apr 22, 2026
Not quite sure on your exact question (hopefully, with more info can help you out there). I just wanted to mention that nothing stops you from copying your gc information into linear memory and using the regular component model as it is right now, the only real dissadvantages to this approach, are you have to copy the data which means walking your gc structs or creating converters somehow and you don't get support for finalizers so there isn't really a way to communicate to another component your done with a resource without manually calling free.
justinfagnani commentedon Apr 23, 2026
@spotandjake yes, you can always copy data in and out of linear memory. This is what I do now.
Like @tanishiking points out, for GC-to-GC languages calls, this results in 3 copies per call: 1 in the calling component from GC types to linear memory, 1 in the host from caller linear memory to receiver linear memory, and 1 in the receiver component from linear memory into GC types. Then 3 copies again for return values. And it's not just the copies, you need to include the code to map to and from linear memory, and an allocator.
So this issue is critical for all Wasm GC languages that are used to write components.
kevmoo commentedon Apr 23, 2026
What @justinfagnani said! (Hi, @justinfagnani 👋 )
simolus3 commentedon Oct 18, 2026
Is there something we can do to help with this, e.g. by turning this into a PR?
I've built component model support for Dart using the approach of copying across the component boundary, but the lack of finalizers means I can't reference any object/resource owned by a linear memory component from a Dart object without leaking memory. This means I have to reimplement things like strings, regular expressions and number formatting over WASM arrays instead of e.g. bundling a Rust library providing this (and strings implemented in linear memory can e.g. use faster SIMD searches than an
(array i16)which only allows byte-level access). So even if we ignore copies at the CM boundary, not having finalizers makes implementing languages much harder than it needs to be.I think having canon definitions emulating a finalization registry (and perhaps weak references too) would be very helpful for GC-based languages. On the web, these languages already import JS weakrefs and finalization registries. Without these primitives in the component model, we also can't implement the full standard library of those languages.
With a finalization registry, GC-based components could allocate linear memory to implement their strings and lists objects and then free them when the object becomes unreferenced. By adopting linear memory for strings and lists, a GC component could use the current ABI without excessive copying.
Does have a PR defining these have any chance of landing currently? I would be happy to help spec builtins for weak references and finalization registries as a first step and open a PR for that.
Also one question on the original proposal: For lists, would it make sense to have an option to pass a subarray? For a component using linear memory, passing a slice across components is a mostly a matter of pointer arithmetic. For a JS guest, a
(list u8)should probably be represented as aUint8List, which is likely implemented with a backing(array i8)+ offset + length. Being able to pass this without copying again would be nice.lukewagner commentedon Oct 21, 2026
That seems like a pretty good idea for an incremental first addition to me!
So just to check my understanding: you're suggesting no changes to lifting/lowering (yet), but rather a few new ABI built-ins (that can use Core WebAssembly reference types b/c they're ABI built-ins) that Jco could polyfill with
FinalizationRegistery/WeakRef? If so, before a full PR, maybe a good first step would be to build a prototype implementation of these new ABI built-ins in Jco that you could validate with your Dart implementation? (cc @vados-cosmonic for thoughts.) Since this issue is already a bit long and is mostly focused on the lifting/lowering ABI questions, it's probably also a good idea to create a new issue dedicated to this idea to sketch out the shape of these new ABI built-ins before or during the prototyping.justinfagnani commentedon Oct 22, 2026
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.
spotandjake commentedon Oct 22, 2026
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.
simolus3 commentedon Oct 22, 2026
Finalizers require callbacks, which requires a decision on when these callbacks run. The component model has a perfect primitive for this in the form of waitables, it doesn't seem like this can be added to core WebAssembly.
I have opened #752 to discuss that specifically, I'd love to hear your thoughts there!