Skip to content

Conversation

@dicej
Copy link
Contributor

@dicej dicej commented Jul 25, 2025

This makes a several changes to how {Stream,Future}|{Reader,Writer} work to make them more efficient and, in some ways, more ergonomic:

  • The background tasks have been removed, allowing reads and writes to complete without task context switching. We now only allocate and use oneshot channels lazily when the other end is not yet ready; this improves real world performance benchmarks (e.g. wasi-http request handling) considerably.

  • Instances of {Stream,Future}Reader can now be lifted and lowered directly; no need for Host{Stream,Future} anymore.

  • The type parameter for Stream{Reader,Writer} no longer refers to the buffer type -- just the payload type (i.e. StreamReader<u8> instead of StreamReader<Vec<u8>>), meaning any buffer type may be used for a given read or write operation. This also means the compiler needs help with type inference less often when calling Instance::stream.

  • Instances of {Stream,Future}|{Reader,Writer} now require access to the store in order to be disposed of properly. I've added RAII wrapper structs (WithAccessor[AndValue]) to help with this, and also updated Store::drop and Instance::run_concurrent to ensure the store thread-local is set when dropping futures closing over &Accessors.

  • In order to ensure that resources containing {Stream,Future}|{Reader,Writer} instances are disposed of properly, I've added LinkerInstance::resource_concurrent and have updated wasmtime-wit-bindgen to use it. This gives resource drop functions access to a StoreContextMut via an Accessor, allowing the stream and future handles to be disposed of.

    • In order to make this work, I had to change Accessor::instance from a Instance to an Option<Instance>, which is awkward but temporary since we're planning to remove Accessor::instance entirely once we've moved concurrent state from ComponentInstance to Store.

That problem of disposal is definitely the most awkward part of all this. In simple cases, it's easy enough to ensure that read and write handles are disposed of properly, but both wasmtime-wasi and wasmtime-wasi-http have some pretty complicated functions where handles are passed between tasks and/or stored inside resources, so it can be tricky to ensure proper disposal on all code paths. I'm open to ideas for improving this, but I suspect we'll need new Rust language features (e.g. linear types) to make it truly ergonomic, robust, and efficient.

While testing the above, I discovered an issue with Instance::poll_until such that it would prematurely give up and return a "deadlock" trap error, believing that there was no further work to do, even though the future passed to it was ready to resolve the next time it was polled. I've fixed this by polling it one last time and only trapping if it returns pending.

Note that I've moved a few associated functions from ConcurrentState to Instance (e.g. guest_drop_writable and others) since they now need access to the store; they're unchanged otherwise. Apologies for the diff noise.

Finally, I've tweaked how wasmtime serve to poll the guest for content before handing the response to Hyper, which helps performance by ensuring the first content chunk can be sent with the same TCP packet as the beginning of the response.

@dicej dicej requested a review from alexcrichton July 25, 2025 21:38
@dicej dicej requested review from a team as code owners July 25, 2025 21:38
@github-actions github-actions bot added the wasi Issues pertaining to WASI label Jul 26, 2025
@alexcrichton
Copy link
Member

This is going to have conflicts with #11328, and I'll help work through those.

@dicej dicej force-pushed the stream-future-refactor-upstream branch from 30756dc to 86e9594 Compare July 29, 2025 21:50
@dicej
Copy link
Contributor Author

dicej commented Jul 29, 2025

I've addressed all the feedback so far except that we're still lowering writes using same stack the host embedder called us on; I'll address that by using a fiber for lowering.

@dicej dicej force-pushed the stream-future-refactor-upstream branch from 86e9594 to 83ed03a Compare July 29, 2025 22:09
impl<T> AccessorTask<T, Ctx, Result<()>> for Task {
async fn run(self, accessor: &Accessor<T, Ctx>) -> Result<()> {
let mut tx = self.tx;
let mut tx = GuardedStreamWriter::new(accessor, self.tx);
Copy link
Member

Choose a reason for hiding this comment

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

Think about this in the context of rebasing with wasi:sockets -- is this necessary? Tasks can only be cancelled for two reasons:

  1. The store is dropped - in this case there's no need to do further cleanup
  2. The AbortHandle is triggered, but in this case it's statically known we just forget about that and ignore it.

Given that does WASI actually need Guarded*?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Perhaps we don't need it, but it saves us from having to remember to close the handle explicitly before returning, which isn't too onerous for this simple task, but could become a maintenance hazard later.

wasmtime-wasi-http has some pretty complicated control flow, where keeping track of what to close when without RAII would be a real pain.

@github-actions github-actions bot added the wasmtime:api Related to the API of the `wasmtime` crate itself label Jul 30, 2025
dicej added 2 commits July 30, 2025 08:15
This makes a several changes to how `{Stream,Future}|{Reader,Writer}` work to
make them more efficient and, in some ways, more ergonomic:

- The background tasks have been removed, allowing reads and writes to complete without task context switching.  We now only allocate and use oneshot channels lazily when the other end is not yet ready; this improves real world performance benchmarks (e.g. wasi-http request handling) considerably.

- Instances of `{Stream,Future}Reader` can now be lifted and lowered directly; no need for `Host{Stream,Future}` anymore.

- The type parameter for `Stream{Reader,Writer}` no longer refers to the buffer type -- just the payload type (i.e. `StreamReader<u8>` instead of `StreamReader<Vec<u8>>`), meaning any buffer type may be used for a given read or write operation.  This also means the compiler needs help with type inference less often when calling `Instance::stream`.

- Instances of `{Stream,Future}|{Reader,Writer}` now require access to the store in order to be disposed of properly.  I've added RAII wrapper structs (`WithAccessor[AndValue]`) to help with this, and also updated `Store::drop` and `Instance::run_concurrent` to ensure the store thread-local is set when dropping futures closing over `&Accessor`s.

- In order to ensure that resources containing `{Stream,Future}|{Reader,Writer}` instances are disposed of properly, I've added `LinkerInstance::resource_concurrent` and have updated `wasmtime-wit-bindgen` to use it.  This gives resource drop functions access to a `StoreContextMut` via an `Accessor`, allowing the stream and future handles to be disposed of.
    - In order to make this work, I had to change `Accessor::instance` from a `Instance` to an `Option<Instance>`, which is awkward but temporary since we're planning to remove `Accessor::instance` entirely once we've moved concurrent state from `ComponentInstance` to `Store`.

That problem of disposal is definitely the most awkward part of all this.  In
simple cases, it's easy enough to ensure that read and write handles are
disposed of properly, but both `wasmtime-wasi` and `wasmtime-wasi-http` have
some pretty complicated functions where handles are passed between tasks and/or
stored inside resources, so it can be tricky to ensure proper disposal on all
code paths.  I'm open to ideas for improving this, but I suspect we'll need new
Rust language features (e.g. linear types) to make it truly ergonomic, robust,
and efficient.

While testing the above, I discovered an issue with `Instance::poll_until` such
that it would prematurely give up and return a "deadlock" trap error, believing
that there was no further work to do, even though the future passed to it was
ready to resolve the next time it was polled.  I've fixed this by polling it one
last time and only trapping if it returns pending.

Note that I've moved a few associated functions from `ConcurrentState` to
`Instance` (e.g. `guest_drop_writable` and others) since they now need access to
the store; they're unchanged otherwise.  Apologies for the diff noise.

Finally, I've tweaked how `wasmtime serve` to poll the guest for content before
handing the response to Hyper, which helps performance by ensuring the first
content chunk can be sent with the same TCP packet as the beginning of the
response.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>

fix wasi p3 build and test failures

Signed-off-by: Joel Dice <joel.dice@fermyon.com>

use `ManuallyDrop` instead of `Option` in `Dropper`

This allows us to drop its `value` field in-place, i.e. without moving it,
thereby upholding the `Pin` guarantee.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>

address review comments

- Remove `DropWithStoreAndValue` and friends; go back to taking a `fn() -> T` parameter in `Instance::future` instead
- Make `DropWithStore::drop[_with]` take `&mut self` instead of `self`
- Make `WithAccessor` and `DropWithStore` private
    - Instead, I've added public `Guarded{Stream,Future}{Reader,Writer}` types for RAII
    - and also `{Stream,Future}{Reader,Writer}::close[_with]` methods
- Use RAII in `FutureReader::read` and `FutureWriter::write` to ensure handles are dropped if the `Future` is dropped

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
This avoids unsoundness due to guest realloc calls while there are host embedder
frames on the stack.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
@dicej dicej force-pushed the stream-future-refactor-upstream branch 3 times, most recently from e4049ee to 0014c19 Compare July 30, 2025 15:47
Signed-off-by: Joel Dice <joel.dice@fermyon.com>
@dicej dicej force-pushed the stream-future-refactor-upstream branch from 0014c19 to 74ee080 Compare July 30, 2025 15:49
@dicej
Copy link
Contributor Author

dicej commented Jul 30, 2025

@alexcrichton I believe I've addressed all your feedback so far. Let me know if you have anything else.

BTW, the optimization for lowering flat payloads during host writes is pretty trivial:

diff --git a/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs
index 2809cb6b12..ed56fd7bce 100644
--- a/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs
+++ b/crates/wasmtime/src/runtime/component/concurrent/futures_and_streams.rs
@@ -1777,9 +1777,7 @@ impl Instance {
                     anyhow::Ok(result)
                 };

-                if
-                // TODO: Check if payload is "flat"
-                false {
+                if T::IS_FLAT_TYPE {
                     // Optimize flat payloads (i.e. those which do not require
                     // calling the guest's realloc function) by lowering
                     // directly instead of using a oneshot::channel and
diff --git a/crates/wasmtime/src/runtime/component/func/typed.rs b/crates/wasmtime/src/runtime/component/func/typed.rs
index ad4328fce4..f27120957a 100644
--- a/crates/wasmtime/src/runtime/component/func/typed.rs
+++ b/crates/wasmtime/src/runtime/component/func/typed.rs
@@ -687,6 +687,9 @@ pub unsafe trait ComponentType: Send + Sync {
     #[doc(hidden)]
     const IS_RUST_UNIT_TYPE: bool = false;

+    #[doc(hidden)]
+    const IS_FLAT_TYPE: bool = false;
+
     /// Returns the number of core wasm abi values will be used to represent
     /// this type in its lowered form.
     ///
@@ -1016,6 +1019,8 @@ macro_rules! integers {

             const ABI: CanonicalAbiInfo = CanonicalAbiInfo::$abi;

+            const IS_FLAT_TYPE: bool = true;
+
             fn typecheck(ty: &InterfaceType, _types: &InstanceType<'_>) -> Result<()> {
                 match ty {
                     InterfaceType::$ty => Ok(()),

@alexcrichton alexcrichton added this pull request to the merge queue Jul 30, 2025
Merged via the queue into bytecodealliance:main with commit b447543 Jul 30, 2025
43 checks passed
alexcrichton added a commit to alexcrichton/wasmtime that referenced this pull request Jul 30, 2025
This fixes a minor merge conflict between bytecodealliance#11325 and bytecodealliance#11328 which isn't
currently exercised by in-repo WASI bindings but will be soon once
wasip3-prototyping is finished merging.
alexcrichton added a commit to alexcrichton/wasmtime that referenced this pull request Jul 30, 2025
This commit is a refinement of bytecodealliance#11325 to use `.unwrap()` internally
instead of ignoring errors from dropping futures and streams. Fallible
drop isn't supported in Rust and these shouldn't panic assuming the host
is properly matching handles to stores.
alexcrichton added a commit to alexcrichton/wasmtime that referenced this pull request Jul 30, 2025
This commit is a refinement of bytecodealliance#11325 to use `.unwrap()` internally
instead of ignoring errors from dropping futures and streams. Fallible
drop isn't supported in Rust and these shouldn't panic assuming the host
is properly matching handles to stores.
alexcrichton added a commit to alexcrichton/wasmtime that referenced this pull request Jul 30, 2025
This is a follow-up to bytecodealliance#11325 with a number of cosmetic changes about
the shape of the API and structure of the internals:

* `{Stream,Future}{Reader,Writer}::guard` is now an alternative
  constructor to `Guard*::new` (import fewer types).
* Internally `WithAccessor` and `DropWithStore` are removed in favor of
  direct `Drop for Guard*` impls.
* An `Option` is used to replace `ManuallyDrop` and `unsafe` code.
* `{Stream,Future}{Reader,Writer}::close{,_with}` now take `&mut self`
  instead of `self` to be more composable with `&mut self` arguments
  during `Drop` for other structures (e.g. build-your-own
  drop-with-store).
* The type parameters on `Guard*` are simplified to just `T`, the future
  or stream payload, and `A: AsAccessor`. This helps cut down on the
  complexity of signatures.
* `Guard*` types now have `into_{stream,future}` as an alternative to
  `.into()` which doesn't require type annotations.
github-merge-queue bot pushed a commit that referenced this pull request Jul 30, 2025
* Account for concurrent resource destructors

This fixes a minor merge conflict between #11325 and #11328 which isn't
currently exercised by in-repo WASI bindings but will be soon once
wasip3-prototyping is finished merging.

* Update expanded test expectations
alexcrichton added a commit to alexcrichton/wasmtime that referenced this pull request Jul 30, 2025
This is a follow-up to bytecodealliance#11325 with a number of cosmetic changes about
the shape of the API and structure of the internals:

* `{Stream,Future}{Reader,Writer}::guard` is now an alternative
  constructor to `Guard*::new` (import fewer types).
* Internally `WithAccessor` and `DropWithStore` are removed in favor of
  direct `Drop for Guard*` impls.
* An `Option` is used to replace `ManuallyDrop` and `unsafe` code.
* `{Stream,Future}{Reader,Writer}::close{,_with}` now take `&mut self`
  instead of `self` to be more composable with `&mut self` arguments
  during `Drop` for other structures (e.g. build-your-own
  drop-with-store).
* The type parameters on `Guard*` are simplified to just `T`, the future
  or stream payload, and `A: AsAccessor`. This helps cut down on the
  complexity of signatures.
* `Guard*` types now have `into_{stream,future}` as an alternative to
  `.into()` which doesn't require type annotations.
github-merge-queue bot pushed a commit that referenced this pull request Jul 30, 2025
* Don't support fallible drop in futures_and_streams

This commit is a refinement of #11325 to use `.unwrap()` internally
instead of ignoring errors from dropping futures and streams. Fallible
drop isn't supported in Rust and these shouldn't panic assuming the host
is properly matching handles to stores.

* Fix build of wasmtime-wasi

* Don't empty the table during store destruction

Leave it around while futures/fibers are being manually dropped so any
destructors associated there get access to the table (as required by
streams/futures/etc).

* Remove unused import
alexcrichton added a commit to alexcrichton/wasmtime that referenced this pull request Jul 30, 2025
This is a follow-up to bytecodealliance#11325 with a number of cosmetic changes about
the shape of the API and structure of the internals:

* `{Stream,Future}{Reader,Writer}::guard` is now an alternative
  constructor to `Guard*::new` (import fewer types).
* Internally `WithAccessor` and `DropWithStore` are removed in favor of
  direct `Drop for Guard*` impls.
* An `Option` is used to replace `ManuallyDrop` and `unsafe` code.
* `{Stream,Future}{Reader,Writer}::close{,_with}` now take `&mut self`
  instead of `self` to be more composable with `&mut self` arguments
  during `Drop` for other structures (e.g. build-your-own
  drop-with-store).
* The type parameters on `Guard*` are simplified to just `T`, the future
  or stream payload, and `A: AsAccessor`. This helps cut down on the
  complexity of signatures.
* `Guard*` types now have `into_{stream,future}` as an alternative to
  `.into()` which doesn't require type annotations.
github-merge-queue bot pushed a commit that referenced this pull request Jul 31, 2025
This is a follow-up to #11325 with a number of cosmetic changes about
the shape of the API and structure of the internals:

* `{Stream,Future}{Reader,Writer}::guard` is now an alternative
  constructor to `Guard*::new` (import fewer types).
* Internally `WithAccessor` and `DropWithStore` are removed in favor of
  direct `Drop for Guard*` impls.
* An `Option` is used to replace `ManuallyDrop` and `unsafe` code.
* `{Stream,Future}{Reader,Writer}::close{,_with}` now take `&mut self`
  instead of `self` to be more composable with `&mut self` arguments
  during `Drop` for other structures (e.g. build-your-own
  drop-with-store).
* The type parameters on `Guard*` are simplified to just `T`, the future
  or stream payload, and `A: AsAccessor`. This helps cut down on the
  complexity of signatures.
* `Guard*` types now have `into_{stream,future}` as an alternative to
  `.into()` which doesn't require type annotations.
bongjunj pushed a commit to prosyslab/wasmtime that referenced this pull request Oct 20, 2025
…dealliance#11325)

* refactor `{Stream,Future}|{Reader,Writer}` APIs and internals

This makes a several changes to how `{Stream,Future}|{Reader,Writer}` work to
make them more efficient and, in some ways, more ergonomic:

- The background tasks have been removed, allowing reads and writes to complete without task context switching.  We now only allocate and use oneshot channels lazily when the other end is not yet ready; this improves real world performance benchmarks (e.g. wasi-http request handling) considerably.

- Instances of `{Stream,Future}Reader` can now be lifted and lowered directly; no need for `Host{Stream,Future}` anymore.

- The type parameter for `Stream{Reader,Writer}` no longer refers to the buffer type -- just the payload type (i.e. `StreamReader<u8>` instead of `StreamReader<Vec<u8>>`), meaning any buffer type may be used for a given read or write operation.  This also means the compiler needs help with type inference less often when calling `Instance::stream`.

- Instances of `{Stream,Future}|{Reader,Writer}` now require access to the store in order to be disposed of properly.  I've added RAII wrapper structs (`WithAccessor[AndValue]`) to help with this, and also updated `Store::drop` and `Instance::run_concurrent` to ensure the store thread-local is set when dropping futures closing over `&Accessor`s.

- In order to ensure that resources containing `{Stream,Future}|{Reader,Writer}` instances are disposed of properly, I've added `LinkerInstance::resource_concurrent` and have updated `wasmtime-wit-bindgen` to use it.  This gives resource drop functions access to a `StoreContextMut` via an `Accessor`, allowing the stream and future handles to be disposed of.
    - In order to make this work, I had to change `Accessor::instance` from a `Instance` to an `Option<Instance>`, which is awkward but temporary since we're planning to remove `Accessor::instance` entirely once we've moved concurrent state from `ComponentInstance` to `Store`.

That problem of disposal is definitely the most awkward part of all this.  In
simple cases, it's easy enough to ensure that read and write handles are
disposed of properly, but both `wasmtime-wasi` and `wasmtime-wasi-http` have
some pretty complicated functions where handles are passed between tasks and/or
stored inside resources, so it can be tricky to ensure proper disposal on all
code paths.  I'm open to ideas for improving this, but I suspect we'll need new
Rust language features (e.g. linear types) to make it truly ergonomic, robust,
and efficient.

While testing the above, I discovered an issue with `Instance::poll_until` such
that it would prematurely give up and return a "deadlock" trap error, believing
that there was no further work to do, even though the future passed to it was
ready to resolve the next time it was polled.  I've fixed this by polling it one
last time and only trapping if it returns pending.

Note that I've moved a few associated functions from `ConcurrentState` to
`Instance` (e.g. `guest_drop_writable` and others) since they now need access to
the store; they're unchanged otherwise.  Apologies for the diff noise.

Finally, I've tweaked how `wasmtime serve` to poll the guest for content before
handing the response to Hyper, which helps performance by ensuring the first
content chunk can be sent with the same TCP packet as the beginning of the
response.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>

fix wasi p3 build and test failures

Signed-off-by: Joel Dice <joel.dice@fermyon.com>

use `ManuallyDrop` instead of `Option` in `Dropper`

This allows us to drop its `value` field in-place, i.e. without moving it,
thereby upholding the `Pin` guarantee.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>

address review comments

- Remove `DropWithStoreAndValue` and friends; go back to taking a `fn() -> T` parameter in `Instance::future` instead
- Make `DropWithStore::drop[_with]` take `&mut self` instead of `self`
- Make `WithAccessor` and `DropWithStore` private
    - Instead, I've added public `Guarded{Stream,Future}{Reader,Writer}` types for RAII
    - and also `{Stream,Future}{Reader,Writer}::close[_with]` methods
- Use RAII in `FutureReader::read` and `FutureWriter::write` to ensure handles are dropped if the `Future` is dropped

Signed-off-by: Joel Dice <joel.dice@fermyon.com>

* lower host stream/future writes in background task

This avoids unsoundness due to guest realloc calls while there are host embedder
frames on the stack.

Signed-off-by: Joel Dice <joel.dice@fermyon.com>

* fix `tcp.rs` regressions

Signed-off-by: Joel Dice <joel.dice@fermyon.com>

---------

Signed-off-by: Joel Dice <joel.dice@fermyon.com>
bongjunj pushed a commit to prosyslab/wasmtime that referenced this pull request Oct 20, 2025
* Account for concurrent resource destructors

This fixes a minor merge conflict between bytecodealliance#11325 and bytecodealliance#11328 which isn't
currently exercised by in-repo WASI bindings but will be soon once
wasip3-prototyping is finished merging.

* Update expanded test expectations
bongjunj pushed a commit to prosyslab/wasmtime that referenced this pull request Oct 20, 2025
…11351)

* Don't support fallible drop in futures_and_streams

This commit is a refinement of bytecodealliance#11325 to use `.unwrap()` internally
instead of ignoring errors from dropping futures and streams. Fallible
drop isn't supported in Rust and these shouldn't panic assuming the host
is properly matching handles to stores.

* Fix build of wasmtime-wasi

* Don't empty the table during store destruction

Leave it around while futures/fibers are being manually dropped so any
destructors associated there get access to the table (as required by
streams/futures/etc).

* Remove unused import
bongjunj pushed a commit to prosyslab/wasmtime that referenced this pull request Oct 20, 2025
This is a follow-up to bytecodealliance#11325 with a number of cosmetic changes about
the shape of the API and structure of the internals:

* `{Stream,Future}{Reader,Writer}::guard` is now an alternative
  constructor to `Guard*::new` (import fewer types).
* Internally `WithAccessor` and `DropWithStore` are removed in favor of
  direct `Drop for Guard*` impls.
* An `Option` is used to replace `ManuallyDrop` and `unsafe` code.
* `{Stream,Future}{Reader,Writer}::close{,_with}` now take `&mut self`
  instead of `self` to be more composable with `&mut self` arguments
  during `Drop` for other structures (e.g. build-your-own
  drop-with-store).
* The type parameters on `Guard*` are simplified to just `T`, the future
  or stream payload, and `A: AsAccessor`. This helps cut down on the
  complexity of signatures.
* `Guard*` types now have `into_{stream,future}` as an alternative to
  `.into()` which doesn't require type annotations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

wasi Issues pertaining to WASI wasmtime:api Related to the API of the `wasmtime` crate itself

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants