Emacs で WASI 入門したハナシ
Emacs で WASI 入門したハナシ
こんばんは。(ラッシャー木村風)
この記事は PySpa Advent Calendar 2022 の 17 日目の記事です 昨日は drillbits でした。
普段は趣味でほそぼそと Rust
を書いたり、 Emacs
のパッケージを書いたりいろいろなことをしてるのですが、 WebAssembly
周りをあまりキャッチアップ
してなかったのでキャッチアップしようと思います。
といってもそんな詳しく調査したわけでもなく最低限、使えるぐらいになれればいいとレベルです。
まあ、せっかくなのでついでになんか作った方が理解が深まるだろうということで Emacs
を絡めたものを作ってみました。
その辺のハナシをしていきたいと思います。
WebAssembly (WASM)
asm.js
云々みたいな歴史的な経緯は詳しい誰かに聞いて下さい。
とりあえず WebAssembly
、 WASM
の大きな特徴として、ポータブルなスタックマシンとして設計されていることが挙げられます。
機能も限定的なものとし、その分設計を簡素化、高速に動作します。
もちろんブラウザ上で動作するため、サンドボックス環境で動作するように設計されています。
ポータビリティが高く、安全にしかも高速に動作する特徴はいろんな環境で応用できます。
これらの特徴はとても魅力的なのでブラウザ外でも動かしたいと思うのは自然の流れかも知れません。
そこで登場するのが Java!ではなく WASI
です。
WASI WebAssembly System Interface
WASM
の特徴を活かしつつ、ブラウザ以外で WASM
使用するための規格です。
ブラウザ外なのでファイルやネットワークなどの使用も視野に入っており、ホストのそれらの資源に安全アクセスさせるための仕様も含むよう検討されています。 仕様は標準化団体、bytecodealliance のもと標準化してます。
ランタイム
ランタイム実装は幾つかあります。
- wasmtime
- bytecodealliance が開発、実質リファレンス実装
- wasmer
- wasmer 社が開発。呼び出しが高速などの特徴がある
が現状主要でよく使われていると思います。
その他に、WasmEdge のように WASI
の特徴を活かし、コンテナの代わりにしようとする試みもあります。
Docker なども実験的にのランタイムをサポートし始めています。
この辺りはまた別の機会に。
ホストとのコミュニケーション
WASI
プログラムはサンドボックス環境で実行するわけですが、ホストとコミュニケーションをとる必要があるケースもあると思います。
ホストとのコミュニケーション方法はいくつかの方法があります。
ひとつは標準入出力、もうひとつは Linear Memory
です。
標準入出力の場合は通常のプログラムと同様、stdin
, stdout
を読み書きしてデータを交換します。
Linear Memory
は単純なメモリ構造で確保から何から何まで自前で管理する方法です。メモリ上でデータをやり取りするので非常に高速です。
ただホスト側の関数を呼び出し、読み出し開始アドレス、何バイト書いたかの情報を別途やりとりし、メモリ上のデータを読み書きする必要があります。
ホスト側の関数を呼び出せるようにしたりいろいろ手続きが必要なため、複雑になります。
WASI を Emacs から読み込んで使ってみる
というわけで今回は Emacs
から WASI
を読み込んで動作させてみたいと思います。
リポジトリは以下にあります。
このモジュールは以下の処理を行います。
- 特定ディレクトリ配下の
wasm
プログラムを読み込む Emacs
から関数呼び出しのタイミングで渡された名前のプログラムをランタイム上で実行するstdio
経由でデータをやり取りする
今回はランタイムに wasmtime を使用します。
初期化、読み込み
pub fn load(&mut self, env: &Env, wasm_dir: &str) -> Result<()> {
// search wasm file
if let Ok(entries) = fs::read_dir(wasm_dir) {
let entries: Vec<fs::DirEntry> = entries
.flatten()
.filter(|x| x.path().extension().unwrap_or_default() == "wasm") // filer .wasm
.collect();
for entry in entries {
if let Ok(path) = entry.path().canonicalize() {
if let Some(file) = path.file_stem() {
if let Ok(module) = Module::from_file(&self.engine, &path) {
// register wasm module
env.message(format!("register wasm module {:?} {:?}", &file, &path))?;
let name = file.to_string_lossy().to_string();
self.modules.entry(name).or_insert(module);
}
}
}
}
}
Ok(())
}
難しいことはありません。WASM
ファイルを読み込んで Hashmap
に保持しています。
以下が実際の呼び出しコードです。
pub fn call(&self, \_env: &Env, name: &str, args: &[String]) -> anyhow::Result<String> {
if let Some(module) = self.modules.get(name) {
// new linker
let mut linker = Linker::new(&self.engine);
wasmtime_wasi::add_to_linker(&mut linker, |s| s)?;
// set stdin
let stdin = ReadPipe::from(args[1].to_string());
let stdout = WritePipe::new_in_memory();
// build wasi ctx
let ctx = WasiCtxBuilder::new()
.stdin(Box::new(stdin))
.stdout(Box::new(stdout.clone()))
.args(args)?
.build();
let mut store = Store::new(&self.engine, ctx);
linker.module(&mut store, "", module)?;
// debug
// env.message(format!("call wasm: {}.wasm", name))?;
// call main
linker
.get_default(&mut store, "")?
.typed::<(), (), _>(&store)?
.call(&mut store, ())?;
drop(store);
// get captured output
let output: Vec<u8> = stdout
.try_into_inner()
.expect("sole remaining reference to WritePipe")
.into_inner();
let out: String = String::from_utf8(output)?;
Ok(out)
} else {
anyhow::bail!(format!("unknown wasm module: {}", name))
}
}
Hashmap
から対応しているモジュールを取り出し、Ctx
を作成してプログラムを実行しています。
標準入出力経由で WASI
プログラムとデータ交換するため、Ctx
に ReadPipe
, WritePipe
をセットして実行します。
(WritePipe
を読み込むには store
を破棄する必要がある)
上記のように標準入出力経由であれば WASI
プログラムとコミュニケーションとるのはそこまで難しくなくコード量も非常に少なくて済みます。
最後に
安全で高速に動作する共通なフォーマットとして WASI(WASM)
は期待されており、上記のようなプラグインシステムなどに使われるようになると思います。
すでに Envoy のフィルタでも WASM は採用されていますし、今後はこのようなケースがもっと増えていくと思います。
WASI
は規格上まだ標準化されている部分は少ないです。今後は複雑なネットワーク周りなども標準化されていく予定です。
現状でもネットワークが使用できるようなものもありますが、各社が独自で実装先行で進めてる部分もあるためどのライブラリを使えばいいか判断が難しいところでもあります。
Web ブラウザ外での WASM
に関しても今後注目していきたいところです。