Skip to content

Pre-Proposal: Wasm GC Support in the Canonical ABI #525

Open

Description

@fitzgen

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:

  1. There are many core types that a single component type could lower into.
  2. 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 gc canonical 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-type option must be present if and only if the gc option 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>`
    
  • gc canonical 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 u8 in a list<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 and tuples, 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 case subtypes are defined in the same rec group allows discriminating between cases with structurally identical payloads.

Discussion: The way we define the exact shape of each case type above is less flexible than other GC lowerings, for example record lowerings, where an arbitrary type in an arbitrary rec group can be specified and the only requirement upon the specified type is that it structurally match the component type. The core case types cannot all be defined in the same rec group 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 particular case of a variant should be lowered into. Therefore, we define the above rules prescribing the exact types of each lowered case value.

An alternative approach would be to define a new canonical option that allows the user to specify the exact core types that each component case value is lowered into:

(case-core-type $my-component-case $my-core-type)

The $my-component-case would name a component variant's case and $my-core-type would name a core struct type whose fields match the associated case's payload's lowering and which is a subtype of the struct type that appears in the (core-type ...) canonical option's core function type's corresponding argument or nested type reference location.

(result and option types 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 record lowering, because all instances of the case value would always be lowered to the same core type, where as two records arguments in the same function call, for example, can be lowered to two different core struct types 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:

  1. 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.

  2. 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 mean none or some(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

  1. Object. Haha -- get it?? Like GC objects. GET IT!? HAHA WHY AREN'T YOU LAUGHING?!?!?!?!? ↩

Activity

  1. fitzgen commented on Jun 3, 2025

    @fitzgen
    CollaboratorAuthor
  2. fitzgen commented on Jun 3, 2025

    @fitzgen
    CollaboratorAuthor

    Realization: we could actually remove the discriminants from sum types if we prescribe that the cases are all defined in the same rec group.

    I'll prototype this and update the OP once it is working in wasm-tools.

  3. added a commit that references this issue on Jun 3, 2025
  4. fitzgen commented on Jun 3, 2025

    @fitzgen
    CollaboratorAuthor

    I'll prototype this and update the OP once it is working in wasm-tools.

    Done and done

  5. lukewagner commented on Jun 4, 2025

    @lukewagner
    Member

    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:

    1. I'm probably misunderstanding the proposal, wasm-gc or both, but: for variant et 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 a struct type that is the first of its rec group and all the subsequent rec group members must be its cases; then the typeidx used in the core function type points to a rec group that points to the cases.

    2. While you're right about the reason that option can't represent none with ref.null in general, could we say that, when option<T> has a non-null-allowing T, ref.null represents none? This is ad hoc and irregular, but we are licensed to do that in the CABI when it's a significant win and option is very common and I expect this optimization would to 95% of options in practice.

    3. result<T> (no E) and result (no T) are also super common and I think perhaps worth optimizing. Could the former use null for error and the latter return a (boolean) i32 (making it no less efficient than enum, so that no one has the perverse incentive to use an enum for a result for perf reasons)?

  6. lukewagner commented on Jun 5, 2025

    @lukewagner
    Member

    Two more follow-up thoughts:

    1. For the latin1+utf16 string-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 an i32 when 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)).
    2. Regarding own, borrow, future and stream (as well as waitable sets and async subtasks): one issue I just realized is that various drop built-ins trap under various conditions (own and borrow trap if there are any extant async import calls to which they were lent; the writable end of a future traps 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 root externrefs 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-via FinalizationRegistry and 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).
  7. rossberg commented on Jun 6, 2025

    @rossberg
    Member

    A few drive-by comments:

    1. The even bigger reason that there are no canonical choices of type definitions to lower to are subtyping declarations, which the comment layer cannot guess.
    2. For the various small scalar types, a common target representation will be i31-ref (e.g., in any language with a uniform representation), so I think (i31 null? ref) should also be allowed as an alternative, for both value and/or storage type.
    3. For larger numeric types, various languages will probably need boxed representations to be able to process them, so perhaps it should be possible to map to those as well.
    4. I like @lukewagner's suggestion regarding variants.
    5. It is rather unfortunate that the CM calls arrays "lists"...

    @lukewagner:

    1. While you're right about the reason that option can't represent none with ref.null in general, could we say that, when option<T> has a non-null-allowing T, ref.null represents none? This is ad hoc and irregular, but we are licensed to do that in the CABI when it's a significant win and option is very common and I expect this optimization would to 95% of options in practice.

    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.

  8. lukewagner commented on Jun 6, 2025

    @lukewagner
    Member

    @rossberg

    While you're right about the reason that option can't represent none with ref.null in general, could we say that, when option has a non-null-allowing T, ref.null represents none? This is ad hoc and irregular, but we are licensed to do that in the CABI when it's a significant win and option is very common and I expect this optimization would to 95% of options in practice.

    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.

    Ah, interesting point. When I said "95%", I was talking about the percent of options appearing in WIT interfaces, but also thinking about how options are represented "at rest" in various languages is a good for regularity.

    Question: if a lowered function type lowers an option into a non-eq reference with an empty struct representing none, can the engine implement none with a single-per-type allocation (since the identity isn't observable via eq or mutation)? If so, then the null optimization wouldn't even be that big of a win.

  9. rossberg commented on Jun 6, 2025

    @rossberg
    Member

    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.

  10. gertvv commented on Jun 9, 2025

    @gertvv

    This is awesome! As an outsider, the wording on variant types is confusing:

    Component variant types are matched against a reference to a struct type with no fields.

    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?

  11. fitzgen commented on Jun 13, 2025

    @fitzgen
    CollaboratorAuthor

    This is awesome! As an outsider, the wording on variant types is confusing:

    Component variant types are matched against a reference to a struct type with no fields.

    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?

    Ah I just forgot to update the example. There shouldn't be any (field i32) there. Will update the OP shortly.

  12. fitzgen commented on Jun 13, 2025

    @fitzgen
    CollaboratorAuthor

    @lukewagner

    1. I'm probably misunderstanding the proposal, wasm-gc or both, but: for variant et 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 a struct type that is the first of its rec group and all the subsequent rec group members must be its cases; then the typeidx used in the core function type points to a rec group that points to the cases.

    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.

    2. While you're right about the reason that option can't represent none with ref.null in general, could we say that, when option<T> has a non-null-allowing T, ref.null represents none? This is ad hoc and irregular, but we are licensed to do that in the CABI when it's a significant win and option is very common and I expect this optimization would to 95% of options in practice.

    3. result<T> (no E) and result (no T) are also super common and I think perhaps worth optimizing. Could the former use null for error and the latter return a (boolean) i32 (making it no less efficient than enum, so that no one has the perverse incentive to use an enum for a result for perf reasons)?

    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.

    For the latin1+utf16 string-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 an i32 when 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)).

    Yeah, this makes sense and the OP reflects a misunderstanding on my part. Will update as soon as I fix my prototype.

    Regarding own, borrow, future and stream (as well as waitable sets and async subtasks): one issue I just realized is that various drop built-ins trap under various conditions (own and borrow trap if there are any extant async import calls to which they were lent; the writable end of a future traps 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 root externrefs 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-via FinalizationRegistry and 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).

    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 externref becomes 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 its with construct).

    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.

  13. fitzgen commented on Jun 13, 2025

    @fitzgen
    CollaboratorAuthor

    @rossberg

    • For the various small scalar types, a common target representation will be i31-ref (e.g., in any language with a uniform representation), so I think (i31 null? ref) should also be allowed as an alternative, for both value and/or storage type.

    • For larger numeric types, various languages will probably need boxed representations to be able to process them, so perhaps it should be possible to map to those as well.

    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 i31ref in 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.

  14. 8 remaining items

  15. tanishiking commented on Feb 20, 2026

    @tanishiking

    Hi @fitzgen
    Would you be open to sharing your prototype of the wasmtime / wasm-tools changes? 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.

  16. fitzgen commented on Feb 23, 2026

    @fitzgen
    CollaboratorAuthor

    @tanishiking everything I prototyped has landed, I don't think I have anything in personal branches. wasmparser has an implementation of this pre-proposal and wasmtime has 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.

  17. tanishiking commented on Feb 24, 2026

    @tanishiking

    @fitzgen Thanks for update! I'll play around with the current implementations to see what'd be possible within the existing design!

    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.

    Cool! I wasn't aware that lazy value lowering would be actively worked on. It would be great if the validx abstraction in lazy value lowering were designed to support not just i32 pointer/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 👍

  18. SebastienGllmt commented on Feb 24, 2026

    @SebastienGllmt

    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 🙏

  19. simolus3 commented on Mar 22, 2026

    @simolus3

    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.return would write the value?

  20. lukewagner commented on Mar 23, 2026

    @lukewagner
    Member

    @simolus3 That sounds right to me.

  21. justinfagnani commented on Apr 22, 2026

    @justinfagnani

    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.

  22. spotandjake commented on Apr 22, 2026

    @spotandjake

    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.

    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.

  23. justinfagnani commented on Apr 23, 2026

    @justinfagnani

    @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.

  24. kevmoo commented on Apr 23, 2026

    @kevmoo

    What @justinfagnani said! (Hi, @justinfagnani 👋 )

  25. simolus3 commented on Oct 18, 2026

    @simolus3

    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 a Uint8List, which is likely implemented with a backing (array i8) + offset + length. Being able to pass this without copying again would be nice.

  26. lukewagner commented on Oct 21, 2026

    @lukewagner
    Member

    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.

  27. justinfagnani commented on Oct 22, 2026

    @justinfagnani

    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.

  28. spotandjake commented on Oct 22, 2026

    @spotandjake

    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.

    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.

  29. simolus3 commented on Oct 22, 2026

    @simolus3

    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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions