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
see also: https://github.com/WebAssembly/WASI/issues/757
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.
If you're thinking of 757, then I don't think these are related
e.g. on windows iirc terminal writes are always blocking
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
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
asynceffect, since onlyasync-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!
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).
I think @Roman Volosatovs is right on the trapping behavior.
I've prototyped the change in wasmtime and it doesn't produce a trap
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
Thanks for clarifying that! OK, so basically this forces the write to spawn
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
And yeah, the idea that the sync function can call but NOT block seems somewhat limiting, but maybe it's fine
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