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?
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.
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?
The (somewhat confusing) thing to understand is that the term async is used two different ways in the component model:
task.return is used to return a value vs. just a "normal" return via the call stack, among other things.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.
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)
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)
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.
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.
@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
Ok, cool. Are there relevant GitHub issues I could follow? Would the necessary changes be in wasmtime, or tied to a future WASI release?
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?
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?
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.
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).
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?
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