You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This issue lifts the inlined accessor pattern proposed in #626 to its broader context, exploring how it fits together with other proposals currently in flight to complete a zero-copy shared-interface cross-language object-model.
The cross-language object-model problem
A producer holds objects in some language-specific representation — Java instances, OCaml records, JS hidden-class objects, Lua tables, Rust structs. A consumer in a possibly different language wants to access state on those objects. The Canonical ABI today copies values across the boundary, which is correct under shared-nothing but expensive when the values are non-trivial. Two situations make the copy especially costly:
Producer's layout is fixed by source semantics. JS hidden classes, Lua tables, Python instances. The compiler can't conform the at-rest representation to whatever shape the canonical ABI expects without changing the language's runtime model.
Producer and consumer have different layouts even when each could control its own. A Java producer and an OCaml consumer both compile to Wasm GC, both pick at-rest representations idiomatic for their language, and the representations don't structurally match.
Layering two complementary strategies
The proposals currently in flight cover this through two strategies organized along the layout-aligned vs. layout-mismatched axis:
Typed reference passing — for the layout-aligned case: data is exposed as a record or list plus associated operations as free functions in the same interface, and #525's lowering passes the data as a typed (ref $T) reference. Field access on the consumer side compiles directly to struct.get and struct.set — no method calls, no inlining required. (the current pre-proposal form is primarily scoped to records and lists with structural Wasm GC correspondence; variants, strings, and nested mutable cases remain partly open.)
Accessor-based passing — for the layout-mismatched case: data and associated operations are bundled into a resource with methods on it. The consumer accesses everything through method calls on an externref. With monomorphic dispatch and inlining, accessor calls reduce to direct memory operations on the producer's at-rest representation, maintaining isolation with access and ownership contracts and static or dynamic bound checks in case of indexed access.
The two strategies aren't competing — they answer different questions, and the choice between them combines two orthogonal dimensions: a modeling decision (separate record + free functions vs. nominal resource bundling state and methods) and a layout decision (whether the producer's at-rest representation aligns with the structural representation the consumer expects). The two strategies also sit at different levels of abstraction: typed reference passing is a concrete lowering for the layout-aligned sub-case, while accessor-based passing is a general lowering whose accessor bodies can wrap arbitrary internal representations — including a typed reference passed via #525 where applicable. Typed reference passing achieves zero-copy when both modeling and layout fit; accessor-based passing achieves zero-copy whenever the resource model fits, regardless of layout alignment, because the layout is hidden behind the accessor interface.
In practice, resource-with-accessors is the right choice when either dimension favors it: when the producer's representation is fixed by source semantics (JS hidden classes, Lua tables, Python instances), when layouts differ between sides and can't be aligned, or when the producer's interface design naturally bundles state with operations and benefits from encapsulation.
Four layers slicing the concerns
Looking at it, there are four layers answering one question, each composable independently:
Either typed reference passing for layout-aligned, or accessor calls for layout-mismatched
The interesting observation: each layer has one or two clear answers among existing/in-flight proposals, and they compose freely. A record with borrow<T> and #525-style typed reference passing could be one valid combination. A resource with borrow<T> and accessor-based passing is another. The architecture seems already to be there — just not written down as a connected picture.
Concrete example
The same logical image expressed under both strategies — note the symmetry: both define data plus associated operations, but organize them differently.
In Strategy 1, field access on an image reference compiles to direct struct.get / array.get operations under #525's lowering; free functions like blend or resize operate on these references and return new ones, consistent with value-type semantics.
In Strategy 2, each accessor is a method call that, with inlining, reduces to the same direct memory operations — with static or dynamic bounds checks for indexed access (pixel-at, set-pixel-at). What that method body contains is the producer's choice: an automatically generated trivial getter or setter, or a hand-written method performing validation, conversion, or lazy computation — consumers see only the declared interface in either case. The readonly modifier on width and height desugars to a getter only; ownership annotations on parameters (borrow<image>, own<image>) follow the existing WIT semantics for resources. The blit method is an example of a bulk operation — see the next section.
Both forms describe the same logical image but with different modeling commitments — record + free functions favors immutable transformations and value semantics; resource favors identity and in-place mutation behind an interface. The lowering choice (Layer 4) is then determined by whether the producer's at-rest representation can be brought into structural alignment with the consumer (Strategy 1), or whether it's fixed or differs and accessors are needed (Strategy 2).
The layout-mismatched path
The accessor pattern works mechanically today. But the zero-cost property — that an accessor call reduces to a direct struct.get — depends on guaranteed inlining. Wasmtime's recently-added function inliner provides this on the engine side. Browser engines and embedded baseline tiers don't reliably inline cross-instance calls, so the pattern remains hypothetical for those targets.
#626 fills this gap on the toolchain side: at link time, component-linking tools can inline accessor calls into the merged module while preserving inter-module isolation through multi-memory partitioning. Together with engine-side inlining where available, this makes the layout-mismatched strategy deployable across the full target spectrum offering guaranteed inlining of accessors — a cornerstone to make this model portable across the diverse spectrum of runtimes.
Bulk operations
Per-element accessor calls are efficient for scalar access (single field read/write, individual indexed lookup). For bulk operations on many elements — image filters traversing all pixels, string scanning, list aggregations — per-element calls accumulate overhead even after inlining: each call is an indirect jump, vectorization opportunities are lost, and bounds checks repeat per iteration. The accessor-based strategy addresses this through three complementary paths:
Explicit bulk methods on the resource — the producer exposes memcpy-style or batch operations directly on the resource (the blit method in the example above is this kind). With inlining, the body can become a single memory.copy or a tight optimized loop. This works today within existing WIT semantics; the producer chooses which bulk operations to expose.
Region borrows — a typed reference to a bounded memory region that the consumer operates on with normal Wasm bulk operations (memory.copy, SIMD load/store) after a single bounds check at borrow acquisition. This is the direction of @lukewagner's lazy-lowering proposal Lazy value lowering #383 and of Memory mapping in the Component Model #568 (mappableref) — a native primitive for region/buffer borrows that handles bulk-region semantics directly.
Optimizer-recovery from per-element loops — recognizing sequential accessor patterns and lowering them to vectorized or bulk operations. With aggressive inlining (Wasmtime's inliner, toolchain-side merge+wasm-opt) this is in principle possible at the engine level, though dependent on optimizer sophistication. The same link-time module-rewriting mechanism described above could implement this as a dedicated pass — pattern-matching accessor loops and emitting bulk operations deterministically rather than relying on engine heuristics.
The three paths fit different situations: bulk methods when the producer can anticipate which bulk operations matter; region borrows when the consumer needs flexibility on a producer-exposed buffer; optimizer-recovery when neither side anticipated the pattern but the access is sequential.
Open question
The picture above seems coherent: two complementary lowering strategies along the layout-aligned vs. layout-mismatched axis, sitting at different levels of abstraction — typed reference passing as the concrete fast path for layout-aligned cases, accessor-based passing as the general lowering that covers the rest and can wrap typed references where applicable. The four-layer separation makes the composition explicit.
The question worth surfacing: is this an architecture the perspectives represented in the CG align with? If the picture is correct, a non-normative note in the spec or Component Model Explainer would give compiler engineers targeting WIT for new languages a clear map — currently the recipes live in pieces across resource documentation, #525, #626, and #568, and there's no single place that says "here is the cross-language object-model architecture, here is which strategy applies to your language's situation."
This issue lifts the inlined accessor pattern proposed in #626 to its broader context, exploring how it fits together with other proposals currently in flight to complete a zero-copy shared-interface cross-language object-model.
The cross-language object-model problem
A producer holds objects in some language-specific representation — Java instances, OCaml records, JS hidden-class objects, Lua tables, Rust structs. A consumer in a possibly different language wants to access state on those objects. The Canonical ABI today copies values across the boundary, which is correct under shared-nothing but expensive when the values are non-trivial. Two situations make the copy especially costly:
Layering two complementary strategies
The proposals currently in flight cover this through two strategies organized along the layout-aligned vs. layout-mismatched axis:
Typed reference passing — for the layout-aligned case: data is exposed as a
recordorlistplus associated operations as free functions in the same interface, and #525's lowering passes the data as a typed(ref $T)reference. Field access on the consumer side compiles directly tostruct.getandstruct.set— no method calls, no inlining required. (the current pre-proposal form is primarily scoped to records and lists with structural Wasm GC correspondence; variants, strings, and nested mutable cases remain partly open.)Accessor-based passing — for the layout-mismatched case: data and associated operations are bundled into a
resourcewith methods on it. The consumer accesses everything through method calls on an externref. With monomorphic dispatch and inlining, accessor calls reduce to direct memory operations on the producer's at-rest representation, maintaining isolation with access and ownership contracts and static or dynamic bound checks in case of indexed access.The two strategies aren't competing — they answer different questions, and the choice between them combines two orthogonal dimensions: a modeling decision (separate record + free functions vs. nominal resource bundling state and methods) and a layout decision (whether the producer's at-rest representation aligns with the structural representation the consumer expects). The two strategies also sit at different levels of abstraction: typed reference passing is a concrete lowering for the layout-aligned sub-case, while accessor-based passing is a general lowering whose accessor bodies can wrap arbitrary internal representations — including a typed reference passed via #525 where applicable. Typed reference passing achieves zero-copy when both modeling and layout fit; accessor-based passing achieves zero-copy whenever the resource model fits, regardless of layout alignment, because the layout is hidden behind the accessor interface.
In practice, resource-with-accessors is the right choice when either dimension favors it: when the producer's representation is fixed by source semantics (JS hidden classes, Lua tables, Python instances), when layouts differ between sides and can't be aligned, or when the producer's interface design naturally bundles state with operations and benefits from encapsulation.
Four layers slicing the concerns
Looking at it, there are four layers answering one question, each composable independently:
record,list,variant,resource— defining identity, opacity, allowed operationsborrow<T>,own<T>, by-value default — specifying lifetime semanticsThe interesting observation: each layer has one or two clear answers among existing/in-flight proposals, and they compose freely. A
recordwithborrow<T>and #525-style typed reference passing could be one valid combination. Aresourcewithborrow<T>and accessor-based passing is another. The architecture seems already to be there — just not written down as a connected picture.Concrete example
The same logical
imageexpressed under both strategies — note the symmetry: both define data plus associated operations, but organize them differently.In Strategy 1, field access on an
imagereference compiles to directstruct.get/array.getoperations under #525's lowering; free functions likeblendorresizeoperate on these references and return new ones, consistent with value-type semantics.In Strategy 2, each accessor is a method call that, with inlining, reduces to the same direct memory operations — with static or dynamic bounds checks for indexed access (
pixel-at,set-pixel-at). What that method body contains is the producer's choice: an automatically generated trivial getter or setter, or a hand-written method performing validation, conversion, or lazy computation — consumers see only the declared interface in either case. Thereadonlymodifier onwidthandheightdesugars to a getter only; ownership annotations on parameters (borrow<image>,own<image>) follow the existing WIT semantics for resources. Theblitmethod is an example of a bulk operation — see the next section.Both forms describe the same logical
imagebut with different modeling commitments — record + free functions favors immutable transformations and value semantics; resource favors identity and in-place mutation behind an interface. The lowering choice (Layer 4) is then determined by whether the producer's at-rest representation can be brought into structural alignment with the consumer (Strategy 1), or whether it's fixed or differs and accessors are needed (Strategy 2).The layout-mismatched path
The accessor pattern works mechanically today. But the zero-cost property — that an accessor call reduces to a direct
struct.get— depends on guaranteed inlining. Wasmtime's recently-added function inliner provides this on the engine side. Browser engines and embedded baseline tiers don't reliably inline cross-instance calls, so the pattern remains hypothetical for those targets.#626 fills this gap on the toolchain side: at link time, component-linking tools can inline accessor calls into the merged module while preserving inter-module isolation through multi-memory partitioning. Together with engine-side inlining where available, this makes the layout-mismatched strategy deployable across the full target spectrum offering guaranteed inlining of accessors — a cornerstone to make this model portable across the diverse spectrum of runtimes.
Bulk operations
Per-element accessor calls are efficient for scalar access (single field read/write, individual indexed lookup). For bulk operations on many elements — image filters traversing all pixels, string scanning, list aggregations — per-element calls accumulate overhead even after inlining: each call is an indirect jump, vectorization opportunities are lost, and bounds checks repeat per iteration. The accessor-based strategy addresses this through three complementary paths:
Explicit bulk methods on the resource — the producer exposes memcpy-style or batch operations directly on the resource (the
blitmethod in the example above is this kind). With inlining, the body can become a singlememory.copyor a tight optimized loop. This works today within existing WIT semantics; the producer chooses which bulk operations to expose.Region borrows — a typed reference to a bounded memory region that the consumer operates on with normal Wasm bulk operations (
memory.copy, SIMD load/store) after a single bounds check at borrow acquisition. This is the direction of @lukewagner's lazy-lowering proposal Lazy value lowering #383 and of Memory mapping in the Component Model #568 (mappableref) — a native primitive for region/buffer borrows that handles bulk-region semantics directly.Optimizer-recovery from per-element loops — recognizing sequential accessor patterns and lowering them to vectorized or bulk operations. With aggressive inlining (Wasmtime's inliner, toolchain-side merge+wasm-opt) this is in principle possible at the engine level, though dependent on optimizer sophistication. The same link-time module-rewriting mechanism described above could implement this as a dedicated pass — pattern-matching accessor loops and emitting bulk operations deterministically rather than relying on engine heuristics.
The three paths fit different situations: bulk methods when the producer can anticipate which bulk operations matter; region borrows when the consumer needs flexibility on a producer-exposed buffer; optimizer-recovery when neither side anticipated the pattern but the access is sequential.
Open question
The picture above seems coherent: two complementary lowering strategies along the layout-aligned vs. layout-mismatched axis, sitting at different levels of abstraction — typed reference passing as the concrete fast path for layout-aligned cases, accessor-based passing as the general lowering that covers the rest and can wrap typed references where applicable. The four-layer separation makes the composition explicit.
The question worth surfacing: is this an architecture the perspectives represented in the CG align with? If the picture is correct, a non-normative note in the spec or Component Model Explainer would give compiler engineers targeting WIT for new languages a clear map — currently the recipes live in pieces across
resourcedocumentation, #525, #626, and #568, and there's no single place that says "here is the cross-language object-model architecture, here is which strategy applies to your language's situation."