-
Notifications
You must be signed in to change notification settings - Fork 83
Add Async, Streams, and Futures concepts page and Migrating from WASI P2 to WASI P3 guide #352
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
63dc70c
3ea8111
58a60a2
fc2a837
c91de06
35629a7
ffec37a
c56583f
3b49882
2695ed4
edd2d2d
84aab9e
707b97e
c041079
120f456
9860cc1
58aabb2
2275fbf
63df4c1
2d0f794
d43e89e
3421c7a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| # Native Async with WASI 0.3 | ||
|
|
||
| WASI 0.3 adds new Canonical ABI primitives to the Component Model that enable async functionality. Components that target WASI 0.3 can use the new features in their WIT files: | ||
| * `async func` | ||
| * `stream<T>` | ||
| * `future<T>` | ||
|
|
||
| These new types let interfaces express asynchronous operations that compose across component boundaries. | ||
|
|
||
| For migration mechanics (e.g., how a WASI 0.2 component maps onto these primitives) see [Migrating from WASI 0.2 to WASI 0.3](./migrating-to-p3.md). | ||
|
|
||
| For a closer look at the WASI 0.3 release, including a full per-interface diff, see [WASI 0.3](https://wasi.dev/releases/wasi-p3) on WASI.dev. | ||
|
|
||
| This page focuses on the Component Model concepts themselves. | ||
|
|
||
| > [!NOTE] | ||
| > WASI 0.3 builds on WASI 0.2 rather than replacing it. Runtimes can host both versions side by side, and a 0.3 host can polyfill 0.2 imports at the boundary, so applications can migrate incrementally as toolchains and dependencies land 0.3 support. | ||
|
|
||
| ## The async problem that WASI 0.3 solves | ||
|
|
||
| The Component Model's Canonical ABI defines how typed values cross component boundaries. Until WASI 0.3, that vocabulary had no notion of suspension or asynchronous completion; every interface call returned synchronously, and asynchronous I/O was modeled with resources (`pollable` for readiness, `input-stream` and `output-stream` for byte channels) scoped to whichever component obtained them. | ||
|
|
||
| That arrangement holds up for two-party interactions, but it falters once components are composed in a chain. If a component awaits work that another component delegates further, the readiness signal has to travel back up the chain. When readiness is expressed as a resource scoped to a single component, the intermediate component is stuck running an event loop purely to forward the wake-up to its caller; the runtime cannot help, because the resource doesn't live in a place the runtime can reach across. This is sometimes called the **sandwich problem**: an async vocabulary that describes a single hop just fine but cannot propagate readiness past one. | ||
|
|
||
| Native async primitives help close this expressivity gap. With updated Component ABI mechanics that enable `async func`, `stream<T>`, and `future<T>` available at the WIT level, scheduling and wake-up propagation become the runtime's job rather than any individual component's. | ||
|
|
||
| Components can pass futures and streams along without keeping their own event loops running to relay readiness, as was necessary with WASI 0.2. | ||
|
|
||
| ## Async functions, Streams, and Futures | ||
|
|
||
| ### Async Functions (`async func`) | ||
|
|
||
| A WIT function declared `async` tells the runtime that the call may suspend before producing its result. The Canonical ABI handles the suspension and resumption; the guest doesn't see a `pollable`, and the host doesn't see a polling loop. | ||
|
|
||
| ```diff | ||
| - handle: func(request: request) -> result<response, error-code>; | ||
| + handle: async func(request: request) -> result<response, error-code>; | ||
| ``` | ||
|
|
||
| Code generated from the WIT picks up each language's natural async idiom: `async fn` in Rust, a `Promise`-returning function in JavaScript, a coroutine in Python. | ||
|
|
||
| ### Streams (`stream<T>`) | ||
|
|
||
| A typed, asynchronous channel for a sequence of `T` values. Crucially, `stream<T>` is a Canonical ABI *value*, not a resource (as opposed to WASI 0.2) -- it can be returned from a call, accepted as a parameter, and handed from one component to another without giving up ownership of the underlying buffer. | ||
|
|
||
| The same value can also be passed straight through one or more intermediate components without those components having to relay any wake-ups. | ||
|
|
||
| ```wit | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about using a simpler example here (and the diff idea)? For example
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hey @ericgregory you think we should skip this one? IMO this example is kind of complicated for just a basic intro to streams (we do have the wasi mapping section later), but I don't feel strongly either way |
||
| read-via-stream: func() -> tuple<stream<u8>, future<result<_, error-code>>>; | ||
| ``` | ||
|
|
||
| ### Futures (`future<T>`) | ||
|
|
||
| A typed handle for a single value that will become available later. Like `stream<T>`, `future<T>` is a value rather than a resource, so it crosses component boundaries the same way a primitive does. | ||
|
|
||
| Note that synchronous functions which return `future<T>`s *cannot* block; the caller can await the result when it needs it. | ||
|
|
||
| ```wit | ||
| write-via-stream: func(data: stream<u8>) -> future<result<_, error-code>>; | ||
| ``` | ||
|
|
||
| ## A look at async patterns in WASI 0.3 | ||
|
|
||
| ### Stream plus terminal future | ||
|
|
||
| Reads return both a data channel and a completion handle, packed into a tuple ([`read-via-stream`](https://github.com/WebAssembly/wasi-filesystem/blob/main/wit/types.wit#L308) in `wasi-filesystem`): | ||
|
|
||
| ```wit | ||
| read-via-stream: func() -> tuple<stream<u8>, future<result<_, error-code>>>; | ||
| ``` | ||
|
|
||
| The two halves are independent. The caller can consume the stream eagerly, sample it, or drop it part-way through; either way the future resolves once the operation has terminated, carrying the success-or-failure outcome. The same shape appears in stdin, filesystem reads, TCP receives, and directory listings. | ||
|
|
||
| ### Stream parameter, future return | ||
|
|
||
| Writes use the symmetric shape: the guest supplies the data as a `stream<u8>` parameter, and the host returns a `future` that resolves once it has consumed the stream. Stdout, stderr, filesystem writes, and TCP sends all follow this shape ([`write-via-stream`](https://github.com/WebAssembly/wasi-filesystem/blob/main/wit/types.wit#L320) in `wasi-filesystem`): | ||
|
|
||
| ```wit | ||
| write-via-stream: func(data: stream<u8>) -> future<result<_, error-code>>; | ||
| ``` | ||
|
|
||
| ## Where to go next | ||
|
|
||
| For an end-to-end Rust example that uses these primitives in practice, see [Creating Runnable Components in Rust](../language-support/creating-runnable-components/rust.md). For runtime support and CLI flags, see [Wasmtime](../running-components/wasmtime.md). For the WIT syntax in detail, see [WIT Reference](./wit.md). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| # Migrating from WASI 0.2 to WASI 0.3 | ||
|
|
||
| WASI 0.3 reshapes WASI's interfaces around the [native async primitives](./async.md) `async func`, `stream<T>`, and `future<T>`. Most of the changes in `wasi:cli`, `wasi:http`, `wasi:filesystem`, and `wasi:sockets` are consequences of moving to these primitives. | ||
|
|
||
| This page covers the mapping between concepts in WASI 0.2 and WASI 0.3. For a WIT-level comparison of every WASI 0.3 interface, see [WASI 0.3](https://wasi.dev/releases/wasi-p3) on WASI.dev. | ||
|
|
||
| ## Do you need to migrate? | ||
|
|
||
| Not immediately -- WASI 0.2 can be used in hosts just as before, WASI 0.3 is a purely additive change. | ||
|
|
||
| Separately, WASI 0.3 runtimes can polyfill 0.2 by mapping 0.2 imports onto native 0.3 primitives at the host boundary, and Wasmtime's `wasmtime serve` already runs both 0.3 and 0.2 components from the same binary, dispatching per component. Migration is the right call when you want: | ||
|
|
||
| - Composable async across component boundaries (the [sandwich problem](./async.md#the-async-problem-that-wasi-03-solves) goes away). | ||
| - The newer interface shapes — in particular, `wasi:http`'s collapse of nine resources down to two. | ||
| - First-class support in 0.3-targeted toolchains as they continue to land. | ||
|
|
||
| ## Concept mapping | ||
|
|
||
| WASI 0.3 replaces every `wasi:io` resource with a Canonical ABI primitive. The translation is mostly one-to-one: | ||
|
|
||
| | WASI 0.2 (`wasi:io`) | WASI 0.3 (Component Model) | | ||
| | -------------------------------- | ---------------------------------------- | | ||
| | `resource pollable` | `future<T>` | | ||
| | `resource input-stream` | `stream<u8>` | | ||
| | `resource output-stream` | `stream<u8>` (passed *into* the call) | | ||
| | `poll(list<pollable>)` | `await` on a future | | ||
| | `subscribe()` on a resource | return a `future` from the call | | ||
| | `start-foo` / `finish-foo` | a single `func` or `async func` | | ||
|
|
||
| ## What changed in WIT | ||
|
|
||
| ### Stream-plus-future for reads | ||
|
|
||
| A WASI 0.2 read call returned a single `input-stream` resource and surfaced terminal errors only as you consumed it. WASI 0.3 splits those concerns: the call returns a `stream<u8>` for the data and a `future<result<_, error-code>>` for the outcome, packed into a tuple. | ||
|
|
||
| ```wit | ||
| // WASI 0.2 (filesystem read) | ||
| read-via-stream: func(offset: filesize) -> result<input-stream, error-code>; | ||
|
|
||
| // WASI 0.3 (filesystem read) | ||
| read-via-stream: func(offset: filesize) -> tuple<stream<u8>, future<result<_, error-code>>>; | ||
| ``` | ||
|
|
||
| In WASI 0.3, the caller does not have to drain the stream to learn whether the read finished cleanly; the future resolves either way. | ||
|
|
||
| ### Write-direction flip | ||
|
|
||
| WASI 0.2 write paths handed a guest some host-owned resource (an `output-stream`) and let the guest push bytes into it. WASI 0.3 inverts that: the guest supplies the data as a `stream<u8>` value, and the host returns a `future` that resolves once it has finished consuming the stream. | ||
|
|
||
| ```wit | ||
| // WASI 0.2: receive an output-stream resource, write into it | ||
| get-stdout: func() -> output-stream; | ||
|
|
||
| // WASI 0.3: pass a stream value in, receive a completion future | ||
| write-via-stream: func(data: stream<u8>) -> future<result<_, error-code>>; | ||
| ``` | ||
|
|
||
| ### Two-step calls collapsed | ||
|
|
||
| WASI 0.2 modeled operations that could suspend as a `start-foo` / `finish-foo` pair, with a `pollable` for readiness in between. WASI 0.3 collapses each pair into a single call: | ||
|
|
||
| ```wit | ||
| // WASI 0.2 | ||
| start-connect: func(network: borrow<network>, remote-address: ip-socket-address) -> result<_, error-code>; | ||
| finish-connect: func() -> result<tuple<input-stream, output-stream>, error-code>; | ||
|
|
||
| // WASI 0.3 | ||
| connect: async func(remote-address: ip-socket-address) -> result<_, error-code>; | ||
| ``` | ||
|
|
||
| The collapsed call is `async func` when the operation needs to suspend in the host (such as `connect`); operations that historically only used the two-step shape for non-blocking dispatch may collapse to plain `func` instead (`bind`, `listen`). | ||
|
|
||
| ## Interface highlights | ||
|
|
||
| The complete per-interface diff lives on [WASI 0.3](https://wasi.dev/releases/wasi-p3#what-changed-in-each-interface) at WASI.dev. The three changes most likely to drive migration work are: | ||
|
|
||
| - **`wasi:io` is gone.** The package has no 0.3.0 release. Every resource it exposed (`pollable`, `input-stream`, `output-stream`) is replaced by a Component Model primitive, per the [concept mapping](#concept-mapping) above. | ||
| - **`wasi:http` collapses from nine resources to two.** The incoming/outgoing × request/response/body matrix plus `future-trailers`, `future-incoming-response`, and `response-outparam` all become `request` and `response`, with `stream<u8>` bodies and a `future` for trailers. The handler is now an `async func`: | ||
|
|
||
| ```wit | ||
| // WASI 0.2 | ||
| handle: func(request: incoming-request, response-out: response-outparam); | ||
|
|
||
| // WASI 0.3 | ||
| handle: async func(request: request) -> result<response, error-code>; | ||
| ``` | ||
|
|
||
| The `proxy` world is replaced by `service`, and a new `middleware` world both imports and exports the handler. | ||
| - **`wasi:sockets` drops its `network` resource.** Network access is granted at the world level instead of being threaded through every `bind`, `connect`, and DNS lookup. The seven WASI 0.2 socket interfaces consolidate into one `types` interface plus `ip-name-lookup`, and TCP `listen` returns `stream<tcp-socket>` directly instead of requiring a separate `accept` loop. | ||
|
|
||
| Smaller per-interface changes — filesystem methods becoming `async func`, the `wasi:clocks` rename pass (`wall-clock` → `system-clock`, `datetime` → `instant`), the `max-len` rename in `wasi:random`, the new shared `wasi:cli/types` interface — are documented in the WASI.dev page linked above. | ||
|
|
||
| ## Tooling requirements | ||
|
|
||
| | Tool | Minimum | Notes | | ||
| | ------------- | --------------------------------------------------------------- | ------------------------------------------------------------------- | | ||
| | Wasmtime | 46+ | WASI 0.3 and `component-model-async` are on by default. | | ||
| | `wit-bindgen` | 0.46+ | Use the `async` feature for 0.3 binding generation. | | ||
| | jco | latest | 0.3 host bindings ship in the `preview3-shim` package. | | ||
| | `wkg` | 0.15+ | Required to fetch `wasi:cli@0.3.0` and related packages. | | ||
| | Rust | nightly | Current stable bundles a `wasm-component-ld` too old for 0.3 outputs of `wit-bindgen` 0.58. | | ||
|
|
||
| ## Further reading | ||
|
|
||
| - [Async, Streams, and Futures](./async.md) — the conceptual foundation | ||
| - [Creating Runnable Components in Rust](../language-support/creating-runnable-components/rust.md) — worked Rust example with the 0.3 `async fn run()` pattern | ||
| - [WASI 0.3](https://wasi.dev/releases/wasi-p3) on WASI.dev — full WIT-level diff per interface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be nice to add a block/info/did-you-know? block here to make clear that 0.3 builds on 0.2, and they can be used interchangably for gradual migration (i.e. it's not that 0.2 is dead)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @ericgregory I think this one got hidden by GH!