本記事は「WebAssembly連載」の第六回目の記事です.
皆さんこんにちは。虎の穴ラボのY.Fです。
だいぶ遅くなりましたが、連載記事の最終回になります。
前回の記事では、WASIとWASIランタイムを使って、ブラウザ外でWebAssemblyを実行する方法について紹介してみました。
今回の記事では前回に引き続き、WASIについて書いてみたいと思います。
WASIでHTTPサーバーを作ってみる
まずは普通にRustでHTTPサーバーを作ってみます。
以下Rustのドキュメントに詳細は記載されています。
細かい部分は置いておいて、リクエストされた場合、固定のHTMLを返すようにしてみます。
use std::io::prelude::*; use std::net::TcpListener; use std::net::TcpStream; const HTML: &str = r#" <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>サンプルページ</title> <link rel="stylesheet" href="styles.css"> </head> <body> <header> <nav> <ul> <li><a href="\#">Home</a></li> <li><a href="\#">About Us</a></li> <li><a href="\#">Blog</a></li> <li><a href="\#">Contact</a></li> </ul> </nav> <h1>My Website</h1> <p>サイトの紹介文</p> </header> <main> <article> <h2>記事のタイトル</h2> <p>記事の内容</p> <p>別の段落</p> </article> <aside> <h2>サイドバー</h2> <ul> <li><a href="\#">Related Link 1</a></li> <li><a href="\#">Related Link 2</a></li> <li><a href="\#">Related Link 3</a></li> </ul> </aside> </main> <footer> <p>Copyright © 2021</p> </footer> </body> </html> "#; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", HTML); stream.write(response.as_bytes()).unwrap(); stream.flush().unwrap(); }
cargo run
コマンドでHTTPサーバーとして起動することが確認できるかと思います。
Rustのコードとして動くことは確認できたので、wasi用WebAssemblyとしてビルドしてみます。ビルドコマンドは前回の記事と同様に以下です。
$ cargo build --target wasm32-wasi
ビルドはできました。できたwasmファイルを前回同様にwasmtimeで動かしてみます。
$ wasmtime target/wasm32-wasi/debug/wasi-http.wasm thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error { kind: Unsupported, message: "operation not supported on this platform" }', src/main.rs:25:56 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace Error: failed to run main module `target/wasm32-wasi/debug/wasi-http.wasm` Caused by: 0: failed to invoke command default 1: error while executing at wasm backtrace: 0: 0x9f0f - <unknown>!__rust_start_panic 1: 0x9b5a - <unknown>!rust_panic 2: 0x9b21 - <unknown>!std::panicking::rust_panic_with_hook::h1c67ce6bc4eb31b7 3: 0x8bd1 - <unknown>!std::panicking::begin_panic_handler::{{closure}}::h749586aa4ef76f6f 4: 0x8afb - <unknown>!std::sys_common::backtrace::__rust_end_short_backtrace::h426b71926848cb31 5: 0x918f - <unknown>!rust_begin_unwind 6: 0xef32 - <unknown>!core::panicking::panic_fmt::hf4ce15c1b219b988 7: 0x10195 - <unknown>!core::result::unwrap_failed::he6bfae7ea6f8795e 8: 0x4d5a - <unknown>!core::result::Result<T,E>::unwrap::h3b189a4961420a3c 9: 0x22c3 - <unknown>!wasi_http::main::hf87b2781af00c254 10: 0x1318 - <unknown>!core::ops::function::FnOnce::call_once::h93c4b7eca246f7f1 11: 0x8aa - <unknown>!std::sys_common::backtrace::__rust_begin_short_backtrace::ha58d3e4dc1e269bc 12: 0x4fb3 - <unknown>!std::rt::lang_start::{{closure}}::h553233d5645067d9 13: 0x6a8f - <unknown>!std::rt::lang_start_internal::h22e2e4bd5ff7bcf4 14: 0x4f50 - <unknown>!std::rt::lang_start::h6897967bacadaa44 15: 0x26a0 - <unknown>!__main_void 16: 0x40e - <unknown>!_start note: using the `WASMTIME_BACKTRACE_DETAILS=1` environment variable may show more debugging information 2: wasm trap: wasm `unreachable` instruction executed
エラーになりました。wasmtimeのドキュメントを見てみます。以下のページにwasmtimeがサポートしているプロポーザルの一覧が記載されています。
実際にいま出ているプロポーザルの一覧はこちらです。
HTTPのプロポーザルはこちらです。
wasmtimeでは、HTTPは現状未サポートとなっているようです。したがって、今回作ったWebAssemblyを動かしたければ違うWASIランタイムを探す必要がありそうです。
WasmEdgeを試してみる
専用のクレートを利用する必要がありますが、WasmEdgeであればネットワークアプリケーションを作れるようです。
先程のRustプログラムを置き換えてみます。
use std::io::prelude::*; use wasmedge_wasi_socket::{TcpListener, TcpStream}; const HTML: &str = r#" <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>サンプルページ</title> <link rel="stylesheet" href="styles.css"> </head> <body> <header> <nav> <ul> <li><a href="\#">Home</a></li> <li><a href="\#">About Us</a></li> <li><a href="\#">Blog</a></li> <li><a href="\#">Contact</a></li> </ul> </nav> <h1>My Website</h1> <p>サイトの紹介文</p> </header> <main> <article> <h2>記事のタイトル</h2> <p>記事の内容</p> <p>別の段落</p> </article> <aside> <h2>サイドバー</h2> <ul> <li><a href="\#">Related Link 1</a></li> <li><a href="\#">Related Link 2</a></li> <li><a href="\#">Related Link 3</a></li> </ul> </aside> </main> <footer> <p>Copyright © 2021</p> </footer> </body> </html> "#; fn main() { let listener = TcpListener::bind("0.0.0.0:7878", false).unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", HTML); stream.write(response.as_bytes()).unwrap(); stream.flush().unwrap(); }
殆ど変わらず、TcpListener
などのネットワーク部分がクレート利用に置き換わっただけです。
ビルド後、wasmtimeと同様に以下のようなコマンドで実行できます。
$ wasmedge target/wasm32-wasi/debug/wasi-http.wasm
DockerのWebAssemblyランタイム
少し前にDockerでWebAssemblyが実行できるようになったと発表がありました。この内部で動いているランタイムがWasmEdgeのようなので、Dockerでも動かしてみます。
なお、Preview2からランタイムがいくつかの中から選べるようになっています。
実際にDocker+Wasmを動かす方法については、以下公式ドキュメントがあります。
Docker Desktopで動かすにはベータ版の機能を有効化する必要があるので有効化しておきます。
Dockerfileを以下のようにします。
# syntax=docker/dockerfile:1 FROM scratch COPY ./target/wasm32-wasi/debug/wasi-http.wasm /wasi-http.wasm ENTRYPOINT [ "wasi-http.wasm" ]
次に、プラットフォームを指定しつつDockerのイメージをビルドします。usernameは任意に変えてください。
docker buildx build --platform wasi/wasm32 -t username/wasi-http .
またプレビュー版ゆえかエラーが出ますが、イメージはできています。
(エラー)
------ > exporting to image: ------ ERROR: failed to solve: no match for platform in manifest sha256:3187cb57f434ae8ba21e9cd78b59cae54b0345bf948bb430d83a3031af82e6ff: not found
(イメージ)
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE username/wasi-http latest 3187cb57f434 38 minutes ago 1.29MB
Docker Desktopのダッシュボードで見るとどのプラットフォームで作られているかも表示されています。
イメージをプッシュします。
$ docker push username/wasi-http
動かしてみます。
$ docker container run --rm -dp 7878:7878 \ --name=wasi-http \ --runtime=io.containerd.wasmedge.v1 \ --platform=wasi/wasm32 \ username/wasi-http
Dockerfileを見てもらうと分かる通り、今回はscratchイメージを使っています。scratchイメージは最小化されたベースイメージで、shコマンドすらありません。 特に設定なく、そのscratchイメージにwasmファイル一個乗せれば動いており、Docker自体にWebAssemblyのランタイムが統合されていることがわかるかと思います。
ファイルを外から読み込む
ここまでのソースは、HTMLがベタ書きされているので、これを別ファイルから読み込めるようにしてみます。
まずは読み込み対象のindex.htmlを作ります。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>サンプルページ</title> <link rel="stylesheet" href="styles.css"> </head> <body> <header> <nav> <ul> <li><a href="\#">Home</a></li> <li><a href="\#">About Us</a></li> <li><a href="\#">Blog</a></li> <li><a href="\#">Contact</a></li> </ul> </nav> <h1>My Website</h1> <p>サイトの紹介文</p> </header> <main> <article> <h2>記事のタイトル</h2> <p>記事の内容</p> <p>別の段落</p> </article> <aside> <h2>サイドバー</h2> <ul> <li><a href="\#">Related Link 1</a></li> <li><a href="\#">Related Link 2</a></li> <li><a href="\#">Related Link 3</a></li> </ul> </aside> </main> <footer> <p>Copyright © 2021</p> </footer> </body> </html>
次に、Rustのソースを以下のように書き換えて、上記index.htmlを読み込むようにします。
use std::fs::File; //...略 fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; let mut file = File::open("index.html").unwrap(); stream.read(&mut buffer).unwrap(); let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents); stream.write(response.as_bytes()).unwrap(); stream.flush().unwrap(); }
先程と同様にビルドします。
$ cargo build --target wasm32-wasi
wasmedgeで動かしてみます。
$ wasmedge --dir .:. target/wasm32-wasi/debug/wasi-http.wasm
ポイントは、dirオプションを渡していることです。このオプションを与えることで、wasiの仮想ファイルシステムに指定したディレクトリをマウントして、読み込めるようにできます。
続いて、先程と同様にDockerでも動かしてみます。こちらは単にindex.htmlをコピーしてあげるだけです。
# syntax=docker/dockerfile:1 FROM scratch COPY ./target/wasm32-wasi/debug/wasi-http.wasm /wasi-http.wasm COPY ./index.html /index.html ENTRYPOINT [ "wasi-http.wasm" ]
まとめ
連載最後の記事として、WASIでHTTPサーバーを作って色々な方法で動かしてみました。
作ってみて感じた利点としては、
- Docker上で動くかつ、ワンバイナリで動くようになるので、サーバーレス系の環境で軽量イメージを動かすのが簡単になりそう
- Cloudflare Workersを初めとした、エッジランタイムがWebAssemblyに対応してたりするので、より高度なWebアプリケーションがWebAssemblyで作れれば色々簡単にできそう
連載最後の記事になりました。今まで読んでいただいた方はありがとうございます。これからもWebAssemblyには注目していきたいと思いますので、また記事が出た際はぜひご一読ください。
P.S.
採用
虎の穴では一緒に働く仲間を募集中です!
この記事を読んで、興味を持っていただけた方はぜひ弊社の採用情報をご覧下さい。
yumenosora.co.jp