wasmtime serve ってなに？

## 概要

この記事では wasmtime v14.0.3 から利用できるようになったサブコマンドである `wasmtime serve` について調査した内容を記載します。

Wasm コンポーネントモデルについて調査していると、コンポーネントモデルのドキュメントに HTTP コンポーネントを実行する方法が紹介されていました。
Web アプリケーションでの活用方法を模索している私としては `HTTP Proxy World` などの仕組みに興味があったので、どのような仕様により実装されているかを確認しました。

- [Running HTTP components with Wasmtime - The WebAssembly Component Model](https://component-model.bytecodealliance.org/runtimes/wasmtime.html#running-http-components-with-wasmtime)

## どんなことができるのか

それでは `wasmtime serve` サブコマンドでどんなことができるかを確認していきます。

`wasmtime serve` は wasi-http の [HTTP proxy world](https://github.com/WebAssembly/wasi-http/blob/v0.2.0-rc-2023-11-10/wit/proxy.wit#L7-L33)を実装したコンポーネントを実行するためのサブコマンドとなっています。そのため引数の Wasm ファイルには HTTP Proxy World の実装と公開が必要です。

実装のために必要なインタフェースである WIT は以下の wasi-http のリポジトリに配置してあるのでこの WIT をもとにコードを実装します。

- [wasi-http/wit at v0.2.0-rc-2023-11-10 · WebAssembly/wasi-http](https://github.com/WebAssembly/wasi-http/tree/v0.2.0-rc-2023-11-10/wit)

wasi-http は [wasi-cloud-core](https://github.com/WebAssembly/wasi-cloud-core) の 1 つで、クラウドプロバイダーやプラットフォーマーが提供するサービスと連携するための汎用的なインタフェースなどが定義されています。その中の HTTP Proxy World では、コアとなる `accept()` ループをホスト側の処理とすることで、HTTP リクエストに応じてホストが動的に Wasm インスタンスをスピンアップできる仕組みとなっています。これは柔軟なオートスケーリング（サーバーレス含む）をサポートすることを意図しています。

ここでは一部のソースコードを抜粋したものを利用して実装を確認します。完全なコードは [watawuwu/blog-wasmtime-serve](https://github.com/watawuwu/blog-wasmtime-serve) を参照してください。この記事のおまけとして、後ほど 2023 年 12 月時点での最新バージョンである wasmtime v15.0.1 での動作手順も記載しています。

Rust の場合は以下のような `incoming-handler` インタフェースを持つ `handle` 関数を実装します。この辺りは FaaS を試したことがある方は HTTP イベントをトリガーとする関数と同じような感覚で実装できると思います。

```rs:lib.rs
impl bindings::exports::wasi::http::incoming_handler::Guest for Component {
    fn handle(_request: IncomingRequest, outparam: ResponseOutparam) {
        let hdrs = Headers::new();
        let resp = OutgoingResponse::new(hdrs);
        resp.set_status_code(200).expect("fail to set status code");
        let body = resp.body().expect("fail to get body");
        ResponseOutparam::set(outparam, Ok(resp));

        let out = body.write().expect("outgoing stream");
        out.blocking_write_and_flush(b"Hello\n")
            .expect("fail to write/flush");

        drop(out);
        OutgoingBody::finish(body, None).expect("fail to finish");
    }
}
```

このソースコードを `cargo component build` でビルドすると、Wasm コンポーネントのファイルが作成されます。

`wasmtime serve` の引数にビルドしたコンポーネントのファイルを指定し起動すると Web サーバーが起動しデフォルトでは `0.0.0.0:8080` でリッスンします。
試しに HTTP クライアントツールなどで HTTP リクエストすることで通信の確認ができます。

```shell
$ wasmtime serve hello.wasm
Serving HTTP on http://0.0.0.0:8080/

$ xh http://0.0.0.0:8080/
HTTP/1.1 200 OK
Date: Wed, 06 Dec 2023 06:46:25 GMT
Transfer-Encoding: chunked

Hello
```


`wasmtime serve` の場合は、wasmtime 自身が Web サーバーを起動することでサービスを提供、またはエミュレートしていることがソースコードで確認できます。

[Listening: wasmtime/src/commands/serve.rs](https://github.com/bytecodealliance/wasmtime/blob/v15.0.1/src/commands/serve.rs#L254-L280)
```rs:serve.rs
        let listener = tokio::net::TcpListener::bind(self.addr).await?;

        eprintln!("Serving HTTP on http://{}/", listener.local_addr()?);

        let _epoch_thread = if let Some(timeout) = self.run.common.wasm.timeout {
            Some(EpochThread::spawn(timeout, engine.clone()))
        } else {
            None
        };

        log::info!("Listening on {}", self.addr);

        let handler = ProxyHandler::new(self, engine, instance);

        loop {
            let (stream, _) = listener.accept().await?;
            let h = handler.clone();
            tokio::task::spawn(async move {
                if let Err(e) = http1::Builder::new()
                    .keep_alive(true)
                    .serve_connection(stream, h)
                    .await
                {
                    eprintln!("error: {e:?}");
                }
            });
        }
```

そして HTTP リクエストのトリガーが発生すると必要なオブジェクトを作成し、実装されたハンドル関数が呼び出され、関数の戻り値を元に HTTP レスポンスされます。

[Call Handle: wasmtime/src/commands/serve.rs](https://github.com/bytecodealliance/wasmtime/blob/v15.0.1/src/commands/serve.rs#L407-L410)

Wasm でも Thread や WASI により socket、io など Web アプリケーションに必要な技術要素の取り込みは進んでいます。ただ、こういったリソースにアクセスさせる方法だけでなく、ホスト側にリソースの管理を任せ、Wasm アプリケーション側ではリソースへのアクセスを限定的にさせる方法も良さそうです。

## おまけ: 起動までの手順

このサンプルコードは [sunfishcode/hello-wasi-http](https://github.com/sunfishcode/hello-wasi-http) をもとに wasmtime v15 に対応をさせたものとなります。

- [watawuwu/blog-wasmtime-serve](https://github.com/watawuwu/blog-wasmtime-serve)

この手順は Rust だけでなく、`cargo component` コマンド、`cargo-component-bindings` ライブラリ、そして WIT の一部の仕様を把握してないと理解が難しい部分があり、私もまだ勉強中です。特に`cargo component` コマンドと `cargo-component-bindings` ライブラリについては理解が浅いため説明を一部省略しています。

まずは `cargo component` を利用し、Wasm コンポーネントモデル用のプロジェクトを作成します。
`component new` サブコマンドに `--reactor` というオプションが付与されていますが、こちらはライブラリを作成するモードのようなものと思っていただければ大丈夫です。
詳細は[こちら](https://github.com/WebAssembly/WASI/issues/13)に説明がありますので参照してください。

```shell
$ cargo component new --reactor hello && cd hello
```

次に wasmtime v15.0.1 バージョンのバイナリで実行したいので、wasmtime のソースコードから HTTP ハンドラーの実装に依存する WIT をコピーしてきます。先ほどの説明では [wasi-http/wit](https://github.com/WebAssembly/wasi-http/tree/v0.2.0-rc-2023-11-10/wit) の WIT を利用すると記載しましたが、 wasmtime v15.0.1 が依存するバージョンが不明だったので wasmtime リポジトリからコピーしています。

```shell
# wasmtime v15.0.1 のソースコードをダウンロードし WIT をコピーする
$ wget -O- https://github.com/bytecodealliance/wasmtime/archive/refs/tags/v15.0.1.tar.gz | tar xzf -
$ cp -a wasmtime-15.0.1/crates/wasi/wit/deps wit
$ rm -rf wasmtime-15.0.1

$ tree .
├── Cargo.toml
├── src
│   └── lib.rs
└── wit
    ├── deps
    │   ├── cli
    │   │   ├── command.wit
    │   │   ├── environment.wit
    │   │   ├── exit.wit
    │   │   ├── reactor.wit
    │   │   ├── run.wit
    │   │   ├── stdio.wit
    │   │   └── terminal.wit
    │   ├── clocks
    │   │   ├── monotonic-clock.wit
    │   │   ├── wall-clock.wit
    │   │   └── world.wit
    │   ├── filesystem
    │   │   ├── preopens.wit
    │   │   ├── types.wit
    │   │   └── world.wit
    │   ├── http
    │   │   ├── handler.wit
    │   │   ├── proxy.wit
    │   │   └── types.wit
    │   ├── io
    │   │   ├── error.wit
    │   │   ├── poll.wit
    │   │   ├── streams.wit
    │   │   └── world.wit
    │   ├── random
    │   │   ├── insecure-seed.wit
    │   │   ├── insecure.wit
    │   │   ├── random.wit
    │   │   └── world.wit
    │   └── sockets
    │       ├── instance-network.wit
    │       ├── ip-name-lookup.wit
    │       ├── network.wit
    │       ├── tcp-create-socket.wit
    │       ├── tcp.wit
    │       ├── udp-create-socket.wit
    │       ├── udp.wit
    │       └── world.wit
    └── world.wit
```

次にアプリケーション用の WIT に先程コピーしてきた `http/proxy` のパッケージを `include` する World を定義します。

```wit:world.wit
package watawuwu:hello;

world hello-world {
  include wasi:http/proxy@0.2.0-rc-2023-11-10;
}
```

`include` は対象の World と同じ `export/import` の設定を持つため、この `include` により `export incoming-handler;` が設定されます。
そのため、この WIT を利用すると `incoming-handler` インタフェースを実装するためのグルーコードが生成されます。

```shell
$ cat wit/deps/http/proxy.wit
package wasi:http@0.2.0-rc-2023-11-10;

/// The `wasi:http/proxy` world captures a widely-implementable intersection of
/// hosts that includes HTTP forward and reverse proxies. Components targeting
/// this world may concurrently stream in and out any number of incoming and
/// outgoing HTTP requests.
world proxy {
  /// HTTP proxies have access to time and randomness.
  import wasi:clocks/wall-clock@0.2.0-rc-2023-11-10;
  import wasi:clocks/monotonic-clock@0.2.0-rc-2023-11-10;
  import wasi:random/random@0.2.0-rc-2023-11-10;

  /// Proxies have standard output and error streams which are expected to
  /// terminate in a developer-facing console provided by the host.
  import wasi:cli/stdout@0.2.0-rc-2023-11-10;
  import wasi:cli/stderr@0.2.0-rc-2023-11-10;

  /// TODO: this is a temporary workaround until component tooling is able to
  /// gracefully handle the absence of stdin. Hosts must return an eof stream
  /// for this import, which is what wasi-libc + tooling will do automatically
  /// when this import is properly removed.
  import wasi:cli/stdin@0.2.0-rc-2023-11-10;

  /// This is the default handler to use when user code simply wants to make an
  /// HTTP request (e.g., via `fetch()`).
  import outgoing-handler;

  /// The host delivers incoming HTTP requests to a component by calling the
  /// `handle` function of this exported interface. A host may arbitrarily reuse
  /// or not reuse component instance when delivering incoming HTTP requests and
  /// thus a component must be able to handle 0..N calls to `handle`.
  export incoming-handler;
}
```

次は `cargo-component-bindings(wit-bindgen)` で WIT からグルーコードを生成できるように、Cargo.toml を追加、修正します。

```toml:Cargo.toml
...

[package.metadata.component]
package = "watawuwu:hello"

# package.metadata.component.dependencies ではない点に注意！！
[package.metadata.component.target.dependencies]
"wasi:cli"        = { path = "wit/deps/cli" }
"wasi:clocks"     = { path = "wit/deps/clocks" }
"wasi:filesystem" = { path = "wit/deps/filesystem" }
"wasi:http"       = { path = "wit/deps/http" }
"wasi:io"         = { path = "wit/deps/io" }
"wasi:random"     = { path = "wit/deps/random" }
"wasi:sockets"    = { path = "wit/deps/sockets" }
```

最後に `incoming-handler` インタフェースを実装します。

```rs:lib.rs
cargo_component_bindings::generate!();

use bindings::wasi::http::types::{
    Headers, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam,
};

struct Component;

impl bindings::exports::wasi::http::incoming_handler::Guest for Component {
    fn handle(_request: IncomingRequest, outparam: ResponseOutparam) {
        let hdrs = Headers::new();
        let resp = OutgoingResponse::new(hdrs);
        resp.set_status_code(200).expect("fail to set status code");
        let body = resp.body().expect("fail to get body");
        ResponseOutparam::set(outparam, Ok(resp));

        let out = body.write().expect("outgoing stream");
        out.blocking_write_and_flush(b"Hello\n")
            .expect("fail to write/flush");

        drop(out);
        OutgoingBody::finish(body, None).expect("fail to finish");
    }
}
```

これで `cargo component build` でビルドし、`wasmtime serve` が実行できます。

```shell
$ cargo component build
   Compiling proc-macro2 v1.0.70
   Compiling unicode-ident v1.0.12
   Compiling cargo-component-macro v0.5.0
   Compiling bitflags v2.4.1
   Compiling wit-bindgen v0.14.0
   Compiling quote v1.0.33
   Compiling syn v2.0.39
   Compiling cargo-component-bindings v0.5.0
   Compiling hello v0.1.0 (/Users/wmatsui/dev/src/github.com/zlabjp/zz_work/sig-wasm/2023-12-05_qiita/hello)
    Finished dev [unoptimized + debuginfo] target(s) in 3.68s
    Creating component /Users/wmatsui/dev/src/github.com/zlabjp/zz_work/sig-wasm/2023-12-05_qiita/hello/target/wasm32-wasi/debug/hello.wasm

$ wasmtime serve target/wasm32-wasi/debug/hello.wasm
Serving HTTP on http://0.0.0.0:8080/
```

あとは好きな HTTP クライアントのコマンドで 8080 ポートにリクエストし、HTTP ステータスコードが 200 で、HTTP レスポンスボディに `Hello` の応答があることを確認します。

```shell
$ xh http://0.0.0.0:8080/
HTTP/1.1 200 OK
Date: Wed, 06 Dec 2023 06:46:25 GMT
Transfer-Encoding: chunked

Hello
```