Stream: wasi

Topic: converting fs async writes to sync writes returning streams


view this post on Zulip Victor Adossi (Jan 11 2026 at 06:28):

I just saw @Dave Bakker (badeend) 's PR over here:
https://github.com/WebAssembly/WASI/pull/870

IIRC this was a point of contention (which we've gone back and forth about) around STDIN/STDOUT, thought we should maybe open up the discussion here as well.

Going to over-tag for this one -- apologies ahead of time:
@Alex Crichton @Joel Dice @Luke Wagner @Roman Volosatovs

Several WASI interfaces (stdio, filesystem, sockets) use the following pattern: resource example { read: func() -> tuple<stream<u8>, future<result<_, error-code>>>; w...

view this post on Zulip Jacob Lifshay (Jan 11 2026 at 07:23):

see also: https://github.com/WebAssembly/WASI/issues/757

I'd like to propose a possible radical restructuring of stdin/stdout/stderr and how they're modeled for 0.3.0. Specifically something like this for 0.3.0: interface stdin { read: async func(amount:...

view this post on Zulip Jacob Lifshay (Jan 11 2026 at 07:26):

for sockets and maybe files I think we still need streams, however for stdout/stderr async/streams are sometimes unnecessary overhead because the underlying I/O operations are blocking calls on some OSes.

view this post on Zulip Dave Bakker (badeend) (Jan 11 2026 at 07:27):

If you're thinking of 757, then I don't think these are related

I'd like to propose a possible radical restructuring of stdin/stdout/stderr and how they're modeled for 0.3.0. Specifically something like this for 0.3.0: interface stdin { read: async func(amount:...

view this post on Zulip Jacob Lifshay (Jan 11 2026 at 07:27):

e.g. on windows iirc terminal writes are always blocking

view this post on Zulip Dave Bakker (badeend) (Jan 11 2026 at 07:28):

Don't get confused about the title of my PR, with what I'm proposing the relevant interfaces still use streams. It's just about async vs. future

view this post on Zulip Victor Adossi (Jan 11 2026 at 07:54):

Hey so noted on the PR but thanks for the link and clarifying -- One thing I was thinking about is the ergonomics side, of whether write should be async or sync to start with (which was part of the discussion in 757 -- the idea of which is more unsurprising).

I think the problem you laid out definitely makes sense and the simplest fix that you laid out also works -- but another place that gave me reason for pause was the semantics of calling an async import from a sync function. Correct me if I'm wrong but that case is supposed to trap if the import (or other async action) is going to block.

Additionally, each of these potentially-blocking operations will trap if the current task's function type does not declare the async effect, since only async-typed functions are allowed to block

To to make what I'm imagining a bit more concrete -- If I'm writing an implementation in a component for this resource (using your suggested change):

resource example {
  read: func() -> tuple<stream<u8>, future<result<_, error-code>>>;
  write: func(data: stream<u8>) -> future<result<_, error-code>>;
}

(I think also realistically it'd probably be a result<future<result<_, error-code>>>, just in case you ran into some unrelated issue)

Then I'm quite limited in what functions I can attempt to call from the write, no? IIUC I cannot block (i.e. reading the stream<u8>, or call any async lowered import).

Happy to learn where I'm wrong here though!

Repository for design and specification of the Component Model - WebAssembly/component-model

view this post on Zulip Roman Volosatovs (Jan 11 2026 at 08:17):

Regarding the trap, I believe the current thinking is that one can call an async import from a sync context, but not block on the result before returning a value (that's the trapping case).

view this post on Zulip Dave Bakker (badeend) (Jan 11 2026 at 08:41):

I think @Roman Volosatovs is right on the trapping behavior.
I've prototyped the change in wasmtime and it doesn't produce a trap

view this post on Zulip Dave Bakker (badeend) (Jan 11 2026 at 09:39):

Ok, scrap that last statement. I've prototyped the implementation as a host not a guest, so I wouldn't get a trap anyway. Regardless, I'm not using any special host powers.

From a guest's perspective this change likely turns the write implementation from

view this post on Zulip Victor Adossi (Jan 11 2026 at 09:39):

Thanks for clarifying that! OK, so basically this forces the write to spawn

view this post on Zulip Victor Adossi (Jan 11 2026 at 09:40):

That makes sense to me -- it seemed like either write would have to spawn or start a thread or something to actually do any (preemptive?) work that it wanted to do

view this post on Zulip Victor Adossi (Jan 11 2026 at 09:40):

And yeah, the idea that the sync function can call but NOT block seems somewhat limiting, but maybe it's fine

view this post on Zulip Alex Crichton (Jan 12 2026 at 17:59):

Thanks for the heads up on this! I'll want to write up some thoughts and will leave them on the PR (overall seems fine, just want to think harder about the implications of this myself)


Last updated: Jan 29 2026 at 13:25 UTC