Skip to content

Conversation

@badeend
Copy link
Member

@badeend badeend commented Jan 10, 2026

Several WASI interfaces (stdio, filesystem, sockets) use the following pattern:

resource example {
  read: func() -> tuple<stream<u8>, future<result<_, error-code>>>;
  write: async func(data: stream<u8>) -> result<_, error-code>;
}
  • read is synchronous and returns a stream and future that are independent of the implicit this handle.
  • write is asynchronous and implicitly borrows this for the entire duration of consuming the input stream.

Problem: Component composition

Because write keeps this borrowed until the input stream is fully consumed, the resource and its owning component must stay alive for the entire stream operation. This prevents a component from performing a one-time setup, forwarding streams, and then returning immediately. In middleware-style patterns, the component cannot "step out of the way" once the streams are connected up, since the async write ties the stream's lifetime to the component's presence.

Problem: Rust lifetimes

On the Rust side, this pattern makes it impossible to wrap such a resource into a single struct without resorting to unsafe code. The write call produces a future that captures a borrow of this, which makes the Future self-referential with respect to the resource being stored. For example:

struct MyTcpStream {
    socket: TcpSocket,
    send: StreamWriter<u8>,
    send_result: Pin<Box<dyn Future<Output = Result<(), ErrorCode>>>>,
    receive: StreamReader<u8>,
    receive_result: FutureReader<Result<(), ErrorCode>>,
}
impl MyTcpStream {
    pub fn from(socket: TcpSocket) -> Self {
        let (send, send_reader) = wit_stream::new();
        let send_result = Box::pin(socket.send(send_reader));
        let (receive, receive_result) = socket.receive();
        Self {
            socket, // ERROR! `socket` is still borrowed by the `send` call.
            send,
            send_result,
            receive,
            receive_result,
        }
    }
}

Conceptually, the send_result future is a self-borrow of socket. This pattern cannot be expressed safely in Rust today and prevents ergonomic bindings for common I/O abstractions like AsyncRead and AsyncWrite.

Solution

The most straight-forward thing I could come up with is to turn the write methods synchronous and return a future instead, mirroring the read methods. Other thoughts are welcome too of course.

From e.g.:
write: async func(data: stream<u8>) -> result<_, error-code>;
To:
write: func(data: stream<u8>) -> future<result<_, error-code>>;

WDTY?

@badeend badeend requested review from a team as code owners January 10, 2026 15:15
@github-actions github-actions bot added P-filesystem Proposal: wasi-filesystem P-sockets Proposal: wasi-sockets P-cli Proposal: wasi-cli labels Jan 10, 2026
@vados-cosmonic
Copy link
Contributor

This was quite the topic of contention IIRC -- heads up I opened a thread on the BCA Zulip#wasi > converting fs async writes to sync writes returning streams to try and discuss this and get some more insight here, since I remember this being debated for STDIN/STDOUT and what the usual interface for those functions should be.

@badeend
Copy link
Member Author

badeend commented Jan 11, 2026

If you're thinking of 757, then I don't think these are related. In this PR here, the relevant interfaces still use streams. It's just about async vs. future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P-cli Proposal: wasi-cli P-filesystem Proposal: wasi-filesystem P-sockets Proposal: wasi-sockets

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants