虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

【WebAssembly連載第六回】WASIを触ってみようその2

本記事は「WebAssembly連載」の第六回目の記事です.

皆さんこんにちは。虎の穴ラボのY.Fです。

だいぶ遅くなりましたが、連載記事の最終回になります。

前回の記事では、WASIとWASIランタイムを使って、ブラウザ外でWebAssemblyを実行する方法について紹介してみました。

toranoana-lab.hatenablog.com

今回の記事では前回に引き続き、WASIについて書いてみたいと思います。

WASIでHTTPサーバーを作ってみる

まずは普通にRustでHTTPサーバーを作ってみます。

以下Rustのドキュメントに詳細は記載されています。

doc.rust-jp.rs

細かい部分は置いておいて、リクエストされた場合、固定の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がサポートしているプロポーザルの一覧が記載されています。

docs.wasmtime.dev

実際にいま出ているプロポーザルの一覧はこちらです。

github.com

HTTPのプロポーザルはこちらです。

github.com

wasmtimeでは、HTTPは現状未サポートとなっているようです。したがって、今回作ったWebAssemblyを動かしたければ違うWASIランタイムを探す必要がありそうです。

WasmEdgeを試してみる

専用のクレートを利用する必要がありますが、WasmEdgeであればネットワークアプリケーションを作れるようです。

wasmedge.org

先程の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でも動かしてみます。

www.publickey1.jp

なお、Preview2からランタイムがいくつかの中から選べるようになっています。

www.docker.com

実際にDocker+Wasmを動かす方法については、以下公式ドキュメントがあります。

docs.docker.com

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の仮想ファイルシステムに指定したディレクトリをマウントして、読み込めるようにできます。

wasmedge.org

続いて、先程と同様に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