Stream: git-wasmtime

Topic: wasmtime / issue #13788 Support bulk lowering for list<fl...


view this post on Zulip Wasmtime GitHub notifications bot (Jul 01 2026 at 11:20):

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 like Val::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 call function or as a special dynamic value representation, that would support bulk lowering for a restricted case:

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 Val API 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> where T is a reflected, fixed-size, canonical-ABI-flat element type.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 01 2026 at 11:27):

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 like Val::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 call function or as a special dynamic value representation, that would support bulk lowering for a restricted case:

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 Val API 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> where T is 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.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 01 2026 at 11:27):

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 like Val::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 call function or as a special dynamic value representation, that would support bulk lowering for a restricted case:

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 Val API 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> where T is 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.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 01 2026 at 15:02):

alexcrichton added the wasm-proposal:component-model label to Issue #13788.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 01 2026 at 15:06):

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!

view this post on Zulip Wasmtime GitHub notifications bot (Jul 03 2026 at 01:42):

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::Func would grow a new prepare_call method that accepts a specification of how each argument will be provided and (assuming success) returns a PreparedCall object 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::bind to get a BoundCall object. You'd then repeatedly call BoundCall::arg (or a typed convenience like arg_flat) to bind a specific source for each argument, and finally call BoundCall::invoke to perform the call and take ownership of the results. There's also BoundCall::invoke_scoped, which instead hands a closure a borrowed view into the results (and both have _async counterparts for async calls).

Results are read by naming a sink typeget::<&[u8]> for a zero-copy view, get::<Vec<u8>> for an owned copy, or get::<Val> for a dynamic value — the mirror of naming a source when providing; view/copy/val are 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, and Stream (which models the CM stream<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) and Lowerable (any T: 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_call checks, once, that the number of ArgSpecs matches the function's arity and that each nominated source is compatible with its parameter — e.g. a Flat (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 plain memcpy. Structural mismatches like these fail at prepare_call, so the hot invoke path doesn't re-check them; only data-dependent problems (for example, a byte buffer whose length isn't a whole number of elements) surface at invoke.

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?

view this post on Zulip Wasmtime GitHub notifications bot (Jul 03 2026 at 01:42):

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::Func would grow a new prepare_call method that accepts a specification of how each argument will be provided and (assuming success) returns a PreparedCall object 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::bind to get a BoundCall object. You'd then repeatedly call BoundCall::arg (or a typed convenience like arg_flat) to bind a specific source for each argument, and finally call BoundCall::invoke to perform the call and take ownership of the results. There's also BoundCall::invoke_scoped, which instead hands a closure a borrowed view into the results (and both have _async counterparts for async calls).

Results are read by naming a sink typeget::<&[u8]> for a zero-copy view, get::<Vec<u8>> for an owned copy, or get::<Val> for a dynamic value — the mirror of naming a source when providing; view/copy/val are 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, and Stream (which models the CM stream<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) and Lowerable (any T: 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_call checks, once, that the number of ArgSpecs matches the function's arity and that each nominated source is compatible with its parameter — e.g. a Flat (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 plain memcpy. Structural mismatches like these fail at prepare_call, so the hot invoke path doesn't re-check them; only data-dependent problems (for example, a byte buffer whose length isn't a whole number of elements) surface at invoke.

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?

view this post on Zulip Wasmtime GitHub notifications bot (Jul 03 2026 at 01:43):

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::Func would grow a new prepare_call method that accepts a specification of how each argument will be provided and (assuming success) returns a PreparedCall object 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::bind to get a BoundCall object. You'd then repeatedly call BoundCall::arg (or a typed convenience like arg_flat) to bind a specific source for each argument, and finally call BoundCall::invoke to perform the call and take ownership of the results. There's also BoundCall::invoke_scoped, which instead hands a closure a borrowed view into the results (and both have _async counterparts for async calls).

Results are read by naming a sink typeget::<&[u8]> for a zero-copy view, get::<Vec<u8>> for an owned copy, or get::<Val> for a dynamic value — the mirror of naming a source when providing; view/copy/val are 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, and Stream (which models the CM stream<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) and Lowerable (any T: 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_call checks, once, that the number of ArgSpecs matches the function's arity and that each nominated source is compatible with its parameter — e.g. a Flat (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 plain memcpy. Structural mismatches like these fail at prepare_call, so the hot invoke path doesn't re-check them; only data-dependent problems (for example, a byte buffer whose length isn't a whole number of elements) surface at invoke.

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?

view this post on Zulip Wasmtime GitHub notifications bot (Jul 04 2026 at 06:51):

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

view this post on Zulip Wasmtime GitHub notifications bot (Jul 04 2026 at 06:51):

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

view this post on Zulip Wasmtime GitHub notifications bot (Jul 05 2026 at 07:05):

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 file crates/wasmtime/src/runtime/component/func/prepared.rs, tests in tests/all/component_model/prepared.rs). It's built up in a few small commits and the whole component_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

One refinement worth flagging

I split the reflection gate into two predicates:

The unchecked Flat memcpy and the zero-copy view gate on the stricter are_all_bit_patterns_valid, so e.g. list<bool> is rejected (a byte ≠ 0/1 would be an invalid bool). A future validated bulk path could use the broader is_cabi_inline plus 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 unsafe trait?

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, with are_all_bit_patterns_valid as the safety boundary; a trait would only need unsafe if 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/flags via a validation sweep; async invoke; Stream / typed-buffer / Lower argument sources; and the guest→host (import) side.

Happy to adjust any of the surface before turning this into a PR.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 05 2026 at 23:34):

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 file crates/wasmtime/src/runtime/component/func/prepared.rs, tests in tests/all/component_model/prepared.rs). It's built up in a few small commits and the whole component_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

One refinement worth flagging

I split the reflection gate into two predicates:

The unchecked Flat memcpy and the zero-copy view gate on the stricter are_all_bit_patterns_valid, so e.g. list<bool> is rejected (a byte ≠ 0/1 would be an invalid bool). A future validated bulk path could use the broader is_cabi_inline plus 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 unsafe trait?

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, with are_all_bit_patterns_valid as the safety boundary; a trait would only need unsafe if 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/flags via a validation sweep; async invoke; Stream / typed-buffer / Lower argument sources; and the guest→host (import) side.

Happy to adjust any of the surface before turning this into a PR.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 05 2026 at 23:41):

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" Val tree, 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.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 06 2026 at 15:01):

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::call on top of it. Another example could be that Val fits 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 keeping Val, but I'd prefer to not have 3 ways to call functions (Val-full-dynamic, this way, and TypedFunc-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.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 20 2026 at 08:00):

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 Val based 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 Flat bytes are
proof-carrying (you can't make a ValidatedCabiBytes without 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
a Lazy leaf 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 ValSource tree needs to support it:

I can't think of anything that would substantially beat this for speed without sacrificing safety.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 20 2026 at 08:01):

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 Val based 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 Flat bytes are proof-carrying (you can't make a ValidatedCabiBytes without 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 a Lazy leaf 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 ValSource tree needs to support it:

I can't think of anything that would substantially beat this for speed without sacrificing safety.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 20 2026 at 20:04):

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 Val on top of something else I think is also reasonable to do.


Last updated: Jul 29 2026 at 05:03 UTC