LoginSignup
7
4

wasmtime serve ってなに?

Last updated at Posted at 2023-12-10

概要

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

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

どんなことができるのか

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

wasmtime serve は wasi-http の HTTP proxy worldを実装したコンポーネントを実行するためのサブコマンドとなっています。そのため引数の Wasm ファイルには HTTP Proxy World の実装と公開が必要です。

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

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

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

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

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 リクエストすることで通信の確認ができます。

$ 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

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

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

おまけ: 起動までの手順

このサンプルコードは sunfishcode/hello-wasi-http をもとに wasmtime v15 に対応をさせたものとなります。

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

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

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

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

# 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 を定義します。

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 インタフェースを実装するためのグルーコードが生成されます。

$ 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 を追加、修正します。

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 インタフェースを実装します。

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 が実行できます。

$ 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 の応答があることを確認します。

$ 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
7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4