[Wasmtime]Rustのwasm32-wasip2でHTTPリクエストクライアントとWebサーバーを動かす(waki)
はじめに
Rust Version 1.82.0 (2024-10-17)にPromote wasm32-wasip2 to Tier 2.がきたのでaxumをWASMで動かすために最近様々な情報を集めています。
Tokioのさらに低レイヤーにあるmio(Metal I/O)がwasm32-wasip2に対応していないのでほとんどのWebアプリケーションフレームワークは動きませんが、WASI専用のHTTPリクエストクライアント&Webサーバーライブラリのwakiがwasm32-wasip2に対応していたので試しに使ってみることにしました。
セットアップ
$ curl https://wasmtime.dev/install.sh -sSf | bash
$ export PATH="$HOME/.wasmtime/bin:$PATH"
まずはWASM/WASIのランタイム、Wasmtimeをインストールします。
$ brew install wasmtime
Homebrew(Linuxbrew)でもインストールできます。
$ rustup default 1.82.0-x86_64-unknown-linux-gnu
$ rustup target add wasm32-wasip2
続いてRustのバージョンを1.82以上にして、wasm32-wasip2ターゲットを入手します。
$ wasmtime -V
wasmtime 26.0.0 (c92317bcc 2024-10-22)
$ rustc -V
rustc 1.82.0 (f6e511eec 2024-10-15)
$ cargo -V
cargo 1.82.0 (8f40fc59f 2024-08-21)
$ rustup target list --installed
wasm32-wasip2
x86_64-unknown-linux-gnu
それぞれインストールできました。
$ cargo init
Creating binary (application) package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
$ cargo add waki -F json
Updating crates.io index
Adding waki v0.4.0 to dependencies
Features:
+ json
- multipart
Updating crates.io index
Locking 49 packages to latest compatible versions
Adding hashbrown v0.14.5 (latest: v0.15.0)
Adding zerocopy v0.7.35 (latest: v0.8.7)
Adding zerocopy-derive v0.7.35 (latest: v0.8.7)
$ cargo add serde_json
Updating crates.io index
Adding serde_json v1.0.132 to dependencies
Features:
+ std
- alloc
- arbitrary_precision
- float_roundtrip
- indexmap
- preserve_order
- raw_value
- unbounded_depth
cargo initコマンドでCargo.toml, targetディレクトリ用の.gitignore, srcディレクトリにmain.rsを作成して、wakiとSerde JSONを追加します。
[package]
name = "main"
version = "0.1.0"
edition = "2021"
[dependencies]
serde_json = "1.0.132"
waki = { version = "0.4.0", features = ["json"] }
これで準備ができました。
HTTPリクエストクライアント
まずはHTTPリクエストクライアントを作ってみます。
use serde_json::Value;
use std::time::Duration;
use waki::Client;
fn main() {
let req = "https://httpbin.org/get";
let headers = [("User-Agent", "WASI/0.2")];
let res = Client::new()
.get(req)
.headers(headers)
.connect_timeout(Duration::from_secs(5))
.send()
.unwrap();
let status = res.status_code();
let body = res.json::<Value>().unwrap();
println!("GET {req} {status}");
println!("BODY {body}");
}
reqwestみたいで使いやすいですね。
$ cargo build -r --target=wasm32-wasip2
Compiling main v0.1.0 (/workspace/main)
Finished `release` profile [optimized] target(s) in 0.75s
ターゲットをwasm32-wasip2にして、Releaseプロファイルでビルドします。
$ wasmtime target/wasm32-wasip2/release/main.wasm
Error: failed to run main module `target/wasm32-wasip2/release/main.wasm`
Caused by:
0: component imports instance `wasi:http/types@0.2.2`, but a matching implementation was not found in the linker
1: instance export `fields` has the wrong type
2: resource implementation is missing
動かそうとしたらエラーが出ました。
Caused byの0に書かれている通り、wasi:httpが見つからないので使えないみたいです。
なので以下のように-S
オプションでhttpを渡してあげると動くようになります。
$ wasmtime -S http target/wasm32-wasip2/release/main.wasm
GET https://httpbin.org/get 200
BODY {"args":{},"headers":{"Host":"httpbin.org","X-Amzn-Trace-Id":"..."},"origin":"...","url":"https://httpbin.org/get"}
最初はヘッダーを設定せずにビルドしたので、User-Agentヘッダーすらない情報が返ってきました。
User-Agentヘッダーを適当に決めて、再度ビルドして動かしてみます。
$ wasmtime -S http target/wasm32-wasip2/release/main.wasm
GET https://httpbin.org/get 200
BODY {"args":{},"headers":{"Host":"httpbin.org","User-Agent":"WASI/0.2","X-Amzn-Trace-Id":"..."},"origin":"...","url":"https://httpbin.org/get"}
いい感じのUser-Agentヘッダーが追加されたので満足です。
Webサーバー
続いてWebサーバーを作ってみます。
use std::collections::HashMap;
use waki::{handler, ErrorCode, Request, Response};
#[handler]
fn home(req: Request) -> Result<Response, ErrorCode> {
let query = req.query();
let content = query.get("content").map_or("Hello, World!", |v| v);
let body = HashMap::from([("content", content)]);
println!("CONTENT {content}");
Response::builder().json(&body).build()
}
fn main() {}
JavaScriptのfetchのようにhandlerマクロにRequest, Responseに関するコードを書いて、bodyやjsonを返すだけです。
WebアプリケーションフレームワークやHTTPリクエストクライアントのようにクエリやヘッダーも追加できますが、URL Patternのようなルーティングはまだできないみたいです。
なお、main関数は使わないので必要ないのですが、バイナリとしてビルドしようとするとエラーが出てしまうので、main関数に何もしない処理を書いておく必要があります。
$ cargo build -r --target=wasm32-wasip2
Compiling main v0.1.0 (/workspace/main)
Finished `release` profile [optimized] target(s) in 0.75s
$ wasmtime -S http target/wasm32-wasip2/release/main.wasm
ビルドして先ほどと同じコマンドで動かそうとしても何も起こりません。
それもそのはず、先ほどmain関数に何もしない処理を書いたからこうなります。
そこでwasmtime serveコマンドを使います。
$ wasmtime serve target/wasm32-wasip2/release/main.wasm
Error: component imports instance `wasi:cli/environment@0.2.0`, but a matching implementation was not found in the linker
Caused by:
0: instance export `get-environment` has the wrong type
1: function implementation is missing
あれ???
動かそうとしたらまたエラーが出てしまいました……。
エラーメッセージに書かれている通り、wasi:cliが見つからないので使えないみたいです。
なので以下のように-S
オプションでcliを渡してあげると動くようになります。
$ wasmtime serve -S cli target/wasm32-wasip2/release/main.wasm
Serving HTTP on http://0.0.0.0:8080/
なるほどCLIにログを表示するために使っているんですね。
curlで確認してみましょう。
$ curl -i http://localhost:8080/
HTTP/1.1 200 OK
content-type: application/json
transfer-encoding: chunked
date: Sun, 27 Oct 2024 08:00:00 GMT
{"content":"Hello, World!"}
ちゃんと動いていて感動。
$ wasmtime serve -S cli target/wasm32-wasip2/release/main.wasm
Serving HTTP on http://0.0.0.0:8080/
stdout [0] :: CONTENT Hello, World!
標準出力の内容がログにも表示されました。
$ curl -i http://localhost:8080/?content=foo
HTTP/1.1 200 OK
content-type: application/json
transfer-encoding: chunked
date: Sun, 27 Oct 2024 08:00:00 GMT
{"content":"foo"}
クエリも使えます。
$ wasmtime serve -S cli target/wasm32-wasip2/release/main.wasm
Serving HTTP on http://0.0.0.0:8080/
stdout [0] :: CONTENT Hello, World!
stdout [1] :: CONTENT foo
期待通りの動作で満足しました。
合成
最後にHTTPリクエストクライアントとWebサーバーを合体させたWebアプリを作ってみます。
use serde_json::{json, Value};
use std::time::Duration;
use waki::{handler, Client, ErrorCode, Request, Response};
#[handler]
fn home(req: Request) -> Result<Response, ErrorCode> {
let query = req.query();
let req = query
.get("id")
.filter(|v| v.starts_with("http:") || v.starts_with("https:"))
.map_or("https://httpbin.org/get", |v| v);
let req_headers = [
("Accept", "application/activity+json"),
("User-Agent", "WASI/0.2"),
];
let res = Client::new()
.get(req)
.headers(req_headers)
.connect_timeout(Duration::from_secs(5))
.send()
.unwrap();
let status = res.status_code();
let body = res.json::<Value>().unwrap_or(json!({}));
let res_headers = [("Content-Type", "application/activity+json")];
println!("GET {req} {status}");
Response::builder().json(&body).headers(res_headers).build()
}
fn main() {}
私はActivityPub実装を作る時にContent-Type: application/activity+json
をGETすることがよくあるので、クエリにURLを渡すだけで簡単にGETしてきてくれるアプリを作りました。
これならWebブラウザからも使えます。
$ cargo build -r --target=wasm32-wasip2
Compiling main v0.1.0 (/workspace/main)
Finished `release` profile [optimized] target(s) in 0.75s
$ wasmtime serve -S cli target/wasm32-wasip2/release/main.wasm
Serving HTTP on http://0.0.0.0:8080/
ビルドして動かします。
$ curl -i http://localhost:8080/
HTTP/1.1 200 OK
content-type: application/activity+json
transfer-encoding: chunked
date: Sun, 27 Oct 2024 08:00:00 GMT
{"args":{},"headers":{"Accept":"application/activity+json","Host":"httpbin.org","User-Agent":"WASI/0.2","X-Amzn-Trace-Id":"..."},"origin":"...","url":"https://httpbin.org/get"}
クエリなしのルートパスにアクセスしてエラーが出たら困るので、HTTPBinからGETしてくるようにしました。
$ wasmtime serve -S cli target/wasm32-wasip2/release/main.wasm
Serving HTTP on http://0.0.0.0:8080/
stdout [0] :: GET https://httpbin.org/get 200
標準出力の内容もログにちゃんと表示されています。
$ curl -i http://localhost:8080/?id=https://mastodon.social/@Gargron
HTTP/1.1 200 OK
content-type: application/activity+json
transfer-encoding: chunked
date: Sun, 27 Oct 2024 08:00:00 GMT
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",...}
Content-Type: application/activity+json
をGETできました。
$ wasmtime serve -S cli target/wasm32-wasip2/release/main.wasm
Serving HTTP on http://0.0.0.0:8080/
stdout [0] :: GET https://httpbin.org/get 200
stdout [1] :: GET https://mastodon.social/@Gargron 200
WASIでWebアプリが作れてとても満足しました。
ちなみにこの.wasmファイルのサイズは390KBぐらいです。
おわりに
WASMやWASIに関するニュースを見るたびに、一体何に使うの? と思うことがよくあるのですが、実際にコードを書いて使ってみると最近のJavaScript/TypeScriptランタイムやサーバーレスアプリケーションサービスと何ら変わらないことが分かりました。
今後のWASMやWASIの発展に期待しつつ、早くaxumをWASMで動かしたい今日この頃です。
Discussion