目次
はじめに
今回のエントリーではずっと気になっていた WASI をようやく試したので、その内容を記載します。
WASI とは?
WebAssembly System Interface の略です。
ブラウザ上でネイティブ並みの実行速度が出せる WebAssembly (WASM)。これをブラウザ以外の用途でも活用できないかと考えた人達がいました。しかし、その場合はホストマシン OS が管理するリソースへのアクセス手段が必要…。このシステムとのインターフェースを仕様として標準化しようという試みが WASI です。Mozilla や Fastly が主だって取り組まれています。
WASI について Docker の共同設立者である Solomon Hykes 氏は以下のようなツイートをされています。
If WASM+WASI existed in 2008, we wouldn’t have needed to created Docker. That’s how important it is. Webassembly on the server is the future of computing. A standardized system interface was the missing link. Let’s hope WASI is up to the task! https://t.co/wnXQg4kwa4
— Solomon Hykes (@solomonstre) 2019年3月27日
『2008 年に WASM + WASI が存在した場合、Docker を作成する必要はありませんでした。それがどれほど重要な事か。サーバー上の WebAssembly はコンピューティングの未来です。標準化されたシステムインターフェースはミッシングリンクでした。WASI が任務を果たすことを願っています!』
Fastly が 2019/11 に WebAssembly を用いたサーバーレス基盤である Compute@Edge のプライベート・ベータ版をローンチしていたり、2019/12 に W3C が WebAssembly のコア仕様をインターネット標準として勧告していたりと、WASM+WASI 界隈は盛り上がりを見せていて非常に楽しそうです。
Let’s WASI
今回は Lucet (ルーセット) を試してみます。Fastly が開発を進める Lucet は、WebAssembly 用のコンパイラを含むツールチェイン、及び、ランタイムを提供していて、その中に WASI サポートも含まれています。
まずは環境を整える
以下に沿って進めます。
https://github.com/bytecodealliance/lucet/wiki/Getting-started
※ Mac OS X 環境、Docker Desktop はインストール済み
Git クローン & リポジトリルートへ移動 & サブモジュール更新
1 2 |
$ git clone git@github.com:bytecodealliance/lucet.git && cd lucet $ git submodule init && git submodule update |
環境変数の設定
初回実行時はセットアップも行われるため 30 分くらい掛かります。
1 |
$ source devenv_setenv.sh |
動作確認
1 2 3 |
$ lucetc --version -- lucetc 0.5.0 |
Hello World を試す
以下に沿って進めます。
https://github.com/bytecodealliance/lucet/wiki/Your-first-Lucet-application
作業用ディレクトリ作成 & 移動
1 |
$ mkdir -p hello && cd hello |
hello.c ファイル作成
1 2 3 4 5 6 7 8 9 10 11 |
#include int main(int argc, char* argv[]) { if (argc > 1) { printf("Hello, %s!\n", argv[1]); } else { puts("Hello, world!"); } return 0; } |
C ファイル → WebAssembly バイナリ
1 |
$ wasm32-wasi-clang -Ofast -o hello.wasm hello.c |
WebAssembly バイナリ → x86-64 ネイティブコード
Lucet は事前コンパイル形式のため、前もって共有ライブラリを生成します。
1 |
$ lucetc-wasi -o hello.so hello.wasm |
Lucet で実行可能か確認
1 2 |
$ lucet-wasi hello.so 10ANTZ Hello, 10ANTZ! |
この Hello World では、WebAssembly バイナリを Lucet コンパイラでネイティブコードに変換、実際に動作させるところまでを確認できました。ちなみにホストマシン側の lucet-wasi はコンテナにセットアップされたアプリケーションを実行しているようです。
1 2 3 4 |
$ cat $(which lucet-wasi) #! /bin/sh exec "/Users/xxx/lucet/devenv_run.sh" /opt/lucet/bin/devenv_setenv.sh /opt/lucet/bin/lucet-wasi "$@" |
ランタイムを試す
Rust 製サーバーに Lucet ランタイムを組み込んで WASM 製共有ライブラリの呼び出しを行ってみようと思います。Lucet の開発環境用コンテナには Rust がインストールされているので同じコンテナを使い回しますが、Mac OS X でも Rust 使いたいので Rust 日本語ドキュメントの Getting started を参考にインストールしておきます (インストール作業は割愛)。
リポジトリルートの Cargo.toml を編集
1 2 3 4 5 |
members = [ (...中略...) "sightglass", "simple-httpd", # ← これを足す ] |
簡易 HTTP サーバー用にバイナリ・アプリケーションとして追加
1 2 |
$ cargo new --bin simple-httpd Created binary (application) `simple-httpd` package |
simple-httpd ディレクトリ、及び、Rust アプリケーションのテンプレートが作成されます。
ディレクトリ移動
1 |
$ cd simple-httpd |
src/main.rs を編集
こちらの記事を参考 (ほぼそのまま。。。) にさせていただきました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
use std::str; use std::thread; use std::io; use std::io::{Write, BufRead}; use std::net::{TcpListener, TcpStream}; fn main() { let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); for stream in listener.incoming() { match stream { Ok(stream) => { thread::spawn(move || { handle_stream(stream) }); } Err(_) => { panic!("connection failed") } }; } } fn handle_stream(stream: TcpStream) { let mut stream = io::BufReader::new(stream); let mut req_str = String::new(); if let Err(err) = stream.read_line(&mut req_str) { panic!("error during receive a line: {}", err); } let mut params = req_str.split_whitespace(); let method = params.next(); let path = params.next(); match (method, path) { (Some("GET"), Some(path)) => { handle_get(path, stream.get_mut()); } _ => panic!("failed to parse"), } } fn handle_get(_path: &str, stream: &mut TcpStream) { let body_str = String::from("this request is GET"); writeln!(stream, "HTTP/1.1 200 OK").unwrap(); writeln!(stream, "Content-Type: text/html; charset=UTF-8").unwrap(); writeln!(stream, "Content-Length: {}", body_str.len()).unwrap(); writeln!(stream).unwrap(); writeln!(stream, "{}", body_str).unwrap(); } |
サーバー起動 (シェル A で実行)
1 2 3 4 |
$ cargo run Compiling simple-httpd v0.1.0 (/Users/xxx/lucet/simple-httpd) Finished dev [unoptimized + debuginfo] target(s) in 3.04s Running `/Users/xxx/lucet/target/debug/simple-httpd` |
確認 (シェル B で実行)
1 2 |
$ curl http://127.0.0.1:8080 this request is GET |
問題なくアクセスできましたので、次はランタイムを組み込んでいきます。
lib.c 作成
1 2 3 4 5 6 |
#include uint32_t add(uint32_t a, uint32_t b) { return a + b; } |
C ファイル → WebAssembly バイナリ
1 |
$ wasm32-wasi-clang -Ofast -nostartfiles -Wl,--no-entry,--export-all -o lib.wasm lib.c |
ライブラリとして使用する前提のオプションを指定しています。
WebAssembly バイナリ → x86-64 ネイティブコード
1 |
$ lucetc-wasi -o lib.so lib.wasm |
Cargo.toml を編集
依存するクレートとして lucet-runtime と lucet-wasi を追加します。
1 2 3 |
[dependencies] lucet-runtime = { version = "*", path = "../lucet-runtime" } lucet-wasi = { version = "*", path = "../lucet-wasi" } |
src/main.rs を編集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
fn handle_get(_path: &str, stream: &mut TcpStream) { - let body_str = String::from("this request is GET"); + // まだバージョンが 1 未満だからだと思われますが、 + // これを実行しないと DlModule::load() 内部で使用している __wasi_fd_prestat_get が見つからずエラーになります。 + // 実行しておくと関数が呼び出せるようになってエラーが発生しなくなります。 + lucet_wasi::export_wasi_funcs(); + + use lucet_runtime::{DlModule, Limits, MmapRegion, Region}; + let module = DlModule::load("lib.so").unwrap(); // ここで WASM を元に作られた共有ライブラリを指定しています。 + let region = MmapRegion::create(1, &Limits::default()).unwrap(); + let mut inst = region.new_instance(module).unwrap(); + + let a = 1u32; + let b = 2u32; + let retval = inst.run("add", &[a.into(), b.into()]).unwrap().unwrap_returned(); // ここで共有ライブラリ側に計算させています。 + + let body_str = format!("{} + {} = {}", a, b, u32::from(retval)); writeln!(stream, "HTTP/1.1 200 OK").unwrap(); writeln!(stream, "Content-Type: text/html; charset=UTF-8").unwrap(); |
リポジトリルートへ移動
1 |
$ cd (リポジトリルート) |
ここからはコンテナ内で作業を行います。
サーバー起動 (シェル A で実行)
1 2 3 4 5 6 |
$ ./devenv_run.sh (コンテナへログイン) /lucet# cd simple-httpd && cargo run Compiling simple-httpd v0.1.0 (/lucet/simple-httpd) Finished dev [unoptimized + debuginfo] target(s) in 53.22s Running `/lucet/target/debug/simple-httpd` |
確認 (シェル B で実行)
1 2 3 4 |
$ ./devenv_run.sh (コンテナへログイン) /lucet# curl http://127.0.0.1:8080 1 + 2 = 3 |
Rust から 1, 2 という数値を共有ライブラリに渡した上で計算を行い、結果として 3 が返ってきているのが確認できました。
まとめ
WebAssembly バイナリは様々な言語から生成できるので、うまく活用できれば言語非依存で実行速度も担保したサービスを提供する事ができそうです。AWS Lambda や Google Cloud Functions を始めとするサーバレス基盤では対応言語に制限がありますが、Fastly Compute@Edge では WASM さえ出力できる言語であればサーバレス基盤の採用が可能になるという事になりますので、Fastly が力を入れるのも頷けます。
ゲームやウェブサービスにおいても、端末側とサーバー側が異なる言語であれライブラリの共有が可能になりますので、アイデア次第ではありますが色々な用途が考えられるのでは無いでしょうか?
参考
- Standardizing WASI: A system interface to run WebAssembly outside the web
- コンテナ技術を捨て、 WASIを試す
- Rustで簡易HTTPサーバーを作成してみる
- Rust meets Lucet:ネイティブアプリでwasm形式の動的リンクプラグインを実行する