Skip to content
This repository was archived by the owner on Nov 25, 2025. It is now read-only.

stdio: Update interfaces to support error results#82

Merged
lann merged 2 commits intoWebAssembly:mainfrom
lann:stdio-errors
Sep 15, 2025
Merged

stdio: Update interfaces to support error results#82
lann merged 2 commits intoWebAssembly:mainfrom
lann:stdio-errors

Conversation

@lann
Copy link
Collaborator

@lann lann commented Sep 8, 2025

Adapted from wasi:filesystem/types@0.3.0-rc-2025-08-15 descriptor.read-via-stream / append-via-stream.

Fixes #81

@alexcrichton
Copy link
Contributor

Returning a tuple on get-stdin makes sense to me, but I would posit that async func(data: stream<u8>) -> result<_, error-code> is sufficient for append-std{out,err}. I don't think that stdio streams are special in their need for flushing, and if flushing is required that seems like we'd want a stream/component-model-level primitive for that

@lann
Copy link
Collaborator Author

lann commented Sep 8, 2025

I already changed it 🙂

stdout is special; with line buffering flush is the only way to write to a terminal without a newline. I think that can be punted for now, but could perhaps live on terminal-output later.

interface stdin {
@since(version = 0.3.0-rc-2025-08-15)
get-stdin: func() -> stream<u8>;
read-stdin: func() -> tuple<stream<u8>, future<result<_, error-code>>>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so, is the idea that you're supposed to call read-stdin each time or just once? if just once that does have the issue that you can't represent stdin that returns EOF (e.g. pressing Ctrl+Z on Linux) and then can read more data after that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either should work, though I'm not aware of any recoverable errors from reading stdin that would apply here (e.g. I don't think EINTR is applicable with WASI).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this can be called multiple times (wouldn't that be required to enable e.g. composition without virtualizing this interface?), what content do consequent streams get?

let's say we do something roughly like this:

echo "hello, world" | wasmtime --inherit-stdio test.wasm
(stream1, fut1) = read-stdin()
stream1.read(7).await // reads `hello, `
(stream2, fut2) = read-stdin()
stream2.read(7).await // does this read `hello, ` or `world` ?
stream1.read(5).await // does this read `world` or EOF ?

Personally, I think I'd expect that if multiple streams exist simultaneously data read is effectively random, i.e. there's no concept of stdin buffer/fan-out in the host and no guarantee on the ordering with multiple concurrent streams. Each stream acts as a "view" into the singular stdin stream and if the guest read N bytes, next read will start reading from byte N+1 regardless of which stream is used. If guest invokes 2 concurrent reads of N and M bytes on different streams, the host only guarantees that at most N + M sequential bytes will be read, but there is no way of knowing which stream read will get the "head" and which - the "tail"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd expect that if multiple streams exist simultaneously data read is effectively random

Yeah that's what I'm trying to say in the last chunk of the doc comment:

/// Multiple streams may be active at the same time. The behavior of concurrent
/// reads is implementation-specific.

@alexcrichton
Copy link
Contributor

This looks reasonable to me, although one thing I'm now thinking about as well -- one option is there could be an entirely separate function for "get the last error". That would remove the need for broadcast-semantics where there are multiple stdin handles, for example, or multiple invocations of append-stdout. Instead the protocol is that callers would wait for the stream to end and then once it ends they'd go check the error to see if one happened. Unsure if this is better, but it's a viable alternative technically I think

@lann
Copy link
Collaborator Author

lann commented Sep 8, 2025

I'm not sure how stdin is supposed to signal EOF, here or for wasi:filesystem/types.descriptor.read-via-stream (asked in WebAssembly/wasi-filesystem#188). Edit: the stream closes and the future resolves ok

@lann
Copy link
Collaborator Author

lann commented Sep 8, 2025

"Get the last error" could be fine if there are no recoverable errors, though I'm not sure if that is realistic. Notably I just added error-code.illegal-byte-sequence to represent Windows encoding errors which seem plausibly recoverable, though that's just speculative at the moment.

@alexcrichton
Copy link
Contributor

The design of component-model streams don't really allow for recoverable errors right now since, for example, when you write to a stream all you get back was "ok I took N bytes" and "here's whether I'm closed". Given that there's no way to say "I'm still open but there's an error somewhere else for you to look at". AFAIK everywhere else in WASI errors on streams are considered unrecoverable as part of WASIp3 basically

@lann
Copy link
Collaborator Author

lann commented Sep 8, 2025

It seems like any approach that would apply here would also apply to opening the same wasi-filesystem file twice, right? i.e. if there are multiple concurrent descriptor.write-to-streams to the same underlying file you might need to broadcast an error to them all?

@alexcrichton
Copy link
Contributor

Thinking more on it I believe stdin is the most affected. For stdout and files N concurrent operations correspond to N different calls that all get to figure out how they want to do errors. For stdin though we have infrastructure to share that amongst all threads and stores in the same process and that's where the broadcast behavior would come into play because we'd explicitly have a list of things blocked on a lock or something like that.

Overall though I wouldn't really place weight on the concern, while "return last error" is technically viable it feels too different from http/filesystem that's probably worth sticking to "here's the result for this operation you just started"

@since(version = 0.3.0-rc-2025-08-15)
enum error-code {
illegal-byte-sequence,
pipe,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think pipe is already expressible, by stream.write returning copy-result.dropped.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, is it the case that copy-result.dropped means the stream is over, and that's when the error code is delivered, so we need a pipe-like code for distinguishing that case from others?

Copy link
Collaborator Author

@lann lann Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for reading dropped is eos. I'm not sure what the intent is for writes but the docs say it "doesn't signify an error." 🤷

@lann lann marked this pull request as ready for review September 9, 2025 18:19
@lann
Copy link
Collaborator Author

lann commented Sep 9, 2025

Adapted docs from wasi:filesystem. Ready for review.

@lann lann changed the title WIP: stdio stream errors stdio: Update interfaces to support error results Sep 9, 2025
///
/// If the stream is dropped by the writer (`stream.write` returns `dropped`),
/// the future will resolve either successfully if stdout was closed by the other
/// end or an error if the write failed.
Copy link
Collaborator Author

@lann lann Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should line buffering be specified or should that be up to wasi-libc?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I think that should be up to wasi-libc as that's an in-guest thing rather than a host thing

@lann
Copy link
Collaborator Author

lann commented Sep 15, 2025

If there are no objections I'm going to plan to merge this to unblock implementation work which can inform future revisions.

Copy link
Member

@sunfishcode sunfishcode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me, with one minor bikeshed suggestion which I'll leave to your discression:

@lann lann merged commit 31337d8 into WebAssembly:main Sep 15, 2025
1 check passed
@lann lann deleted the stdio-errors branch September 15, 2025 17:22
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[0.3.0-draft] No way to communicate error-on-write via stdio streams

6 participants