Stream: wasi

Topic: Creating a `stream` in a sync context


view this post on Zulip zslayton (Jul 10 2026 at 08:06):

Hi! I'm trying to add WASI p3 support to WASI-Virt. As a jumping off point to get familiar with everything, I started writing a minimal example component that virtualizes wasi:filesystem@0.3.0. However, I've run into a problem that seems to be (at least for the time being?) impossible.

Here's a gist with the relevant bits of my code.

WASI-Virt allows you to specify file names to be backed by a given byte array. I want to implement Descriptor::read_via_stream by either actually reading a file or by copying a static byte array into the stream as needed. However, I cannot find a way to actually copy a byte array into the stream.

read-via-stream is defined as a synchronous function in the WIT, and that cannot be overridden (for example, using the async setting in wit-bindgen::generate). Because another component would call into this method as its entry point, there's no async runtime set up in this component to process async tasks. That means I cannot use wit_bindgen::spawn_local or wit_bindgen::block_on to populate the stream. Doing so produces an async task that never gets polled, leading the reader to hang at runtime. A host application can use custom logic to work around this limitation, but that's not an option for WASI-Virt.

There doesn't seem to be any way to meaningfully initialize a stream in a synchronous context. Am I missing something in the APIs? Are components not meant to be able to virtualize a synchronous method?

view this post on Zulip Christof Petig (Jul 10 2026 at 13:11):

At first glance this looks like you ran into the same problem I reported two days ago at https://bytecodealliance.zulipchat.com/#narrow/channel/219900-wasi/topic/.E2.9C.94.20Interval.20timer/near/608682442 . If so a fix is planned without a time frame or priority.

view this post on Zulip zslayton (Jul 10 2026 at 13:22):

Thanks, Christof! This does indeed sound quite similar. Regarding the fix, are you referring to what @Joel Dice said?:

Makes sense. I think the plan is to allow the sync lift ABI to use task.return (and thus allow post-return execution) as well, but that hasn't landed yet.

If so, that does seem like it might address my problem. Joel, could you confirm that this fix would allow a synchronous function to call spawn_local? Is my understanding that read-via-stream cannot be adapted/lifted into an async function correct?

view this post on Zulip Joel Dice (Jul 10 2026 at 14:04):

The (somewhat confusing) thing to understand is that the term async is used two different ways in the component model:

The key insight here is that the lift/lower ABI used need not match the type: you can use the async lift ABI for a sync function or vice-versa. The Rust wit-bindgen will default to matching the ABI to the type, but you can override that using the async option via the CLI or the generate! macro. So in your case, you can lift read-via-stream using the async ABI even though the type is sync, and you can use spawn_local to write to the stream and future after returning them.

view this post on Zulip zslayton (Jul 11 2026 at 06:16):

Thanks, Joel. I've tried configuring read-via-async to be an async fn before in the call to generate!, like this:

    async: [
        "export:wasi:filesystem/types@0.3.0#[method]descriptor.read-via-stream",
    ]

(and the same in the importer with import:wasi:...) but at runtime I get this error:

 /home/zslayton/.cargo/bin/wasmtime --dir . composed.wasm
Error: failed to parse WebAssembly module

Caused by:
    the `async` canonical option requires an async function type

which I interpreted to mean that I couldn't do that. I'm using:

wasmtime 48.0.0 (66f269153 2026-07-09)
wit-bindgen 0.59.0
rustc 1.98.0-nightly (4c9d2bfe4 2026-07-01)

view this post on Zulip zslayton (Jul 11 2026 at 07:05):

Narrowing this down a bit further: when I include

async: [
    "export:wasi:filesystem/types@0.3.0#[method]descriptor.read-via-stream",
]

on the exporter side, the component it produces will not validate on its own with wasm-tools:

 wasm-tools component wit spike-virt/target/wasm32-wasip2/debug/spike_virt.wasm
error: the `async` canonical option requires an async function type (at offset 0x57ae5c)

view this post on Zulip zslayton (Jul 11 2026 at 07:25):

In this wit-bindgen issue, @Alex Crichton seems to say that the async option isn't legal for functions that are sync in the WIT.

view this post on Zulip Joel Dice (Jul 11 2026 at 13:25):

Oof, yeah, I forgot about the implications of https://github.com/WebAssembly/component-model/pull/646, a change that was made pretty late in the design process. And I guess I didn't realize how it would affect this case.

@Luke Wagner am I missing something, or is it effectively impossible to write to a stream returned by a sync function such as read-via-stream without guest superpowers? If so, that's something we'll need to address.

view this post on Zulip Luke Wagner (Jul 11 2026 at 13:40):

@Joel Dice That's right. Both coop threads and the ABI changes (which would decouple the ability to call task.return from being async) will fix this, I believe

view this post on Zulip zslayton (Jul 11 2026 at 14:21):

Ok, cool. Are there relevant GitHub issues I could follow? Would the necessary changes be in wasmtime, or tied to a future WASI release?

view this post on Zulip Luke Wagner (Jul 12 2026 at 00:29):

For cooperative threads, the plan is that it will go in WASI minor release (a 0.3.x) once the feature has been implemented and the WASI SG has a vote (it's already mostly implemented in wasmtime behind a flag, iiuc, but there's more producer toolchain work to do). I'm not sure if there's an overall tracking issue for cooperative threads to follow... but maybe someone else does?

view this post on Zulip zslayton (Jul 13 2026 at 06:37):

Both coop threads and the ABI changes [...] will fix this, I believe

Re-reading this, I wasn't sure if this meant that both features need to land for this or either one would suffice? What phrase could I use to research the ABI changes?

view this post on Zulip Luke Wagner (Jul 13 2026 at 16:35):

Both features would independently address the issue, with cooperative threads being much closer to completion of the two. In the CM spec repo, the cooperative thread additions are marked by the :thread: character, and these are, i believe, all implemented in Wasmtime behind the flag -W component-model-threading.

The "ABI changes" are not as clearly defined, but outlined in this BA blog post section. As part of these changes, I believe we can make task.return-vs-flat-results-and-post-return an ABI choice that is independent of whether the declared WIT function type is async.

view this post on Zulip Luke Wagner (Jul 16 2026 at 17:19):

Oh, I realized there is a third solution that could work in 0.3.0:

So iiuc, the goal here is for WASI-Virt to be able to virtualize read-via-stream in terms of data segments and the challenge is: during the synchronous read-via-stream call, we can call stream.new, we can initiate an async stream.write on the new writable end (passing a pointer to a linear memory region read from a data segment) which will always block (the caller hasn't had a chance to stream.read yet), and we can return the new readable end by returning from core wasm, but how do we now waitable-set.wait to find out when the stream.write completes so that we can release/reuse the memory buffer passed into it and call stream.close-writable? I think a third solution would be: in the context of WASI-Virt, have the component that is virtualizing/exporting WASI add an extra export init: async func() that gets called by the top-level component (produced by WASI-virt that links the virtualizing component with the guest component) up-front. init would immediately task.return and then return WAIT to its callback loop, waiting on a global waitable-set that read-via-stream appends its pending writable end to. This callback loop could then free the memory and close the writable end (or, more generally, execute any async work).

view this post on Zulip zslayton (Jul 27 2026 at 08:52):

I'm a bit beyond my depth here, but if I'm understanding correctly that sounds like it might be an issue.

If I virtualize guest component Foo, another guest component Bar that could previously be composed together with Foo would fail at runtime because it didn't know to call init() to set up the async machinery before invoking Foo's methods. Is that right?

view this post on Zulip Luke Wagner (Jul 27 2026 at 21:53):

Ah, great question; the idea is: in the context of WASI-Virt, since WASI-Virt is wrapping the original guest Foo, it can guarantee that init() is called before Foo can possibly call any virtualized imports and that these virtualized imports are only callable by Foo (a totally separate Bar wouldn't have access to them b/c they are scoped by the parent component generated by WASI-Virt that links the entire composition together).


Last updated: Jul 29 2026 at 05:03 UTC