飲食店のテーブルには謎のボタンがついていることがあります。 お店の中のものを勝手に触ると怒られますが、謎のボタンを押すと店員さんがやってきていろいろなサービスを受けることができます。 これって何かに似てませんか?
前回は WebAssembly のランタイムを実装しました。
WebAssembly 単体では計算とメモリアクセスくらいしかできません。 *1 APIを通してOSに依頼することでいろいろなサービスを受けることができます。
ということで、今回はAPIを実装します。
2種類のAPI
API を実装するにあたり、2種類の方式が思いつきます。
名前ベース方式
モジュール名と関数名を直接指定してインポートした関数を呼び出します。 WASI がこの方式を採用しています。
呼び出すコードはシンプルになりますが、機能ごとにインポートするためインポート項目が多くなります。
また、システムコールは通常 WASM 同士を直接リンクするわけではないので、2つの違う世界の関数を結びつけるためにパラメータの変換などを呼び出される全ての関数に実装する必要があってコストがかかります。
WASM を直接リンクできるリンカーがある場合、この方式ではサードパーティーによる拡張が比較的容易になります。
番号ベース(システムコール)方式
入口となる関数(システムコール)のみをインポートし、番号で機能を指定します。 確か Emscripten が Linux? API をエミュレーションするためにこの方式を採用していた気がします。
インターフェースとして実装する関数が少なくなるメリットがありますが、どの番号がどの機能になるか管理する必要があってサードパーティーによる拡張が難しく、一度公開された機能番号は後から変更が難しく将来廃れたAPIが歯抜けになりやすい問題もあります。
また、この方式は多くの OS においてユーザーモードからカーネルを呼び出すインターフェースに実際に使われています。 System Call や SVC (Supervisor Call) と呼ばれます。
今回はこちらを採用します。
こんにちは、世界
ではハローワールドを実装してみます。
WebAssembly に対応している言語は増えてきているので他の言語でアプリケーションを実装することもできますが、当面は Rust でサンプルを開発することにします。
Rust の wasm ターゲットは wasm32-unknown-unknown
となります。まずはコンパイラの準備をします。
% rustup target add wasm32-unknown-unknown
準備ができたらアプリフォルダを作ってゴリゴリ実装していきます。 ハローワールドくらいなら直接 API を呼び出してもいいですが、システムコール部分は myoslib というライブラリにまとめて他のアプリも開発できるようにしたいので以下のようなフォルダ構成にしました。
. ├── Cargo.toml ├── hello │ ├── Cargo.toml │ └── src │ └── main.rs └── myoslib ├── Cargo.toml └── src └── lib.rs
この構成にしてルート側の Cargo.toml の [workspace]
のところにメンバーを追加することで一発でビルドできるようになります。
また、 hello/Cargo.toml は以下のように myoslib を読み込めるようにします。
[package] authors = ["省略"] edition = "2018" name = "hello" version = "0.1.0" [dependencies] myoslib = {path = "../myoslib"}
hello/src/main.rs に以下のようなコードを書きます。
#![no_main] #![no_std] use core::fmt::Write; use myoslib::*; #[no_mangle] fn _start() { println!("Hello, world!"); }
変なおまじないがたくさんありますが、割と普通?な Rust のソースです。
これをビルドします。
% cargo build --target wasm32-unknown-unknown --release
ビルドが成功したら target/wasm32-unknown-unknown/release/hello.wasm が出来上がるのでこれを動かしてみます。
動きました。 *2
マクロの裏側
前述のハローワールドは println!
マクロを使うために結構いろんなライブラリ関数が組み込まれているようで、バイナリサイズが数十KBになります。バイナリの中をよくみるとリリースビルドしてもデバッグ情報が含まれていたので wasm-strip してみると数KBまで縮まりました。
そもそも println!
マクロを使うのをやめると数百バイトまで小さくなります。
println!
マクロは便利ですが小規模なアプリであまり乱用してはいけないと思いました。
ということで、 println!
マクロを使わないハローワールドは以下のようなコードになります。
#![no_main] #![no_std] use myoslib::*; #[no_mangle] fn _start() { os_print("Hello, world!\n"); }
この os_print
は myoslib の中で以下のように定義されています。
pub fn os_print(s: &str) { unsafe { syscall2(1, s.as_ptr() as usize, s.len()); } }
ここで呼び出している syscall2
が本当の API でシステムコールに相当します。
システムコールは Rust のソースの外の世界という扱いになるので呼び出すところは unsafe
が必要になります。
なお、システムコールを実際に処理する関数は1つですが、 WebAssembly では引数の型によって別の関数が必要になるので引数の型に合わせて syscall0
〜 syscall6
というエイリアスを提供しています。
なお、この辺はまだ実験段階なので将来細かいところが変わる可能性があります。
1MB の壁
出来あがった wasm の内容をツールで確認してみると (memory 17)
という項目があります。
これはメモリをおよそ 1MB 確保するという意味になります。
メモリをほとんど使ってないはずなのに、これはなんでしょうか?
Rust は Vec や Box などで明示的にラッピングしない限り基本的にスタックにオブジェクトを確保しようとします。 スタックはヒープよりも高速に割り当て・開放ができてアロケーターも不要になるためです。 wasm 自体も実行するためにスタックを必要としますが、こちらは通常のプログラムからは不可視です。 そこで、memory の一部を Rust が仮想的にスタックとして使える領域として使います。ここの初期値が 1MB となっています。 なお、 global 変数の 0 番目が仮想スタックポインタとして使われることが多いようです。
ほとんどメモリを使わない小さなアプリでも 1MB 固定で確保されてしまうのはなんとかしたいです。
Cargo.tomlのあるルートフォルダの下に .cargo/config
というファイルを作り、
[target.wasm32-unknown-unknown] rustflags = [ "-C", "link-args=-z stack-size=32768", ]
のような記述をすることで、この領域を小さくすることができます。数字を増やせば逆に大きくすることもできます。 スタック領域が小さすぎるとアプリが正常に動作しませんし、大きすぎると実行時に大量のメモリを無駄に消費してしまうので調整が難しいところです。
なお、 WebAssembly ではメモリサイズを 64KB 単位のページで扱っているので、スタックサイズを最小の16バイトを指定しても実行時には最低 64KB 確保されます。 極端に小さくしすぎても無駄になるので筆者は間をとってとりあえず32KBにしておきました。
おまけ: AssemblyScript の場合
AssemblyScript の場合、デフォルトではメモリ管理用のランタイムが付いてくるのでハローワールドが 15KB ほどになってしまいますが、ランタイムが必要ない場合はオプションで取り除くことでソースをそのまま WebAssembly に変換したような無駄の少ないバイナリを得ることができます。 AssemblyScript は WebAssembly 専用の言語なので、他の言語よりも WebAssembly 本来の機能に近い演算子やキーワードを記述できるのが強みな感じがします。
一方、文字列型が UTF-16 になってしまう点や、オブジェクトを API に引き渡す部分が AssemblyScript 専用のインターフェースが必要になってしまうので、今回本採用は見送りになりました。いずれ対応するかもしれません。
次のステップ
ハローワールドは動くようになりましたが、それ以上のアプリを作るにはAPIが圧倒的に足りていません。 しばらくは API を充実させていくのが主な作業になりそうです。