Emacs で WASI 入門したハナシ

Emacs で WASI 入門したハナシ

こんばんは。(ラッシャー木村風)

この記事は PySpa Advent Calendar 2022 の 17 日目の記事です 昨日は drillbits でした。

普段は趣味でほそぼそと Rust を書いたり、 Emacs のパッケージを書いたりいろいろなことをしてるのですが、 WebAssembly 周りをあまりキャッチアップ してなかったのでキャッチアップしようと思います。 といってもそんな詳しく調査したわけでもなく最低限、使えるぐらいになれればいいとレベルです。

まあ、せっかくなのでついでになんか作った方が理解が深まるだろうということで Emacs を絡めたものを作ってみました。 その辺のハナシをしていきたいと思います。

WebAssembly (WASM)

asm.js 云々みたいな歴史的な経緯は詳しい誰かに聞いて下さい。 とりあえず WebAssemblyWASM の大きな特徴として、ポータブルなスタックマシンとして設計されていることが挙げられます。

機能も限定的なものとし、その分設計を簡素化、高速に動作します。

もちろんブラウザ上で動作するため、サンドボックス環境で動作するように設計されています。

ポータビリティが高く、安全にしかも高速に動作する特徴はいろんな環境で応用できます。

これらの特徴はとても魅力的なのでブラウザ外でも動かしたいと思うのは自然の流れかも知れません。

そこで登場するのが Java!ではなく WASI です。

WASI WebAssembly System Interface

WASM の特徴を活かしつつ、ブラウザ以外で WASM 使用するための規格です。

ブラウザ外なのでファイルやネットワークなどの使用も視野に入っており、ホストのそれらの資源に安全アクセスさせるための仕様も含むよう検討されています。 仕様は標準化団体、bytecodealliance のもと標準化してます。

ランタイム

ランタイム実装は幾つかあります。

が現状主要でよく使われていると思います。 その他に、WasmEdge のように WASI の特徴を活かし、コンテナの代わりにしようとする試みもあります。

Docker なども実験的にのランタイムをサポートし始めています

この辺りはまた別の機会に。

ホストとのコミュニケーション

WASI プログラムはサンドボックス環境で実行するわけですが、ホストとコミュニケーションをとる必要があるケースもあると思います。 ホストとのコミュニケーション方法はいくつかの方法があります。

ひとつは標準入出力、もうひとつは Linear Memory です。

標準入出力の場合は通常のプログラムと同様、stdin, stdout を読み書きしてデータを交換します。

Linear Memory は単純なメモリ構造で確保から何から何まで自前で管理する方法です。メモリ上でデータをやり取りするので非常に高速です。 ただホスト側の関数を呼び出し、読み出し開始アドレス、何バイト書いたかの情報を別途やりとりし、メモリ上のデータを読み書きする必要があります。 ホスト側の関数を呼び出せるようにしたりいろいろ手続きが必要なため、複雑になります。

WASI を Emacs から読み込んで使ってみる

というわけで今回は Emacs から WASI を読み込んで動作させてみたいと思います。

リポジトリは以下にあります。

pyspa/emacs-wasm-loader

このモジュールは以下の処理を行います。

  • 特定ディレクトリ配下の 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 プログラムとデータ交換するため、CtxReadPipe , WritePipe をセットして実行します。

WritePipe を読み込むには store を破棄する必要がある)

上記のように標準入出力経由であれば WASI プログラムとコミュニケーションとるのはそこまで難しくなくコード量も非常に少なくて済みます。

最後に

安全で高速に動作する共通なフォーマットとして WASI(WASM)は期待されており、上記のようなプラグインシステムなどに使われるようになると思います。 すでに Envoy のフィルタでも WASM は採用されていますし、今後はこのようなケースがもっと増えていくと思います。

WASI は規格上まだ標準化されている部分は少ないです。今後は複雑なネットワーク周りなども標準化されていく予定です。 現状でもネットワークが使用できるようなものもありますが、各社が独自で実装先行で進めてる部分もあるためどのライブラリを使えばいいか判断が難しいところでもあります。

Web ブラウザ外での WASM に関しても今後注目していきたいところです。