jeffparsons opened issue #13788:
I have a use case where I want to store collections of canonical-ABI-shaped component values in host memory, and then pass them in bulk to guest component functions.
Crucially, the host has no static knowledge of the component model types involved. The relevant WIT/component types are discovered at runtime. The host also never directly constructs the individual elements in a type-aware way: they are produced by guests that do have static knowledge of those component model types, then stored by the host as opaque bytes associated with reflected component-model type information.
Right now, as far as I can tell, there is no component-oriented way to dynamically call a guest function with an argument like “a
list<T>whose elements are already available as contiguous canonical-ABI-layout bytes,” without recursively constructing something likeVal::List(Vec<Val>)and paying per-element dynamic lowering overhead.What I’m imagining is a Wasmtime API, either as a new kind of dynamic
callfunction or as a special dynamic value representation, that would support bulk lowering for a restricted case:
- the argument is a
list<T>;Tis known through Wasmtime’s reflected component type information;- Wasmtime verifies that
Tis safe to bulk-copy in this way, for example because it has a fixed canonical ABI layout and contains no transitive strings, lists, resources, handles, or other pointer/ownership-bearing values;- Wasmtime computes the element size/stride/alignment from its own canonical ABI metadata;
- the source byte slice length is checked against the computed element stride; and
- Wasmtime allocates/copies the data into the callee’s linear memory using the normal component/canonical-ABI machinery.
The goal is not to expose an unchecked “raw bytes can be any component value” escape hatch. I’m looking for a checked fast path for the flat/fixed-size case, where the current dynamic
ValAPI is semantically appropriate but forces avoidable per-element boxing/lowering work for large lists.Would the Wasmtime project be open to supporting this use case in some form?
This seems related to broader requests for lower-overhead dynamic component calls, but the intended scope here is narrower than a fully unchecked component-call API: only bulk lowering/lifting for
list<T>whereTis a reflected, fixed-size, canonical-ABI-flat element type.
jeffparsons edited issue #13788:
I have a use case where I want to store collections of canonical-ABI-shaped component values in host memory, and then pass them in bulk to guest component functions.
Crucially, the host has no static knowledge of the component model types involved. The relevant WIT/component types are discovered at runtime. The host also never directly constructs the individual elements in a type-aware way: they are produced by guests that do have static knowledge of those component model types, then stored by the host as opaque bytes associated with reflected component-model type information.
Right now, as far as I can tell, there is no component-oriented way to dynamically call a guest function with an argument like “a
list<T>whose elements are already available as contiguous canonical-ABI-layout bytes,” without recursively constructing something likeVal::List(Vec<Val>)and paying per-element dynamic lowering overhead.What I’m imagining is a Wasmtime API, either as a new kind of dynamic
callfunction or as a special dynamic value representation, that would support bulk lowering for a restricted case:
- the argument is a
list<T>;Tis known through Wasmtime’s reflected component type information;- Wasmtime verifies that
Tis safe to bulk-copy in this way, for example because it has a fixed canonical ABI layout and contains no transitive strings, lists, resources, handles, or other pointer/ownership-bearing values;- Wasmtime computes the element size/stride/alignment from its own canonical ABI metadata;
- the source byte slice length is checked against the computed element stride; and
- Wasmtime allocates/copies the data into the callee’s linear memory using the normal component/canonical-ABI machinery.
The goal is not to expose an unchecked “raw bytes can be any component value” escape hatch. I’m looking for a checked fast path for the flat/fixed-size case, where the current dynamic
ValAPI is semantically appropriate but forces avoidable per-element boxing/lowering work for large lists.Would the Wasmtime project be open to supporting this use case in some form?
This seems related to broader requests for lower-overhead dynamic component calls, but the intended scope here is narrower than a fully unchecked component-call API: only bulk lowering/lifting for
list<T>whereTis a reflected, fixed-size, canonical-ABI-flat element type.Edit: My specific use case is about trying to make a Wasm Component native archetypal ECS, but I wonder if there might be other, less "fringe" use cases for such a mechanism.
jeffparsons edited issue #13788:
I have a use case where I want to store collections of canonical-ABI-shaped component values in host memory, and then pass them in bulk to guest component functions.
Crucially, the host has no static knowledge of the component model types involved. The relevant WIT/component types are discovered at runtime. The host also never directly constructs the individual elements in a type-aware way: they are produced by guests that do have static knowledge of those component model types, then stored by the host as opaque bytes associated with reflected component-model type information.
Right now, as far as I can tell, there is no component-oriented way to dynamically call a guest function with an argument like “a
list<T>whose elements are already available as contiguous canonical-ABI-layout bytes,” without recursively constructing something likeVal::List(Vec<Val>)and paying per-element dynamic lowering overhead.What I’m imagining is a Wasmtime API, either as a new kind of dynamic
callfunction or as a special dynamic value representation, that would support bulk lowering for a restricted case:
- the argument is a
list<T>;Tis known through Wasmtime’s reflected component type information;- Wasmtime verifies that
Tis safe to bulk-copy in this way, for example because it has a fixed canonical ABI layout and contains no transitive strings, lists, resources, handles, or other pointer/ownership-bearing values;- Wasmtime computes the element size/stride/alignment from its own canonical ABI metadata;
- the source byte slice length is checked against the computed element stride; and
- Wasmtime allocates/copies the data into the callee’s linear memory using the normal component/canonical-ABI machinery.
The goal is not to expose an unchecked “raw bytes can be any component value” escape hatch. I’m looking for a checked fast path for the flat/fixed-size case, where the current dynamic
ValAPI is semantically appropriate but forces avoidable per-element boxing/lowering work for large lists.Would the Wasmtime project be open to supporting this use case in some form?
This seems related to broader requests for lower-overhead dynamic component calls, but the intended scope here is narrower than a fully unchecked component-call API: only bulk lowering/lifting for
list<T>whereTis a reflected, fixed-size, canonical-ABI-flat element type.Edit: My specific use case is about trying to make a Wasm Component native archetypal ECS, but I wonder if there might be other, less "fringe" use cases for such a mechanism.
alexcrichton added the wasm-proposal:component-model label to Issue #13788.
alexcrichton commented on issue #13788:
This is definitely something I'd be interested in supporting in Wasmtime! I've long lamented how the dynamic path in Wasmtime is basically too inefficient for a lot of use cases like yours. Historically though I've not had any good ideas about how to expose something that's a mid-point between "you do everything yourself" and the current API. I agree that it'd be quite desirable to have a middle-ground of checked-with-a-fast-path or something similar.
If you've got ideas of what this might look like to add to Wasmtime though I'd be happy to discuss it here and/or review a PR!
jeffparsons commented on issue #13788:
I've done some brainstorming over here (warning/disclosure: heavy Claude use inside) and came up with a design for an embedder calling into a guest component instance that I think offers a decent amount of flexibility in how arguments are provided and return values are received. The full rationale — a cost breakdown, borrow-safety "proofs", and the API sketch — is in
call-builder-sim/DESIGN.md.First,
component::Funcwould grow a newprepare_callmethod that accepts a specification of how each argument will be provided and (assuming success) returns aPreparedCallobject that can be used for multiple calls. I'm proposing that this be a separate first step so that you can front-load validation that the user's nominated kind of source for each argument is actually compatible with that guest function parameter. (More on validation below.)When you're ready to bind concrete arguments for a single invocation of the guest function, you would then call
PreparedCall::bindto get aBoundCallobject. You'd then repeatedly callBoundCall::arg(or a typed convenience likearg_flat) to bind a specific source for each argument, and finally callBoundCall::invoketo perform the call and take ownership of the results. There's alsoBoundCall::invoke_scoped, which instead hands a closure a borrowed view into the results (and both have_asynccounterparts for async calls).Results are read by naming a sink type —
get::<&[u8]>for a zero-copy view,get::<Vec<u8>>for an owned copy, orget::<Val>for a dynamic value — the mirror of naming a source when providing;view/copy/valare sugar for those.This proposal covers only the host-calls-guest direction. I also have ideas for the mirror — functions the embedder exports for the guest to call — but I'm leaving those for later to focus on this direction first.
Here's what the new API surface might look like for an embedder calling into a guest:
impl component::Type { pub fn is_cabi_inline(&self) -> bool; } pub trait Producer { fn next_chunk(&mut self) -> Option<&[u8]>; } #[non_exhaustive] pub enum ArgSpec { Val, Flat, Stream, TypedBuffer, Lowerable, } #[non_exhaustive] pub enum ArgSource<'a> { Val(Val), Flat(&'a [u8]), Stream(&'a mut dyn Producer), TypedBuffer(...), Lowerable(...), } impl component::Func { pub fn prepare_call(&self, store: impl AsContext, args: &[ArgSpec]) -> Result<PreparedCall>; } impl PreparedCall { pub fn bind(&self) -> BoundCall<'_>; } impl<'a> BoundCall<'a> { pub fn arg(self, source: ArgSource<'a>) -> Self; pub fn arg_val(self, value: Val) -> Self; pub fn arg_flat(self, bytes: &'a [u8]) -> Self; pub fn arg_stream(self, producer: &'a mut dyn Producer) -> Self; pub fn invoke(self, store: impl AsContextMut) -> Result<Results<'static>>; pub fn invoke_scoped<R>( self, store: impl AsContextMut, f: impl FnOnce(&Results<'_>) -> Result<R>, ) -> Result<R>; } pub trait FromResult<'a>: Sized { fn from_result(results: &'a Results<'_>, index: usize) -> Result<Self>; } impl<'a> Results<'a> { pub fn len(&self) -> usize; pub fn get<'r, T: FromResult<'r>>(&'r self, index: usize) -> Result<T>; pub fn view(&self, index: usize) -> Result<&[u8]>; pub fn copy(&self, index: usize) -> Result<Vec<u8>>; pub fn val(&self, index: usize) -> Result<Val>; }
Val,Flat, andStream(which models the CMstream<T>) are the worked-out sources;Buffer(a slice of a host-side typed buffer, for collecting results out of one instance and replaying them into others) andLowerable(anyT: Lower, i.e. direct bindgen interop) are elided with...here just to show that the enum is meant to grow — it's#[non_exhaustive]for exactly that reason.On validation:
prepare_callchecks, once, that the number ofArgSpecs matches the function's arity and that each nominated source is compatible with its parameter — e.g. aFlat(pre-encoded as cABI) source is accepted only when the parameter's element type has a fixed cABI layout with no out-of-line storage or ownership (Type::is_cabi_inline), so the fast path can be a plainmemcpy. Structural mismatches like these fail atprepare_call, so the hotinvokepath doesn't re-check them; only data-dependent problems (for example, a byte buffer whose length isn't a whole number of elements) surface atinvoke.With this an embedder can even, for example, receive a large list of values of some specific component model type (of which the embedder has no static knowledge) returned from one component instance, and hand it straight to another — copying the list's bytes just once, directly from one instance's linear memory into the other's:
let make_items = source.prepare_call(&store_a, &[])?; let consume_items = sink.prepare_call(&store_b, &[ArgSpec::Flat])?; make_items.bind().invoke_scoped(&mut store_a, |produced| { consume_items .bind() .arg_flat(produced.view(0)?) .invoke(&mut store_b)?; Ok(()) })?;How's that sound? Is there anything that jumps out to you as obviously bad or incompatible with the way Wasmtime actually works vs. my mental model?
jeffparsons edited a comment on issue #13788:
I've done some brainstorming over here (warning/disclosure: heavy Claude use inside) and came up with a design for an embedder calling into a guest component instance that I think offers a decent amount of flexibility in how arguments are provided and return values are received. The full rationale if you're exceedingly. curious — a cost breakdown, borrow-safety "proofs", and the API sketch — is in
call-builder-sim/DESIGN.md.First,
component::Funcwould grow a newprepare_callmethod that accepts a specification of how each argument will be provided and (assuming success) returns aPreparedCallobject that can be used for multiple calls. I'm proposing that this be a separate first step so that you can front-load validation that the user's nominated kind of source for each argument is actually compatible with that guest function parameter. (More on validation below.)When you're ready to bind concrete arguments for a single invocation of the guest function, you would then call
PreparedCall::bindto get aBoundCallobject. You'd then repeatedly callBoundCall::arg(or a typed convenience likearg_flat) to bind a specific source for each argument, and finally callBoundCall::invoketo perform the call and take ownership of the results. There's alsoBoundCall::invoke_scoped, which instead hands a closure a borrowed view into the results (and both have_asynccounterparts for async calls).Results are read by naming a sink type —
get::<&[u8]>for a zero-copy view,get::<Vec<u8>>for an owned copy, orget::<Val>for a dynamic value — the mirror of naming a source when providing;view/copy/valare sugar for those.This proposal covers only the host-calls-guest direction. I also have ideas for the mirror — functions the embedder exports for the guest to call — but I'm leaving those for later to focus on this direction first.
Here's what the new API surface might look like for an embedder calling into a guest:
impl component::Type { pub fn is_cabi_inline(&self) -> bool; } pub trait Producer { fn next_chunk(&mut self) -> Option<&[u8]>; } #[non_exhaustive] pub enum ArgSpec { Val, Flat, Stream, TypedBuffer, Lowerable, } #[non_exhaustive] pub enum ArgSource<'a> { Val(Val), Flat(&'a [u8]), Stream(&'a mut dyn Producer), TypedBuffer(...), Lowerable(...), } impl component::Func { pub fn prepare_call(&self, store: impl AsContext, args: &[ArgSpec]) -> Result<PreparedCall>; } impl PreparedCall { pub fn bind(&self) -> BoundCall<'_>; } impl<'a> BoundCall<'a> { pub fn arg(self, source: ArgSource<'a>) -> Self; pub fn arg_val(self, value: Val) -> Self; pub fn arg_flat(self, bytes: &'a [u8]) -> Self; pub fn arg_stream(self, producer: &'a mut dyn Producer) -> Self; pub fn invoke(self, store: impl AsContextMut) -> Result<Results<'static>>; pub fn invoke_scoped<R>( self, store: impl AsContextMut, f: impl FnOnce(&Results<'_>) -> Result<R>, ) -> Result<R>; } pub trait FromResult<'a>: Sized { fn from_result(results: &'a Results<'_>, index: usize) -> Result<Self>; } impl<'a> Results<'a> { pub fn len(&self) -> usize; pub fn get<'r, T: FromResult<'r>>(&'r self, index: usize) -> Result<T>; pub fn view(&self, index: usize) -> Result<&[u8]>; pub fn copy(&self, index: usize) -> Result<Vec<u8>>; pub fn val(&self, index: usize) -> Result<Val>; }
Val,Flat, andStream(which models the CMstream<T>) are the worked-out sources;Buffer(a slice of a host-side typed buffer, for collecting results out of one instance and replaying them into others) andLowerable(anyT: Lower, i.e. direct bindgen interop) are elided with...here just to show that the enum is meant to grow — it's#[non_exhaustive]for exactly that reason.On validation:
prepare_callchecks, once, that the number ofArgSpecs matches the function's arity and that each nominated source is compatible with its parameter — e.g. aFlat(pre-encoded as cABI) source is accepted only when the parameter's element type has a fixed cABI layout with no out-of-line storage or ownership (Type::is_cabi_inline), so the fast path can be a plainmemcpy. Structural mismatches like these fail atprepare_call, so the hotinvokepath doesn't re-check them; only data-dependent problems (for example, a byte buffer whose length isn't a whole number of elements) surface atinvoke.With this an embedder can even, for example, receive a large list of values of some specific component model type (of which the embedder has no static knowledge) returned from one component instance, and hand it straight to another — copying the list's bytes just once, directly from one instance's linear memory into the other's:
let make_items = source.prepare_call(&store_a, &[])?; let consume_items = sink.prepare_call(&store_b, &[ArgSpec::Flat])?; make_items.bind().invoke_scoped(&mut store_a, |produced| { consume_items .bind() .arg_flat(produced.view(0)?) .invoke(&mut store_b)?; Ok(()) })?;How's that sound? Is there anything that jumps out to you as obviously bad or incompatible with the way Wasmtime actually works vs. my mental model?
jeffparsons edited a comment on issue #13788:
I've done some brainstorming over here (warning/disclosure: heavy Claude use inside) and came up with a design for an embedder calling into a guest component instance that I think offers a decent amount of flexibility in how arguments are provided and return values are received. The full rationale if you're exceedingly curious — a cost breakdown, borrow-safety "proofs", and the API sketch — is in
call-builder-sim/DESIGN.md.First,
component::Funcwould grow a newprepare_callmethod that accepts a specification of how each argument will be provided and (assuming success) returns aPreparedCallobject that can be used for multiple calls. I'm proposing that this be a separate first step so that you can front-load validation that the user's nominated kind of source for each argument is actually compatible with that guest function parameter. (More on validation below.)When you're ready to bind concrete arguments for a single invocation of the guest function, you would then call
PreparedCall::bindto get aBoundCallobject. You'd then repeatedly callBoundCall::arg(or a typed convenience likearg_flat) to bind a specific source for each argument, and finally callBoundCall::invoketo perform the call and take ownership of the results. There's alsoBoundCall::invoke_scoped, which instead hands a closure a borrowed view into the results (and both have_asynccounterparts for async calls).Results are read by naming a sink type —
get::<&[u8]>for a zero-copy view,get::<Vec<u8>>for an owned copy, orget::<Val>for a dynamic value — the mirror of naming a source when providing;view/copy/valare sugar for those.This proposal covers only the host-calls-guest direction. I also have ideas for the mirror — functions the embedder exports for the guest to call — but I'm leaving those for later to focus on this direction first.
Here's what the new API surface might look like for an embedder calling into a guest:
impl component::Type { pub fn is_cabi_inline(&self) -> bool; } pub trait Producer { fn next_chunk(&mut self) -> Option<&[u8]>; } #[non_exhaustive] pub enum ArgSpec { Val, Flat, Stream, TypedBuffer, Lowerable, } #[non_exhaustive] pub enum ArgSource<'a> { Val(Val), Flat(&'a [u8]), Stream(&'a mut dyn Producer), TypedBuffer(...), Lowerable(...), } impl component::Func { pub fn prepare_call(&self, store: impl AsContext, args: &[ArgSpec]) -> Result<PreparedCall>; } impl PreparedCall { pub fn bind(&self) -> BoundCall<'_>; } impl<'a> BoundCall<'a> { pub fn arg(self, source: ArgSource<'a>) -> Self; pub fn arg_val(self, value: Val) -> Self; pub fn arg_flat(self, bytes: &'a [u8]) -> Self; pub fn arg_stream(self, producer: &'a mut dyn Producer) -> Self; pub fn invoke(self, store: impl AsContextMut) -> Result<Results<'static>>; pub fn invoke_scoped<R>( self, store: impl AsContextMut, f: impl FnOnce(&Results<'_>) -> Result<R>, ) -> Result<R>; } pub trait FromResult<'a>: Sized { fn from_result(results: &'a Results<'_>, index: usize) -> Result<Self>; } impl<'a> Results<'a> { pub fn len(&self) -> usize; pub fn get<'r, T: FromResult<'r>>(&'r self, index: usize) -> Result<T>; pub fn view(&self, index: usize) -> Result<&[u8]>; pub fn copy(&self, index: usize) -> Result<Vec<u8>>; pub fn val(&self, index: usize) -> Result<Val>; }
Val,Flat, andStream(which models the CMstream<T>) are the worked-out sources;Buffer(a slice of a host-side typed buffer, for collecting results out of one instance and replaying them into others) andLowerable(anyT: Lower, i.e. direct bindgen interop) are elided with...here just to show that the enum is meant to grow — it's#[non_exhaustive]for exactly that reason.On validation:
prepare_callchecks, once, that the number ofArgSpecs matches the function's arity and that each nominated source is compatible with its parameter — e.g. aFlat(pre-encoded as cABI) source is accepted only when the parameter's element type has a fixed cABI layout with no out-of-line storage or ownership (Type::is_cabi_inline), so the fast path can be a plainmemcpy. Structural mismatches like these fail atprepare_call, so the hotinvokepath doesn't re-check them; only data-dependent problems (for example, a byte buffer whose length isn't a whole number of elements) surface atinvoke.With this an embedder can even, for example, receive a large list of values of some specific component model type (of which the embedder has no static knowledge) returned from one component instance, and hand it straight to another — copying the list's bytes just once, directly from one instance's linear memory into the other's:
let make_items = source.prepare_call(&store_a, &[])?; let consume_items = sink.prepare_call(&store_b, &[ArgSpec::Flat])?; make_items.bind().invoke_scoped(&mut store_a, |produced| { consume_items .bind() .arg_flat(produced.view(0)?) .invoke(&mut store_b)?; Ok(()) })?;How's that sound? Is there anything that jumps out to you as obviously bad or incompatible with the way Wasmtime actually works vs. my mental model?
jeffparsons commented on issue #13788:
I just realised that the inline-representable check is inadequate because not all bit patterns are valid for all types, e.g.
char.I also need to have a bit more of a think about the consequences of lazy value lowering, assuming that comes along eventually; it _could_ be accommodated by the API I've proposed, but I wonder if there's a more elegant API that could somehow do something more for non-inline-representable types...
jeffparsons edited a comment on issue #13788:
I just realised that the inline-representable check is inadequate because not all bit patterns are valid for all types, e.g.
char. So that concept needs a bit more nuance.I also need to have a bit more of a think about the consequences of lazy value lowering, assuming that comes along eventually; it _could_ be accommodated by the API I've proposed, but I wonder if there's a more elegant API that could somehow do something more for non-inline-representable types...
jeffparsons commented on issue #13788:
Quick progress update — I've got a working implementation of the host→guest direction on a branch:
jeffparsons/wasmtime@component-prepare-call(main filecrates/wasmtime/src/runtime/component/func/prepared.rs, tests intests/all/component_model/prepared.rs). It's built up in a few small commits and the wholecomponent_model::suite passes. Not a PR yet — I'd welcome a sanity check on the surface and one design question before I polish it into one.What's there
Func::prepare_call(store, &[ArgSpec]) -> PreparedCall→.bind()→BoundCall::arg(ArgSource)→invoke(&mut [Val])orinvoke_scoped(|&Results| …).ArgSource::Flat(&[u8])lowers alist<T>from a borrowed, pre-encoded canonical image as a singlememcpy(handling both the flat-params and the indirect/store_argsparameter layouts);ArgSource::Val(Val)for anything else; the two mix freely per argument. Both enums are#[non_exhaustive].invoke_scopedhands back a borrowedResultsaccessor offering, per result,view(zero-copy&[u8]),copy(Vec<u8>), orval(Val). Borrowed views are confined to the closure by the borrow checker.- Reuses
call_raw/lift_resultsunchanged; results still lift through the existing path.One refinement worth flagging
I split the reflection gate into two predicates:
Type::is_cabi_inline()— fixed canonical layout with no out-of-line storage or ownership (so alist<T>is contiguous). Includesbool/char/enum/flags/option/result/variant/record/tuple of inline types.Type::are_all_bit_patterns_valid()— every bit pattern of the canonical image is a valid value: the integers, floats, and records/tuples of them.The unchecked
Flatmemcpy and the zero-copyviewgate on the stricterare_all_bit_patterns_valid, so e.g.list<bool>is rejected (a byte ≠ 0/1 would be an invalidbool). A future validated bulk path could use the broaderis_cabi_inlineplus a one-time validation sweep. Does that division match how you'd want it named/factored?Design question: does a raw-bytes argument source need to be an
unsafetrait?Since the bytes land in sandboxed guest linear memory and the gate excludes every pointer/handle/ownership-bearing type, the worst a "wrong"
Flat(or future streaming) source can do — for the gated types, where all bit patterns are valid — is hand the guest a semantically-arbitrary but well-typed value: a logic bug, not host UB. So my read is that the provide side can stay a safe API, withare_all_bit_patterns_validas the safety boundary; a trait would only needunsafeif we added an escape hatch that skipped the gate for arbitrary types. Does that match your model of the invariants Wasmtime relies on (e.g. that it always re-validates guest-provided handle indices and list bounds on lift, so nothing here can forge those)?Deferred (named, not blocking)
bool/char/enum/flagsvia a validation sweep; asyncinvoke;Stream/ typed-buffer /Lowerargument sources; and the guest→host (import) side.Happy to adjust any of the surface before turning this into a PR.
jeffparsons deleted a comment on issue #13788:
Quick progress update — I've got a working implementation of the host→guest direction on a branch:
jeffparsons/wasmtime@component-prepare-call(main filecrates/wasmtime/src/runtime/component/func/prepared.rs, tests intests/all/component_model/prepared.rs). It's built up in a few small commits and the wholecomponent_model::suite passes. Not a PR yet — I'd welcome a sanity check on the surface and one design question before I polish it into one.What's there
Func::prepare_call(store, &[ArgSpec]) -> PreparedCall→.bind()→BoundCall::arg(ArgSource)→invoke(&mut [Val])orinvoke_scoped(|&Results| …).ArgSource::Flat(&[u8])lowers alist<T>from a borrowed, pre-encoded canonical image as a singlememcpy(handling both the flat-params and the indirect/store_argsparameter layouts);ArgSource::Val(Val)for anything else; the two mix freely per argument. Both enums are#[non_exhaustive].invoke_scopedhands back a borrowedResultsaccessor offering, per result,view(zero-copy&[u8]),copy(Vec<u8>), orval(Val). Borrowed views are confined to the closure by the borrow checker.- Reuses
call_raw/lift_resultsunchanged; results still lift through the existing path.One refinement worth flagging
I split the reflection gate into two predicates:
Type::is_cabi_inline()— fixed canonical layout with no out-of-line storage or ownership (so alist<T>is contiguous). Includesbool/char/enum/flags/option/result/variant/record/tuple of inline types.Type::are_all_bit_patterns_valid()— every bit pattern of the canonical image is a valid value: the integers, floats, and records/tuples of them.The unchecked
Flatmemcpy and the zero-copyviewgate on the stricterare_all_bit_patterns_valid, so e.g.list<bool>is rejected (a byte ≠ 0/1 would be an invalidbool). A future validated bulk path could use the broaderis_cabi_inlineplus a one-time validation sweep. Does that division match how you'd want it named/factored?Design question: does a raw-bytes argument source need to be an
unsafetrait?Since the bytes land in sandboxed guest linear memory and the gate excludes every pointer/handle/ownership-bearing type, the worst a "wrong"
Flat(or future streaming) source can do — for the gated types, where all bit patterns are valid — is hand the guest a semantically-arbitrary but well-typed value: a logic bug, not host UB. So my read is that the provide side can stay a safe API, withare_all_bit_patterns_validas the safety boundary; a trait would only needunsafeif we added an escape hatch that skipped the gate for arbitrary types. Does that match your model of the invariants Wasmtime relies on (e.g. that it always re-validates guest-provided handle indices and list bounds on lift, so nothing here can forge those)?Deferred (named, not blocking)
bool/char/enum/flagsvia a validation sweep; asyncinvoke;Stream/ typed-buffer /Lowerargument sources; and the guest→host (import) side.Happy to adjust any of the surface before turning this into a PR.
jeffparsons commented on issue #13788:
UPDATE: I'm working on a "v2" of this proposal, which switches from deciding lowering strategy at the argument level to a "value source" tree that allows freely mixing different kinds of value sources for a single argument. E.g. an argument struct might contain both an array where the elements are all inline-representable and where all bit patterns are valid values, and _also_ something like a resource handle that can't be blindly memcpy'd. It's basically a fancy version of the existing "fully dynamic"
Valtree, where you can choose for individual nodes where the data comes from.I have a few kinks to work out before I propose a concrete API surface, but it feels promising so far in that it manages to give the embedder heaps of flexibility in how values are lowered, lets you copy already-validated bytes from a guest into owned buffers for later use _or_ forward them directly to another component instance (as long as it's in a different Store), and IINM should play really nicely with lazy value lowering if/when that comes along.
alexcrichton commented on issue #13788:
This overall looks pretty reasonable to me, but I've found the devil's often in the details for changes like this so it's hard to provide a super-solid opinion before reviewing a PR (which is fine by me, but want to say so as well). One thing I'd personally like to take on as a constraint though is to avoid adding another side method of invoking component functions compared to what we already have. Lowering and such is pretty critical to both correctness and safety and so I want to make sure the critical surface area in Wasmtime stays minimized.
To that extent my ideal for a system like this would be to build the currently-existing
Func::callon top of it. Another example could be thatValfits in as "one way to use the system" but it could be used in any number of other ways if so desired. Basically I'm happy keepingVal, but I'd prefer to not have 3 ways to call functions (Val-full-dynamic, this way, andTypedFunc-full-static). How exactly to achieve this, if it is even achievable, I'm not entirely sure, but it's a personal goal of mine at least.
jeffparsons commented on issue #13788:
Thanks for the feedback!
One thing I'd personally like to take on as a constraint though is to avoid adding another side method of invoking component functions compared to what we already have. Lowering and such is pretty critical to both correctness and safety and so I want to make sure the critical surface area in Wasmtime stays minimized.
The new implementation could become the foundation and the existing set of
Valbased methods could become a layer over the top of those. I have an implementation written entirely by Claude that does just this. I'm not necessarily suggesting that you should even look at this as-is, but I thought it would be worth making sure that I can at least throw the design at Claude and make it produce something that builds and passes tests. If you decide you want something like this API design, then I'd propose to deliver it as a series of much, _much_ smaller PRs in which I could honestly say I vouch for every line of code, even if Claude was still involved. (Honestly, that's the only way I'd realistically find time to deliver anything on this.)The gist of "v2" of my proposal is to not choose a representation _per argument_, but to provide each argument as a tree where each node independently choose how it will provide bytes to the guest.
#[non_exhaustive] pub enum ValSource<'a> { Val(Val), // dynamic + owned — the always-available leaf Flat(ValidatedCabiBytes<'a>), // pre-encoded canonical bytes; one memcpy Record(Vec<ValSource<'a>>), // per-field sources — mix representations within one value List(ListSource<'a>), // Flat(ValidatedCabiBytes) | Elems(Vec<ValSource>) // later, additively: Lazy (a trait-object leaf pulled on demand, dovetailing with // CM lazy value lowering), Stream, resource nodes, ... }Perhaps worth noting what this design keeps _out_ of Wasmtime: because
Flatbytes are
proof-carrying (you can't make aValidatedCabiByteswithout someone proving they're valid), most fancier machinery can be provided by another crate. E.g. a pre-validated host arena that collects results from one instance and forwards slices of them on into many others is just a container of
ValidatedCabiBytes. Wasmtime _could_ ship one as a helper, but doesn't have to; and
aLazyleaf would let embedders supply their own on-demand producers (say, a strided
column gather) without Wasmtime knowing anything about them. The runtime only ever
owns the checked lowering driver; third parties extend at the leaves. (I'm thinking about my original "Wasm Component native archetypal ECS" use case here.)The rest of the API, which mostly falls out of what the
ValSourcetree needs to support it:
Func::prepare_call(&store, &[ValSpec])validates the shape once (aValSpectree mirrorsValSourcewithout data):Flatonly where the type has a fixed inline layout, arities match, etc. Reused across many calls.prepared.bind().arg(src)...arg(src).invoke(&mut store)binds concrete sources for one invocation; borrows live on the tree and are released wheninvokeconsumes it.invoke_scoped(store, |results| ...)is the zero-copy read variant (post-return runs after the closure, so views can't escape).ValidatedCabiBytesis a proof-carrying wrapper: constructing it validates the bytes against a reflectedType— O(1) when every bit pattern of the type is valid (ints, floats, records thereof), a linear sweep otherwise (bool/char/enum/flags/variant discriminants). Two new reflection predicates support this:Type::is_cabi_inline()(fixed layout, no out-of-line storage or ownership — makes a memcpy memory-safe) andType::are_all_bit_patterns_valid()(makes it sufficient, no sweep needed). Bytes read back from a guest can be re-wrapped cheaply, which is what makes the guest -> host -> guest conduit a single validated copy.I can't think of anything that would substantially beat this for speed without sacrificing safety.
jeffparsons edited a comment on issue #13788:
Thanks for the feedback!
One thing I'd personally like to take on as a constraint though is to avoid adding another side method of invoking component functions compared to what we already have. Lowering and such is pretty critical to both correctness and safety and so I want to make sure the critical surface area in Wasmtime stays minimized.
The new implementation could become the foundation and the existing set of
Valbased methods could become a layer over the top of those. I have an implementation written entirely by Claude that does just this. I'm not necessarily suggesting that you should even look at this as-is, but I thought it would be worth making sure that I can at least throw the design at Claude and make it produce something that builds and passes tests. If you decide you want something like this API design, then I'd propose to deliver it as a series of much, _much_ smaller PRs in which I could honestly say I vouch for every line of code, even if Claude was still involved. (Honestly, that's the only way I'd realistically find time to deliver anything on this.)The gist of "v2" of my proposal is to not choose a representation _per argument_, but to provide each argument as a tree where each node independently choose how it will provide bytes to the guest.
#[non_exhaustive] pub enum ValSource<'a> { Val(Val), // dynamic + owned — the always-available leaf Flat(ValidatedCabiBytes<'a>), // pre-encoded canonical bytes; one memcpy Record(Vec<ValSource<'a>>), // per-field sources — mix representations within one value List(ListSource<'a>), // Flat(ValidatedCabiBytes) | Elems(Vec<ValSource>) // later, additively: Lazy (a trait-object leaf pulled on demand, dovetailing with // CM lazy value lowering), Stream, resource nodes, ... }Perhaps worth noting what this design keeps _out_ of Wasmtime: because
Flatbytes are proof-carrying (you can't make aValidatedCabiByteswithout someone proving they're valid), most fancier machinery can be provided by another crate. E.g. a pre-validated host arena that collects results from one instance and forwards slices of them on into many others is just a container ofValidatedCabiBytes. Wasmtime _could_ ship one as a helper, but doesn't have to; and aLazyleaf would let embedders supply their own on-demand producers (say, a strided column gather) without Wasmtime knowing anything about them. The runtime only ever owns the checked lowering driver; third parties extend at the leaves. (I'm thinking about my original "Wasm Component native archetypal ECS" use case here.)The rest of the API, which mostly falls out of what the
ValSourcetree needs to support it:
Func::prepare_call(&store, &[ValSpec])validates the shape once (aValSpectree mirrorsValSourcewithout data):Flatonly where the type has a fixed inline layout, arities match, etc. Reused across many calls.prepared.bind().arg(src)...arg(src).invoke(&mut store)binds concrete sources for one invocation; borrows live on the tree and are released wheninvokeconsumes it.invoke_scoped(store, |results| ...)is the zero-copy read variant (post-return runs after the closure, so views can't escape).ValidatedCabiBytesis a proof-carrying wrapper: constructing it validates the bytes against a reflectedType— O(1) when every bit pattern of the type is valid (ints, floats, records thereof), a linear sweep otherwise (bool/char/enum/flags/variant discriminants). Two new reflection predicates support this:Type::is_cabi_inline()(fixed layout, no out-of-line storage or ownership — makes a memcpy memory-safe) andType::are_all_bit_patterns_valid()(makes it sufficient, no sweep needed). Bytes read back from a guest can be re-wrapped cheaply, which is what makes the guest -> host -> guest conduit a single validated copy.I can't think of anything that would substantially beat this for speed without sacrificing safety.
alexcrichton commented on issue #13788:
At a high-level that all sounds reasonable to me, yeah. I agree that Claude/LLMs are a good fit for a refactoring like this, and reimplementing
Valon top of something else I think is also reasonable to do.
Last updated: Jul 29 2026 at 05:03 UTC