dicej opened PR #11515 from dicej:stream-future-api-revamp to bytecodealliance:main:
This changes the host APIs for dealing with futures and streams from a "rendezvous"-style API to a callback-oriented one.
Previously you would create e.g. a
StreamReader/StreamWriterpair and call theirreadandwritemethods, respectively, and those methods would returnFutures that resolved when the operation was matched with a correspondingwriteorreadoperation on the other end.With the new API, you instead provide a
StreamProducertrait implementation whe creating the stream, whoseproducemethod will be called as soon as a read happens, giving the implementation a chance to respond immediately without making the reader wait for a rendezvous. Likewise, you can match the read end of a stream to aStreamConsumerto respond immediately to writes. This model should reduce scheduling overhead and make it easier to e.g. pipe items to/fromAsyncWrite/AsyncReadorSink/Streamimplementations without needing to explicitly spawn background tasks. In addition, the new API provides direct access to guest read and write buffers forstream<u8>operations, enabling zero-copy operations.Other changes:
I've removed the
HostTaskOutput; we were using it to run extra code with access to the store after a host task completes, but we can do that more elegantly inside the future usingtls::get. This also allowed me to simplifyInstance::poll_untila bit.I've removed the
watch_{reader,writer}functionality; it's not needed now given that the runtime will automatically dispose of the producer or consumer when the other end of the stream or future is closed -- no need for embedder code to manage that.In order to make
UntypedWriteBufferSend, I had to wrap its raw pointerbuffield in aSendSyncPtr.I've removed
{Future,Stream}Writerentirely and movedInstance::{future,stream}to{Future,Stream}Reader::new, respectively.I've added a bounds check to the beginnings of
Instance::guest_readandInstance::guest_writeso that we need not do it later inGuest{Source,Destination}::remaining, meaning those functions can be infallible.Note that I haven't updated
wasmtime-wasiyet to match; that will happen in one or more follow-up commits.<!--
Please make sure you include the following information:
If this work has been discussed elsewhere, please include a link to that
conversation. If it was discussed in an issue, just mention "issue #...".Explain why this change is needed. If the details are in an issue already,
this can be brief.Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.htmlPlease ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->
dicej updated PR #11515.
dicej updated PR #11515.
alexcrichton created PR review comment:
Technically this should use
poll_ready, right?
alexcrichton submitted PR review:
Is it worth it to resolve the zero-length read/write TODOs in the code before landing? We don't have many users of that right now so it also seems ok to defer that too.
Otherwise though this all seems reasonable to me, although I'm mostly relying on tests. The interfaces we've talked about historically and I think are ok to land. I'm also happy to help out with the porting of wasmtime-wasi later this afternoon
alexcrichton created PR review comment:
Technically this should call
.next()and buffer up the result if one comes in, right?
Is it worth it to resolve the zero-length read/write TODOs in the code before landing? We don't have many users of that right now so it also seems ok to defer that too.
Yup, I'll do that. Testing it will be mildly tedious, but might as well bite the bullet.
Otherwise though this all seems reasonable to me, although I'm mostly relying on tests. The interfaces we've talked about historically and I think are ok to land. I'm also happy to help out with the porting of wasmtime-wasi later this afternoon
@rvolosatovs is planning to take a crack at updating
wasmtime-wasistarting tomorrow, FYI.
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
It appears that we'll need to expose the second type parameter to
Accessorin order to be able to use this API from within e.g.wasmtime_wasi.
Binding implementations get an opaqueTpassed in, e.g. https://github.com/bytecodealliance/wasmtime/blob/3aa392399e785df414ad6ee6ee7f034989b75301/crates/wasi/src/p3/sockets/host/types/tcp.rs#L252-L253pub trait FutureProducer<T, U, V>: Send + 'static { /// Handle a host- or guest-initiated read by producing a value. fn produce(self, accessor: &Accessor<T, U>) -> impl Future<Output = Result<V>> + Send; }
alexcrichton submitted PR review.
alexcrichton created PR review comment:
Oh for that @dicej and I talked about this a bit ago, I'd prefer to avoid the type parameter explosion but I have an alternative solution that should work. Let me sketch that out and push it up here too
alexcrichton updated PR #11515.
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
Awesome, thanks @alexcrichton .
I actually just ran into a very similar issue earlier today inwasi:http, trying to reuse the same task implementation both from outside and from within thewasi:httpcrate, which I worked around the following way:impl<T, U, Fut> AccessorTask<T, U, wasmtime::Result<()>> for GuestBodyTask<Fut> where T: WasiHttpView, U: HasData, Fut: Future<Output = Result<(), ErrorCode>> + Send + 'static, { async fn run(self, store: &Accessor<T, U>) -> wasmtime::Result<()> { self.run(store, |mut store, trailers| { store .data_mut() .http() .table .delete(trailers) .context("failed to delete trailers") }) .await } } /// This is a duplicate of [GuestBodyTask], which can be used from within this crate pub(crate) struct GuestBodyTaskInternal<T>(GuestBodyTask<T>); impl<T, U, Fut> AccessorTask<T, U, wasmtime::Result<()>> for GuestBodyTaskInternal<Fut> where for<'a> U: HasData<Data<'a> = WasiHttpCtxView<'a>>, Fut: Future<Output = Result<(), ErrorCode>> + Send + 'static, { async fn run(self, store: &Accessor<T, U>) -> wasmtime::Result<()> { self.0 .run(store, |mut store, trailers| { store .get() .table .delete(trailers) .context("failed to delete trailers") }) .await } }Perhaps this serves as a good data point for the fix
alexcrichton submitted PR review.
alexcrichton created PR review comment:
Ok I pushed an extra commit up with what I was thinking, the rough idea is that the WASI implementations will close over the "getter", the
fn(&mut T) -> D::Data<'_>projection, themselves. If that's onerous though to schlep with WASI we can look to bake it in.
rvolosatovs created PR review comment:
Testing it now, looks like bindgen still needs to be updated for the rename
rvolosatovs submitted PR review.
rvolosatovs edited PR review comment.
rvolosatovs updated PR #11515.
dicej updated PR #11515.
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
Unfortunately, I don't think that the refactor fixes the underlying issue, which is that the host interface implementations do not have access to the getter provided by the the call to
add_to_linker. The getter provided byAccess<T>::getterwill always produce&mut T, because the second type parameter defaults toHasSelf<T>.What this means is that for this to work, the getter would have to be passed to the implementation through
WasiSocketsCtxView<'a>, for example.
Accessor::newcreates a getter, which is an identity function https://github.com/bytecodealliance/wasmtime/pull/11515/files#diff-714ba43342be98eb093bc17d4ad955a3f3056f4c424635ac399da76349a3a8d7R422, that's the only getter visible toproduceimplementations- The actual getter implementations need though is only visible to the generated bindings https://github.com/bytecodealliance/wasmtime/pull/11515/files#diff-b365a0c26d559b75058e519f7e01c9e0b201da9f8a16df2b96cf6693f6b38ce6R1458 and it's later used via
with_getterto map the 2nd type parameter to a different type https://github.com/bytecodealliance/wasmtime/pull/11515/files#diff-b365a0c26d559b75058e519f7e01c9e0b201da9f8a16df2b96cf6693f6b38ce6R2493-R2520I'm happy to jump on a quick call to talk through this today to give more details.
rvolosatovs edited PR review comment.
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
Once a
FutureReaderis returned by a host implementation, is it guaranteed that the runtime will callcloseif the guest does not use it? (or e.g. traps)
rvolosatovs created PR review comment:
Since we're dealing with byte buffers only here, could we reuse https://docs.rs/tokio/latest/tokio/io/struct.ReadBuf.html directly in some way?
rvolosatovs created PR review comment:
I would have expected this function to not be
async, in fact, shouldn't this function to moveDestination, such that it could only be called once with at most the number of elements that reader asked for?
rvolosatovs edited PR review comment.
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
nvm, we have figured it out together with @dicej , thanks!
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
Resolved offline. Once returned to the guest the handle will be properly disposed off with a well-behaving guest. We probably want to introduce some limit on the amount of open future/stream handles that the guest can have
cc @dicej
rvolosatovs created PR review comment:
Resolved offline, we may need to wait for realloc, which is why this needs to be
async.
@dicej will add aremainingfunction to this struct
rvolosatovs submitted PR review.
dicej updated PR #11515.
rvolosatovs updated PR #11515.
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
I think this is an important detail of the implementation to call out - in the absence of task, the stream handle returned in
tuple<stream<T>, future<result<_, E>>>pattern has to be dropped/closed before a value can arrive on thefuture, I suspect that is because in case of a task, the host could independently track the status of the socket and be notified of the socket shutdown by the OS as part of it's own event loop. Without a task, the guest must effectively "signal" the host to "do work", in success case triggering theDropof the producer, I suppose.I wonder if that's the expected behavior here and whether that would play well with GC languages.
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
One solution could be giving both stream and future producers access to the underlying socket and check for shutdown in the future producer as well. It seems that such an approach would require the future producer to also perform reads from the socket and communicate those to stream producer.
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
Giving it a bit more thought, the guest is getting
StreamResult::Completeon L46, shouldn't the host eagerly drop the producer before returning this to the guest?
rvolosatovs edited PR review comment.
rvolosatovs edited PR review comment.
rvolosatovs deleted PR review comment.
dicej submitted PR review.
dicej created PR review comment:
Giving it a bit more thought, the guest is getting
StreamResult::Completeon L46, shouldn't the host eagerly drop the producer _before_ letting the guest know that the stream is done?Yes, it should; if it's not, I'd consider that a bug. I'll take a look.
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
I've originally mistaken
CompleteforDropped, but realized that was wrong https://github.com/bytecodealliance/wit-bindgen/blob/f61634fd7905595ba7b83bb8cda42a4f3ee3ac29/crates/guest-rust/src/rt/async_support/stream_support.rs#L225-L235 and deleted the comment.I think the test was simply incorrect, see #wasmtime > async API revamp implications
dicej submitted PR review.
dicej created PR review comment:
I just opened https://github.com/bytecodealliance/wasmtime/issues/11552 to track that.
dicej created PR review comment:
Wasmtime doesn't currently depend on
tokioexcept for testing; I'm not sureReadBufadds enough value to justify pulling it in.
dicej submitted PR review.
rvolosatovs updated PR #11515.
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
It seems that the consumer MUST loop here to ensure that all of the data already read from the guest has been written - otherwise, if the consumer were to buffer, how would the guest know that a partial write occurred?
It seems that what we really want is to have a way for the consumer to report back the amount of elements actually read, replicating something like https://doc.rust-lang.org/nightly/std/io/trait.Read.html#tymethod.read
Given the existing API, it looks like the consumer should be able to return the buffer, just like the
Destination::writewould currently do.This is, of course, already addressed for byte-buffers originating from the guest, but this seems problematic in general case
rvolosatovs edited PR review comment.
rvolosatovs updated PR #11515.
rvolosatovs updated PR #11515.
rvolosatovs updated PR #11515.
rvolosatovs updated PR #11515.
rvolosatovs commented on PR #11515:
All existing WASI tests pass now for me locally. I'm a bit surprised that it looks like CI does not run on this PR anymore?
I've removed the reuseaddr test workaround, since this change set should remove the need for it, and close https://github.com/bytecodealliance/wasmtime/issues/11342
rvolosatovs edited a comment on PR #11515:
All existing WASI tests pass now for me locally. I'm a bit surprised that it looks like CI does not run on this PR anymore?
I've removed the reuseaddr test workaround, since this change set should remove the need for it and close https://github.com/bytecodealliance/wasmtime/issues/11342
rvolosatovs commented on PR #11515:
The main remaining item left here is the cancellation-safety of
producefunctions, but we've been having some private conversations with @dicej and hopefully a pretty minor API change should suffice to address it.
@dicej please let me know when the new API is available and I will happily fix WASI again.I will meanwhile work on migrating #11440 to this API
rvolosatovs edited a comment on PR #11515:
The main remaining item left here is the cancellation-safety of
producefunctions, but we've been having some private conversations with @dicej and hopefully a pretty minor API change should suffice to address it.
@dicej please let me know when the new API is available and I will happily fix WASI again.
Could you also rebase this PR on latestmain, please?I will meanwhile work on migrating #11440 to this API
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
Is "lying" like this OK, @dicej, or do we have to drive I/O and buffer the result?
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
Same as above, do we need to drive I/O?
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
same here
rvolosatovs commented on PR #11515:
On a second thought, I'm guessing that "lying" about the stream being open is probably not the expected behavior, so I'll work on driving I/O in these and buffering first
rvolosatovs updated PR #11515.
On a second thought, I'm guessing that "lying" about the stream being open is probably not the expected behavior, so I'll work on driving I/O in these and buffering first
This is a good reminder that we should add tests that involve zero-length reads and writes from the guest, asserting that the following non-zero-length read or write completes immediately. So far, this hasn't really been relevant, but once we add p3 support to wasi-libc we'll have real-world code that relies on it.
rvolosatovs commented on PR #11515:
On a second thought, I'm guessing that "lying" about the stream being open is probably not the expected behavior, so I'll work on driving I/O in these and buffering first
This is a good reminder that we should add tests that involve zero-length reads and writes from the guest, asserting that the following non-zero-length read or write completes immediately. So far, this hasn't really been relevant, but once we add p3 support to wasi-libc we'll have real-world code that relies on it.
I was under impression that we wanted to avoid buffering, however buffering seems to be the only way of providing this behavior for writes to e.g. files. It looks like for filesystem we'll need to introduce a construct (similar to p2), where we'll have a thread performing file I/O and
wasi:filesystemwrite just sends the buffer on something likempscchannel.Is that the expectation here?
I think makes sense to me, but it appears we'd also need to introduce
sync/flushor similar towasi:filesystemto be able to ensure that the host worker thread buffer has actually been passed over to the kernel
rvolosatovs edited a comment on PR #11515:
On a second thought, I'm guessing that "lying" about the stream being open is probably not the expected behavior, so I'll work on driving I/O in these and buffering first
This is a good reminder that we should add tests that involve zero-length reads and writes from the guest, asserting that the following non-zero-length read or write completes immediately. So far, this hasn't really been relevant, but once we add p3 support to wasi-libc we'll have real-world code that relies on it.
I was under impression that we wanted to avoid buffering, however buffering seems to be the only way of providing this behavior for writes to e.g. files. It looks like for filesystem we'll need to introduce a construct (similar to p2), where we'll have a thread performing file I/O and
wasi:filesystemwrite just sends the buffer on something likempscchannel.Is that the expectation here?
I think that makes sense to me, but it appears we'd also need to introduce
sync/flushor similar towasi:filesystemto be able to ensure that the host worker thread buffer has actually been passed over to the kernel
rvolosatovs edited a comment on PR #11515:
On a second thought, I'm guessing that "lying" about the stream being open is probably not the expected behavior, so I'll work on driving I/O in these and buffering first
This is a good reminder that we should add tests that involve zero-length reads and writes from the guest, asserting that the following non-zero-length read or write completes immediately. So far, this hasn't really been relevant, but once we add p3 support to wasi-libc we'll have real-world code that relies on it.
I was under impression that we wanted to avoid buffering, however buffering seems to be the only way of providing this behavior for writes to e.g. files. It looks like for filesystem we'll need to introduce a construct (similar to p2), where we'll have a thread performing file I/O and
wasi:filesystemwrite just sends the buffer on something likempscchannel.Is that the expectation here?
I think that makes sense to me, but it appears we'd also need to introduce
sync/flushor similar towasi:filesystemto be able to ensure that the host worker thread buffer has actually been passed over to the kernel sincestream.writewill no longer guarantee that
rvolosatovs commented on PR #11515:
hmm,
syncdoes exist https://github.com/WebAssembly/wasi-filesystem/blob/d81d6256c271fe1c8937eb8353e2ddc25517c153/wit-0.3.0-draft/types.wit#L434, so would we make itasyncthen and await the flush before returning?
rvolosatovs edited a comment on PR #11515:
hmm,
syncdoes exist https://github.com/WebAssembly/wasi-filesystem/blob/d81d6256c271fe1c8937eb8353e2ddc25517c153/wit-0.3.0-draft/types.wit#L434, so would we need to await the flush before returning?
dicej submitted PR review.
dicej created PR review comment:
Yeah, I can see how that could be awkward -- you read items from the
Sourceand then try to write them to some kind of sink, but that sink might not be immediately ready to accept all the items. In that case do you just .await until the sink has accepted them all? If you do, then you're kind of blocking the original writer more than you should -- you'd rather just say you didn't read all the items and let the writer write them again if and when it wants to. Also, what if the sink closes before you can write them all?So yeah, I think we might need to change this API as well as the
StreamProducerone.
rvolosatovs updated PR #11515.
rvolosatovs commented on PR #11515:
for reference: #wasi > 0-length stream writes with files and stdio
sunfishcode commented on PR #11515:
hmm,
syncdoes exist https://github.com/WebAssembly/wasi-filesystem/blob/d81d6256c271fe1c8937eb8353e2ddc25517c153/wit-0.3.0-draft/types.wit#L434, so would we need to await the flush before returning?There are two kinds of synchronizing here: one is that the data will be visible to independent readers, and the other is that the data will be visible to independent readers even after an abrupt power failure. wasil-filesystem's
syncis for that second kind, which can be very slow, so we shouldn't use it unless the application has explicitly requested it.
dicej updated PR #11515.
dicej updated PR #11515.
rvolosatovs updated PR #11515.
dicej updated PR #11515.
dicej updated PR #11515.
rvolosatovs updated PR #11515.
rvolosatovs updated PR #11515.
rvolosatovs updated PR #11515.
rvolosatovs updated PR #11515.
rvolosatovs created PR review comment:
this would always return
None, wouldn't it? (since that's a guest buffer)
rvolosatovs submitted PR review.
rvolosatovs edited PR review comment.
dicej submitted PR review.
dicej created PR review comment:
Good point; I didn't revisit that code after I added the
if/elseblock around it.
rvolosatovs updated PR #11515.
rvolosatovs updated PR #11515.
rvolosatovs updated PR #11515.
rvolosatovs commented on PR #11515:
I'm signing off for today and have just finished the WASI crate adaptation a few minutes ago. I have not even really proof-read, for example, the
read_directoryimplementation yet.
There's quite a bit of room for improvement, mainly refactoring and cutting down on duplication.
I also noticed thatread_directoryseems to not be tested at all.That all said, all tests pass (at least locally, will have to wait for CI) and IMO this is "good enough" to get this PR merged. Feel free to clean-up as desired, otherwise I will do that tomorrow.
alexcrichton commented on PR #11515:
I talked with @dicej about this and we concluded that let's go ahead and land this. I've got follow-up feedback and Joel's got some follow-up implementation work but we feel it's best to land this and iterate rather than continuing to block this. The hope is that by landing this @rvolosatovs you're more-or-less unblocke to continue to work on wasi-http while we continue to smith some details here in parallel
alexcrichton updated PR #11515.
alexcrichton has marked PR #11515 as ready for review.
alexcrichton requested alexcrichton for a review on PR #11515.
alexcrichton requested wasmtime-core-reviewers for a review on PR #11515.
alexcrichton requested wasmtime-wasi-reviewers for a review on PR #11515.
alexcrichton submitted PR review.
alexcrichton created PR review comment:
Can we skip this fallback and rely on
as_direct_destinationalways succeeding?
alexcrichton created PR review comment:
I opened https://github.com/WebAssembly/wasi-cli/issues/81 for this, but for now it might be good to
log::warn!the error too
alexcrichton created PR review comment:
How come this needs a special case vs the below case?
alexcrichton has enabled auto merge for PR #11515.
alexcrichton submitted PR review.
alexcrichton created PR review comment:
For stdio in particular I think we'll additionally want to call
poll_flushhere before returningCompletedbecause otherwise tokio only puts this off on some other task and it hasn't actually made its way to stdout yet.
alexcrichton created PR review comment:
I'm going to look into restructuring this function to hopefully not need duplication in this
matchhere and the one far below after the spawn, but that's just refactoring a bit.
alexcrichton created PR review comment:
Like writes below I think this'll want a special-case of 0-length buffers to do a
poll_read_readyhere
alexcrichton created PR review comment:
This prompted discussion between Joel and I and led to https://github.com/WebAssembly/component-model/issues/561 but we'll need to carefully handle 0-length reads here
alexcrichton created PR review comment:
Talked with Joel about this and he's going to refactor things so the fallback isn't necessary (e.g.
as_direct_destinationalways returns non-None)
alexcrichton created PR review comment:
(talked with Joel and I'll handle this in a follow-up)
alexcrichton merged PR #11515.
rvolosatovs submitted PR review.
rvolosatovs created PR review comment:
With a 0-length read this tries a buffered read and if that would block, it polls readiness.
From what I understand that's the expectation of this API - produce at least 1 (buffered) element.Is there a reason to poll readiness before optimistically trying to read?
alexcrichton submitted PR review.
alexcrichton created PR review comment:
True! I should clarify my comment more... I'm assuming that with @dicej's queued up changes the
Nonecase foras_direct_destinationgoes away and so I was assuming that all the non-as_direct_destinationcode was going to be deleted. In that world the 0-length case is missed here, but otherwise you're right the 0-length case is handled through the buffers otherwise. (although that has its own problems)
Last updated: Dec 06 2025 at 06:05 UTC