RustをWebAssemblyにコンパイルしてlucet-runtimeで動かす
はじめに
Lucet は Fastly が開発している WebAssembly のコンパイラ & ランタイムです。
公式ドキュメント:
Lucet は WebAssembly を実行するための CLI に加え、Rust のプログラムから Lucet を利用できる lucet-runtime
という ランタイム API も提供しています。
今回、この lucet-runtime を公式ドキュメントの通りに試したところ意外とハマるポイントが多かったことや、
公式ドキュメントには WebAssembly にコンパイルするサンプルコードがC 言語で書かれたものしかなく「Rust で書くには?」と試行錯誤したため、備忘録的に手順をまとめておきます。
Lucet のソースコードをコンパイルしたもので動作確認しましたが、記事執筆時点の最新コミットは 51fb1ed
です。
前提:必要なものをインストール
Lucet 本体
「2.1. Compiling Lucet」に従い Lucet をローカル環境でコンパイルします。
ドキュメントでは Linux と macOS でのインストール手順が記載されており、私は macOS でコンパイルしました。
wasm32-wasi
Rust のビルドターゲット WASI に対応した WebAssembly ファイルにコンパイルするため、ビルドターゲットを追加しておきます。
$ rustup target add wasm32-wasi
手順
1. Rust でパッケージを作成し、WebAssembly にコンパイルする
はじめに、WebAssembly ファイル作成用のパッケージを作成します。パッケージの置き場所、パッケージ名は自由に決めて大丈夫です。
$ cd ~/workspace
$ cargo new example
$ cd example
src/main.rs
の中身は、ここでは println!
だけの main 関数とします。
fn main() {
println!("Hello, Lucet!");
}
保存したら、このファイルをビルドします。その際、ターゲットに wasm32-wasi
を指定します。
$ cargo build --target wasm32-wasi
target/wasm32-wasi/debug
ディレクトリの下に ***.wasm
(***
はパッケージ名。この場合 example
)が生成されています。
2. WebAssembly ファイルをネイティブコードにコンパイルする
Lucet が実行するのは WebAssembly ファイルそのものではなく、事前に WebAssembly をネイティブコードにコンパイルしたファイルです。
(このあたり私は詳しくありませんが、AOT (Ahead of Time) コンパイル方式と呼ぶそうです。
参考:WASMで様々なリソースにアクセス. How Lucet runs WebAssembly | by FUJITA Tomonori | nttlabs | Medium)
ネイティブコードへのコンパイルには lucetc-wasi
というコマンドを使います。
Lucet のコンパイルの手順の中で source /opt/lucet/bin/setenv.sh
を実行していればパスが通っているはずです。
$ lucetc-wasi -o example.so target/wasm32-wasi/debug/example.wasm
$ ls
Cargo.lock Cargo.toml example.so* src/ target/
3. lucet-runtime を使ったパッケージを Lucet リポジトリ内に作成する
作成した .so ファイルを読み込み、lucet-runtime で実行するための別のパッケージを作成します。
Lucet リポジトリ内にパッケージを作成する際、2通りの方法があります。
- リポジトリ内に
docs/lucet-runtime-example
というサンプルパッケージがあるため、これをそのまま編集する - リポジトリ内の任意の位置に
cargo new
でパッケージを作成し、リポジトリのルートにある Cargo.toml のmembers
に追加する
後者の場合で説明します。前者の場合、3-1 〜 3-3 は不要ですが 3-4 main.rs の修正は必要です。
3-1. リポジトリのルートの Cargo.toml にパッケージへのパスを追加
# 例: src/ ディレクトリの下に hello-lucet-runtime というパッケージを作る
$ cd path/to/lucet
$ mkdir src
$ cd src
$ cargo new hello-lucet-runtime
でパッケージを作成した後、リポジトリのルートの Cargo.toml にパッケージへのパスを追加します。
[workspace]
members = [
"benchmarks/lucet-benchmarks",
"docs/lucet-runtime-example",
+ "src/hello-lucet-runtime",
"lucet-concurrency-tests",
"lucet-module",
...
この後は基本的には「2.3. Using the Lucet runtime API from Rust」に従い、作成したパッケージを編集します。
3-2. .cargo/config ファイルを追加
[build]
rustflags = ["-C", "link-args=-rdynamic"]
[dependencies]
に lucet-runtime および lucet-wasi を相対パスで追加
3-3. Cargo.toml の # 相対パスはパッケージディレクトリの深さによって変わる
[dependencies]
lucet-runtime = { path = "../../lucet-runtime" }
lucet-wasi = { path = "../../lucet-wasi" }
3-4. src/main.rs を編集
src/main.rs を次のようにします。
use lucet_runtime::{DlModule, Limits, MmapRegion, Region};
use lucet_wasi::WasiCtxBuilder;
fn main() {
// ensure the WASI symbols are exported from the final executable
lucet_wasi::export_wasi_funcs();
// load the compiled Lucet module
let dl_module = DlModule::load("example.so").unwrap();
// (*1) create a new memory region with default limits on heap and stack size
let region = MmapRegion::create(
1,
&Limits::default().with_heap_memory_size(100 * 16 * 64 * 1024),
)
// instantiate the module in the memory region
let mut instance = region.new_instance(dl_module).unwrap();
// prepare the WASI context, inheriting stdio handles from the host executable
let wasi_ctx = WasiCtxBuilder::new().inherit_stdio().build().unwrap();
instance.insert_embed_ctx(wasi_ctx);
// (*2) run the WASI main function
instance.run("_start", &[]).unwrap();
}
ドキュメントとは違う点が2箇所あります。
-
(*1)
heap memory size の変更-
&Limits::default()
のままだとエラーになるので、.with_heap_memory_size()
というメソッドを使用して割り当てるメモリサイズを変更する - 数値に根拠はないが、コードから デフォルト値は
16 * 64 * 1024
だった ので 100倍を指定している
-
-
(*2)
最後の行でinstance.run(...)
に渡す文字列は、"main"
ではなく"_start"
が正しい
4. .so ファイルをコピー
ステップ2. で作成した .so ファイルを Lucet 内に作成したパッケージ下に配置します。
$ cd path/to/lucet/src/hello-lucet-runtime
$ cp ~/workspace/example/example.so .
$ ls
Cargo.toml example.so* src/
5. 実行
cargo run
で実行します。手順1. で println!
マクロに記載した文字列が表示されれば成功です。
$ cargo run
Hello, Lucet!
応用
ここまでで main 関数だけのスクリプトは WebAssembly にコンパイルして動かすことができました。
以下はもう少し複雑なことを WebAssembly 側でやろうとして動作を確認したメモです。
任意の関数名で実行したい
main.rs 側で #[no_mangle]
つきで定義します。
#[no_mangle]
fn hello() {
println!("hello");
}
lucet-runtime を使う側では、instance.run()
に指定する関数名を変更すれば OK です。
fn main() {
// ...
// run the WASI main function
instance.run("hello", &[]).unwrap();
}
関数の引数に値を渡したい & 関数からの戻り値を得たい
まず、WebAssembly にコンパイルする側。こちらは通常の Rust の関数と同じように引数、戻り値を定義します。
#[no_mangle]
fn add(a: i32, b: i32) -> i32 {
return a + b;
}
lucet-runtime を使う側では、instance.run()
の第二引数で引数を指定します。
また戻り値は unwrap_returned()
を呼び出すことで取得できるようです。
fn main() {
// ...
// instantiate the module in the memory region
let mut instance = region.new_instance(dl_module).unwrap();
// prepare the WASI context, inheriting stdio handles from the host executable
let wasi_ctx = WasiCtxBuilder::new().inherit_stdio().build().unwrap();
instance.insert_embed_ctx(wasi_ctx);
// run the WASI main function
- instance.run("_start", &[]).unwrap();
+ let retval = instance
+ .run("add", &[5i32.into(), 3i32.into()])
+ .unwrap()
+ .unwrap_returned();
+ println!("{}", i32::from(retval)); // 8
+}
ここについては少し理解が不十分ですが、https://docs.rs/lucet-runtime/0.6.1/lucet_runtime/ を読んだところ
引数は Val
、戻り値は UntypedVal
という型であり、どちらも適切な型でキャストする必要があるためこのような書き方をする必要がある、という理解です。
また Val
を見る限り文字列などの直接のやり取りはできなさそうです。(要調査)
トラブルシューティング
調べる過程で遭遇したエラー達です。
ドキュメントの通りに main.rs 書いたけどエラー
最初に遭遇したやつ。
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: SymbolNotFound("main")', docs/lucet-runtime-example/src/main.rs:17:31
解決策💡
instance.run("main", ...)
ではなく instance.run("_start", ...)
が正しかった。
Issue がある。
lucet-runtime, lucet-wasi のバージョンを指定して実行したけどエラー
Lucet リポジトリとは無関係のディレクトリでパッケージを作成し、
[dependencies]
lucet-runtime = "0.6.1"
lucet-wasi = "0.6.1"
として実行したところ以下のエラー。
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: DlError(Custom { kind: Other, error: "dlopen(/Users/yamazaki/workspace/lucet-runtime-example/example.so, 2): Symbol not found: _hostcall_wasi_snapshot_preview1_fd_close\n Referenced from: /Users/yamazaki/workspace/lucet-runtime-example/example.so\n Expected in: flat namespace\n in /Users/yamazaki/workspace/lucet-runtime-example/example.so" })', src/main.rs:8:50
解決策💡
上に書いたとおり、Lucet リポジトリ内にパッケージを作って最新のコードを使う。
lucet-runtime, lucet-wasi をリポジトリの最新バージョンを指定したけどエラー
Cargo.toml の dependencies には git のリポジトリも指定できるので、これで Lucet 内に置いたのと同様に動くのでは?と思って試したやつ。
[dependencies]
lucet-runtime = { git = "https://github.com/bytecodealliance/lucet", branch = "main" }
lucet-wasi = { git = "https://github.com/bytecodealliance/lucet", branch = "main" }
結果は以下のエラー
--> /Users/yamazaki/.cargo/git/checkouts/lucet-d29c7ec0f6d2e0d4/51fb1ed/wasmtime/crates/wasi-common/cap-std-sync/src/file.rs:84:14
|
84 | .set_times(convert_systimespec(atime), convert_systimespec(mtime))?;
| ^^^^^^^^^ method cannot be called on `cap_std::fs::File` due to unsatisfied trait bounds
|
::: /Users/yamazaki/.cargo/registry/src/github.com-1ecc6299db9ec823/cap-std-0.13.9/src/fs/file.rs:32:1
|
32 | pub struct File {
| ---------------
| |
| doesn't satisfy `_: unsafe_io::unsafe_handle::AsUnsafeFile`
| doesn't satisfy `cap_std::fs::File: SetTimes`
|
= note: the following trait bounds were not satisfied:
`cap_std::fs::File: unsafe_io::unsafe_handle::AsUnsafeFile`
which is required by `cap_std::fs::File: SetTimes`
= help: items from traits can only be used if the trait is in scope
= note: the following trait is implemented but not in scope; perhaps add a `use` for it:
`use fs_set_times::set_times::SetTimes;`
解決策💡
同上
ヒープメモリサイズが足りないというエラー
docs/lucet-runtime-example
のコードのまま実行したところ以下のエラー。
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: LimitsExceeded("heap spec initial size: HeapSpec { reserved_size: 4194304, guard_size: 4194304, initial_size: 1114112, max_size: None }")', docs/lucet-runtime-example/src/main.rs:102:55
解決策💡
上に書いたとおり、 MmapRegion::create()
の第二引数を変更する。
let region = MmapRegion::create(
1,
&Limits::default().with_heap_memory_size(100 * 16 * 64 * 1024),
)
ヒープメモリサイズに適当な値を指定するとエラー
一つ前で、サイズが足りないと言うので適当に大きな値を指定したところ
let region = MmapRegion::create(
1,
&Limits::default().with_heap_memory_size(1000000),
)
それはそれで別のエラー。
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: InvalidArgument("memory size must be a multiple of host page size")', docs/lucet-runtime-example/src/main.rs:96:91
解決策💡
同上
参考リンク
-
https://github.com/bytecodealliance/lucet
- リポジトリ
-
https://bytecodealliance.github.io/lucet/Overview.html
- 公式ドキュメント
- 該当箇所はこのあたり
-
https://docs.rs/lucet-runtime/0.6.1/lucet_runtime/
- lucet-runtime の仕様とか
-
Announcing Lucet: Fastly’s native WebAssembly compiler and runtime | Fastly
- ここにも使い方書いてるけど古い。公式ドキュメントの方を読んだほうがいい
- Rust で WASI 対応の WebAssembly を作成して実行 - なんとなくな Developer のメモ
- WASMで様々なリソースにアクセス. How Lucet runs WebAssembly | by FUJITA Tomonori | nttlabs | Medium
-
WebAssemblyランタイム比較(Lucet, Wasmer, Wasmtime)
- 私のスクラップです。このあと Lucet 以外も試す予定です
-
Cargoのワークスペース - The Rust Programming Language 日本語版
- Cargo.toml の
members
ってなんじゃい、となったときに読んだ
- Cargo.toml の
-
From and Into - Rust By Example
- lucet-runtime の引数と戻り値のキャストに関係して。まだちゃんと読んでない
Discussion