WASI 0.3

WASI 0.3

WASI 0.3 is official, and async is now native to WebAssembly Components. The WASI Subgroup voted to ratify WASI 0.3.0, rebasing WASI onto the WebAssembly Component Model’s async primitives. The 0.3.0 specification is now stable, and runtime and toolchain support is landing now. WASI 0.3 正式发布,异步(async)现已成为 WebAssembly 组件的原生特性。WASI 工作组投票通过了 WASI 0.3.0 标准,将 WASI 重新构建在 WebAssembly 组件模型的异步原语之上。0.3.0 规范现已稳定,运行时和工具链支持也正在陆续落地。

The work that wasi:io in WASI 0.2 used to do (pollables, input-streams, output-streams) is now part of the canonical ABI, where the Component Model now offers these primitives natively. As a consequence of that, most of the changes from WASI 0.2 to 0.3 are entirely mechanical and significantly simplify the signatures we had before. The new async primitives are part of the Component Model’s canonical ABI, enabling bindings generators to emit idiomatic async bindings for their given language. WASI 0.2 中 wasi:io 所承担的工作(如 pollables、input-streams、output-streams)现在已成为规范 ABI(Canonical ABI)的一部分,组件模型现已原生提供这些原语。因此,从 WASI 0.2 到 0.3 的大部分变更都是机械性的,并显著简化了之前的函数签名。新的异步原语作为组件模型规范 ABI 的一部分,使得绑定生成器能够为各自的编程语言生成符合惯用法的异步绑定。

The Component Model Async ABI

组件模型异步 ABI

In WASI 0.2 each component needed its own event loop/async runtime. That meant that individual components could be run on a host, but there was no way for those event loops to coordinate with one another. If a component used streaming or async APIs, it couldn’t be composed with any other components. 在 WASI 0.2 中,每个组件都需要自己的事件循环或异步运行时。这意味着虽然单个组件可以在宿主上运行,但这些事件循环之间无法相互协调。如果一个组件使用了流式或异步 API,它就无法与其他组件进行组合。

WASI 0.3 makes it so the host is now the one in charge of managing the one event loop that is shared by all components. This is enabled by adding stream<T>, future<T>, and async as first-class constructs to the canonical ABI: stream<T> and future<T> function like resource types: each is an owned handle, passing one across a component boundary transfers ownership from caller to callee. Unlike resource types, they can’t be borrowed. WASI 0.3 将事件循环的管理权交给了宿主,由宿主维护一个供所有组件共享的事件循环。通过将 stream<T>future<T>async 作为一等公民引入规范 ABI,实现了这一目标:stream<T>future<T> 的功能类似于资源类型(resource types):它们都是拥有所有权的句柄,跨组件边界传递时会将所有权从调用者转移给被调用者。与资源类型不同的是,它们不能被借用(borrowed)。

The runtime, not each component, drives the scheduling. When a value has been delivered to a future, the runtime schedules whichever task is awaiting it, even if it was passed through multiple component boundaries. The writer that delivers that value might be the host, another component, or even the same component that holds the read end. 调度由运行时而非各个组件驱动。当一个值被传递给 future 时,运行时会调度正在等待该值的任务,即使该值经过了多个组件边界。交付该值的写入者可能是宿主、另一个组件,甚至是持有读取端的同一个组件。

The async model is completion-based, not readiness-based. This is similar to the ultra-efficient Linux io_uring and Windows’ IOCP/IoRing APIs. An epoll/kqueue-style readiness API can be emulated on top of this for programs which need the compatibility. Components export and import async funcs directly. Gone is the three-step start-foo / finish-foo / subscribe dance from WASI 0.2. 该异步模型基于“完成”(completion-based)而非“就绪”(readiness-based)。这类似于高效的 Linux io_uring 和 Windows 的 IOCP/IoRing API。对于需要兼容性的程序,可以在此基础上模拟 epoll/kqueue 风格的就绪 API。组件现在可以直接导出和导入异步函数,WASI 0.2 中繁琐的 start-foo / finish-foo / subscribe 三步走流程已成为历史。

Changes to the WASI interfaces

WASI 接口的变更

Most of the changes in the 0.3 interfaces are entirely mechanical. WASI 0.2 had to perform some acrobatics to make async work, but now that async is native to the component model we can write the same things we did before but much more ergonomically. Here is an overview of the patterns we were encoding in WASI 0.2 with the wasi:io package, and what those patterns now look like in 0.3 with Component Model async: 0.3 接口中的大部分变更都是机械性的。WASI 0.2 为了实现异步不得不进行一些“杂技”式的操作,但现在异步已成为组件模型的原生特性,我们可以用更符合人体工程学的方式实现同样的功能。以下是 WASI 0.2 中使用 wasi:io 包编码的模式与 0.3 中使用组件模型异步模式的对比:

WASI 0.2 (wasi:io)WASI 0.3 (Component Model)
resource pollablefuture<T>
resource input-streamstream<u8>
resource output-streamstream<u8> (written-to direction)
poll(list<pollable>)await on a future (runtime-handled)
subscribe() on resourcereturn a future<...> from the call
start-foo / finish-foofoo: async func(...)

A problem with WASI 0.2 was that it surfaced terminal errors inline on each read call on the stream. That meant callers only learned the outcome if they kept reading. If readers stopped early they could not distinguish between a stream close and an error. In WASI 0.3 streams now return an additional future which resolves independently of how much of the stream is consumed, solving the stream status problem of WASI 0.2: WASI 0.2 的一个问题是,它在流的每次读取调用中内联抛出终止错误。这意味着调用者只有在持续读取时才能获知结果。如果读取者提前停止,就无法区分是流关闭还是发生了错误。在 WASI 0.3 中,流现在会返回一个额外的 future,它独立于流的消费进度进行解析,从而解决了 WASI 0.2 的流状态问题:

// WASI 0.2
read-via-stream: func() -> result<input-stream, error-code>;

// WASI 0.3
read-via-stream: func() -> tuple<stream<u8>, future<result<_, error-code>>>;

Changes to language bindings

语言绑定的变更

One of the super powers of the component model is that it makes it trivial to create bindings to and from other languages. With the addition of first-class async it means guest binding generators can leverage that to create async bindings that feel native to that language. Take for example the wasi:http/handler interface. This interface exposes one function, handle, which is marked async: 组件模型的一大优势在于,它使得创建跨语言绑定变得非常简单。随着一等公民异步特性的加入,客体(guest)绑定生成器可以利用这一点,创建出符合该语言原生习惯的异步绑定。以 wasi:http/handler 接口为例,该接口暴露了一个标记为 async 的函数 handle

interface handler {
  handle: async func(request: request) -> result<response, error-code>;
}

To implement an HTTP server in Rust with this, we can use the wit-bindgen crate. This maps the interface handler to a trait Guest, and maps the handle: async func to an async fn handle: 要使用 Rust 实现 HTTP 服务器,我们可以使用 wit-bindgen crate。它将接口 handler 映射为 trait Guest,并将 handle: async func 映射为 async fn handle

use wasi::http::types::{ErrorCode, Request, Response};

impl Guest for Component {
  async fn handle(request: Request) -> Result<Response, ErrorCode> {
    // ...
  }
}

Async support for guest binding generators is also in-progress for many languages including Python, JavaScript, C#, and C. All of these languages rely on stackless coroutines. But the Component Model’s async ABI was designed from the ground up to accommodate both stackful and stackless coroutines side-by-side. An example of a language with those is Go. Instead of exposing async and non-async functions, Go’s runtime is able to convert synchronous-looking calls to async calls, and provides concurrent execution via virtual threads called “goroutines”. 包括 Python、JavaScript、C# 和 C 在内的多种语言的客体绑定生成器异步支持工作正在进行中。这些语言大多依赖无栈协程(stackless coroutines)。但组件模型的异步 ABI 从设计之初就考虑到了同时兼容有栈和无栈协程。Go 语言就是一个例子。Go 的运行时无需显式暴露异步和非异步函数,而是能够将看起来同步的调用转换为异步调用,并通过称为“goroutines”的虚拟线程提供并发执行能力。

Using componentize-go we can implement an HTTP server by exporting a func Handle. This enables streaming bodies via goroutines that do blocking calls. The runtime then parks the goroutine at the ABI boundary and resumes it when the stream is ready, without blocking the rest of the program: 使用 componentize-go,我们可以通过导出 Handle 函数来实现 HTTP 服务器。这使得通过执行阻塞调用的 goroutine 来实现流式传输主体成为可能。运行时会在 ABI 边界处挂起 goroutine,并在流就绪时恢复它,而不会阻塞程序的其余部分:

package export_wasi_http_handler

import (
  . "wit_component/wasi_http_types"
  . "go.bytecodealliance.org/pkg/wit/types"
)

func Handle(request *Request) Result[*Response, ErrorCode] {
  tx, rx := MakeStreamU8() // ← 1. Create a channel pair
  go func() { // ← 2. Spawn a virtual thread
    defer tx.Drop()
    tx.WriteAll([]uint8("Hello, world!")) // ← 3. Write into the channel
  }()
  response, send := ResponseNew(
    FieldsFromList([]Tuple2[string, []byte]{
      {F0: "content-type", F1: []byte("text/plain")},
    }).Ok(),
    Some(rx), // ← 5. Pass the receiver as the HTTP body
    trailersFuture(),
  )
  send.Drop()
  return Ok[*Response, ErrorCode](response) // ← 6. Return the HTTP response
}