2019の8日目のエントリです。12月28日にコードを書き始めて、大晦日に記事公開となりました。この記事ではRust製アプリにwasm形式のプラグインを動的にリンクして実行する方法を紹介します。WebAssemblyのランタイム環境にはFastlyが開発したLucetを用います。これによりWebブラウザを通さず、Rustアプリ上で直接WebAssemblyコードが実行できます。WebAssemblyコードはLucetのコンパイラで事前にネイティブコードにコンパイルしますので、Rustで書かれたコードに近い速度で実行できます1。プラグインにwasm形式を採用すると、特定の言語に縛られずにプラグインを開発できるというメリットがあります。ファイルアクセスなどの制限のかかったサンドボックス内でプラグインを実行しますので、ある程度の安全性を確保できます。WebAssemblyのシステムインターフェースとしてWASIを使いますので、許可されたプラグインならファイルアクセスなども可能になります。サンプルコードのプラグイン側は一方をRustで、もう一方をCで実装しました。FFIが使用できる言語なら同じようなしくみが構築できると思います。元記事についてこの記事は、dalanceさんとubnt_intrepidさんが以下の記事で提案されていたRustのプラグインシステムを参考にして書きました。Rustで動的ロードによるプラグインシステムを作る(dalanceさん、2019-12-24)Rustの(FFIまわりの安全性を考慮した)プラグインシステム(ubnt_intrepidさん、2019-12-25)元記事ではRustで書かれたアプリから、Rustで書かれた共有ライブラリをプラグインとしてロードして実行しています。この記事はそれらのプラグイン部分をwasmに置き換えたものになります。筆者はLucetが公開されたときから、WebAssembly自体とLucetを試してみたいと思っていました。プラグインシステムのようなものが必要になる機会もなく、気がついたら公開から1年余りが過ぎていました。この記事を書くきっかけを提供してくださったお二人に感謝します。筆者にとってはこれが初めてのWebAssemblyプログラミングになりますので、間違いなどあるかもしれません。なにかお気づきの点があれば、コメント欄などでお知らせください。サンプルプロジェクトリポジトリはこちらになります。https://gitlab.com/tatsuya6502-rust-mokumoku/rust-wasm-plugin-example元記事と同様、電卓プログラムに演算子をプラグインで追加するといった感じのサンプルプログラムです。アプリ本体とプラグインを2つ含んだCargoワークスペースになっています。x86_64ネイティブアプリcalc-plugin-add:足し算を行うプラグインRustで実装。Rustコンパイラでwasm形式へコンパイル後、Lucetコンパイラでx86_64ネイティブコードを含んだ共有ライブラリ(so)へ変換するcalc-plugin-mul:掛け算を行うプラグインCで実装。clangでwasm形式へコンパイル後、Lucetコンパイラでx86_64ネイティブコードを含んだ共有ライブラリ(so)へ変換する実際にプラグインを使っている様子はcalc/src/main.rsにあります。Ok(())}プラグインマネージャのインスタンスを作成し、2つのプラグインをロードします。続けて各プラグインのcalcで計算しています。実行すると以下のようになります。2元記事と同じ実行結果になります。プラグインシステムにWebAssemblyを使うメリットWebAssemblyはWebブラウザ上でネイティブコードに近い実行速度で高速に実行できるバイナリフォーマットとして誕生しました。現在ではLucetのようなランタイムが現れ、Webブラウザ外の環境でも実行できるようになっています。そのようなブラウザ外の環境で、メモリ、スレッド、ファイル、ネットワークなどを安全に使うためのインターフェースとしてWASI(ワズィと発音)という標準規格の策定も進められています。アプリケーションのプラグインとしてwasm形式の動的ロードライブラリを使うことで、以下のようなメリットがあります。言語非依存プラグインをさまざまな言語で開発できるC、C++、Rust、Go、AssemblyScriptなどからwasmを生成できるクロスプラットフォームwasmファイルはCPU非依存の命令で構成されている。ランタイム環境で実行されるときになって初めてネイティブコードにコンパイルされるプラグインを提供する側は単一のwasmファイルを用意するだけでよい。ユーザはそれを好きな環境に持っていってネイティブコードへコンパイル、実行できるただしLucetは現状ではx86_64系のLinuxと、実験的にmacOSをサポートしているのみ。将来はARMなどもサポートしていく予定セキュリティ使用メモリの上限やファイルアクセスの制限などのあるサンドボックス内で実行されるので、他人が開発したプラグインでも、比較的安全に実行できるLucetとはLucetはWebブラウザ外で使えるWebAssembly実行環境で、コンパイラなどのツールチェインとランタイムで構成されます。Alliance傘下のプロジェクトになっています。Lucetには以下のような特徴があります。事前コンパイル方式Lucetではlucetcという最適化コンパイラで、wasmをネイティブコードにAOTコンパイル(事前コンパイル)します。この方式ではコンパイルにかかる時間は長くなりますが、コンパイル済みのコードが最初から高速に実行できます。同じコードを繰り返し使うプラグイン的な用途に適しているといえるでしょう。Webブラウザではダウンロードしたwasmを即実行しなければならないため、AOTとJITコンパイルを組み合わせるのが普通です。最適化をほとんどしない高速コンパイラでAOTコンパイルし、その後、最適化コンパイラによるJITコンパイルで実行中のコードを最適化されたものに差し替えていきます。この方式はコードの1回限りの実行には適していますが、同じコードを繰り返し使う用途には無駄が多くなります。インスタンスの柔軟な設定Lucetでは動的ロードしたwasmライブラリをインスタンス化する際に柔軟な設定ができます。APIが用意されています。FFIをサポートするさまざまな言語から利用できます。ランタイム自体はRustで書かれています。Lucetの特徴は以上になります。余談ですが、Lucetが生まれた背景についてはPublickeyの独占インタビュー記事が面白かったです。CTOに聞く、同社がWebAssembly実行環境の「Lucet」をエッジコンピューティング環境として開発している理由とは?(Publickey、2019-05-14)同記事によると、Lucetという名前は、バイキングがロープや組ひもなどを作るために使った道具に由来しているそうです。環境のセットアップコードの内容を説明する前に、サンプルコードのビルドと実行に必要な環境のセットアップ方法を説明します。x86_64とDockerLucetを使用するには現状ではx86_64上のLinuxが必要です。Lucetのツールチェインを簡単にセットアップするためにDockerも必要です。RustRustはstable版を使います。1.40.0を使用しました。ターゲットを追加するので、rustupでインストールするのがお勧めです。stable版がインストールできたら、wasm32-wasiターゲットを追加します。wasm32-wasiまたx86_64環境向けのリンカが必要です。binutils(Ubuntuの場合)などでインストールしてください。LucetツールチェインLucetのツールチェイン(AOTコンパイラなど)はDockerイメージ内に構築されます。以下のようにしてセットアップします。3.51GBコードの解説ここからはコードの内容を解説していきます。さきほどクローンしたlucetディレクトリの下にサンプルコードを置きます。いうのは、LucetのDockerベースのツールチェインがアクセスできるのはlucetディレクトリ配下のファイルに限定されているからです。buildで全体を一括してビルドすることができません。今回はcalc、calc-plugin-*のそれぞれに対してコマンドを叩いてコンパイルします。プラグインのAPI元記事では各プラグインが提供する機能(API)をPluginトレイトとして定義していました。u32;}電卓として必要なのはcalcだけですが、表示用にStringを返すメソッドも用意されています。WebAssemblyで実現する場合は好きな言語で書ける反面、APIも複数の言語で通用する形式で定義しなければなりません。このようなケースでは、APIをインターフェース記述言語(IDL)という専用の言語で記述し、それを元に実装言語向けのスタブコードを生成するのが一般的です。LucetにもIDLがあるのですが、今回は使用しませんでした。いうのは現状で生成できるのはCのコードだけなので、Rustで実装するときにには、あまり役に立ちそうもなかったからです。IDLはまた別の機会に試してみようと思います。いうわけで今回はスタブなどを自動生成せず、手でコードを書いていきます。このやり方ですと、もし関数のシグネチャ(名前と引数)を間違えたり、関数自体を定義し忘れたりしても、コンパイルエラーにはなってくれません。実行時のエラーになります。不便ですが今回のように小規模なAPIなら大きな問題にはならないでしょう。calc関数の実装(Rust版)まずは実際に計算を行うcalc関数を実装します。Rust版のcalc-plugin-addでは以下のようになります。"C"が必要ですが、それ以外は特に難しい事はありません。Rust版プラグインのCargo.tomlも作成します。["cdylib"]クレートタイプはcdylibにします。コンパイルターゲットはwasm32-wasiになります。.cargo/configを作成してデフォルトのターゲットを変更します。"wasm32-wasi"calc関数の実装(C版)C版のcalc-plugin-mulは以下のようになります。b;}こちらはごく普通のCプログラムですね。ホスト側のCargoパッケージの設定プラグイン側に並行して本体側の実装も見ていきます。calcパッケージのCargo.tomlの依存クレートは以下のようになっています。"0.3"元記事にならってエラー処理用のanyhowクレートを使用します。lucet-runtimeがWebAssemblyランタイムです。このランタイムのバージョンと、Lucetツールチェインのバージョンが合っていないと、せっかく作成した共有ライブラリがバージョン不一致のエラーでロードできなくなるので注意してください。筆者はそれで理由でツールチェインのDockerイメージを再構築することになってしまいました。この記事の最初の方の手順に従って、lucetリポジトリのタグをチェックアウトしていれば大丈夫です。0.4.1)wasi-common-lucetはWASIのサポートをランタイムに追加するためのものです。実は実装し始めたときはWASIなしの素のWebAsseblyで書いていたのですが、それだとデバッグが難しく、諦めてWASIを追加しました。memoryエラー)が出力されず、辛かったです。ホスト側(calcパッケージ)のRustコンパイラには追加のパラメータが必要です。.cargo/configファイルを追加して以下の内容を書きました。"link-args=-rdynamic"]実はこれと同じ内容が親ディレクトリであるlucetリポジトリの.cargo/configにも書かれていて、Cargoはそれを探し出して読んでいるようです。いまのディレクトリ構成ですと、ここで指定しなくても大丈夫かもしれません。将来、calcをlucetディレクトリの外でビルドすることもあるかもしれないので、念のためここにも書いておきます。PluginManagerロードした個々のプラグインを表すPluginと、それらを管理するPluginManagerを実装します。Vec<Plugin>,}PluginはLucetランタイムにロードされたプラグインのインスタンスへのハンドルを持ちます。PluginManagerはロードしたPluginを保持するVecを持ちます。PluginManagerの実装は以下のとおりです。これに加えてヒープ領域として最低1ページ必要。}}init関数で呼んでいるLucetの2つの関数ですが、これらについてのドキュメントがないので、なにをするためのものかはわかりません。動的リンクモジュールのリンクエラーを防ぐためのものだと想像しています。動的リンクモジュールから使われる予定の関数が、ビルド時の最適化で削除されないようにする効果があるようです。1つ目のlucet_runtime::lucet_internal_ensure_linked()関数はコード例にも載っていません。これがないと本体側を--releaseモードでビルドした際、実行時にプラグインの動的リンクに失敗します。request(Lucetランタイムのバージョン0.5向け)の修正後のコードで呼ばれていることを見つけて、ここに追加したところその問題が解決しました。Lucetランタイムのバージョン0.5以降では1つ目の関数はランタイム内部で呼ばれるので不要になるようです。プラグインをロードするload関数では最初にlucet_runtime::Limitsというプラグインのインスタンスに課す制限を定義しています。現状では設定できるのはメモリ関連の上限値だけのようです。このなかのheap_memory_sizeの設定値を変更します。デフォルトの1MBでは、Rustで書いたプラグインをインスタンス化するときに制限値オーバーでエラーになってしまうためです。ここではヒープと呼ばれていますが、このWebAssemblyのヒープはRustでいうヒープ領域を表すわけではありません。ゲストが読み書き可能かつ動的に拡張可能なメモリ領域という意味になります。そのゲストメモリの中をどう使うかはプラグインに任されており、RustやCだとヒープ領域に加えてスタック領域としても使っているようです。WebAssemblyのゲストメモリのページサイズは64KBです。Rustのプラグインはスタック領域として1MB超割り当てるようなので、17ページ(1.06MB)の使用を許可したら動きました。これに加えてヒープ領域も必要です。今回はヒープ領域は文字列を格納するくらいにしか使わないので1ページで十分ですが、余裕をみて2ページまで許可しましょう。heap_memory_sizeは合計で19ページ分の1.19MBに設定しました。LucetのDlModule::load関数でwasmの共有ライブラリを読み込みます。MmapRegion::create関数でゲストメモリを作成します。createの引数に先ほど定義したLimitsを与えます。MmapRegionのnew_instanceメソッドで、そのゲストメモリに紐づくプラグインのインスタンスを作成します。new_instanceメソッドはインスタンスへのハンドル(InstanceHandle)を返すので、それをPlugin構造体に格納します。Lucetの関数やメソッドの呼び出し後、?演算子の前にmap_err(anyhow::Error::msg)を付けました。Lucetのエラー型はstd::error::Errorトレイトを実装していないので、?演算子でanyhowのエラー型に暗黙的に変換できません。map_err()で明示的に変換しています。Plugin側のcalcメソッドの実装Pluginにcalcメソッドを実装します。}}このように、WebAssemblyの関数はself.instanceが束縛されたInstanceHandleのrunメソッドで実行します。runメソッドの戻り値はlucet_runtime::RunResultです。これはenumとして定義されており、バリアントとして戻り値を示すReturned(UntypedRetVal)とコルーチンのyieldを示すYielded(YieldedVal)を持ちます。後者はLucetのhostcallの使用時に現れることがあるようです。今回はReturnedバリアントしか使わないので、unwrap_returnedメソッドでアンラップしてます。unwrap_returnedメソッドは値がYieldedバリアントだったときはpanicするので注意が必要です。Error>を返すreturnedメソッドを使用してください。最後にUntypedRetValのintoメソッドで、値をu32型に変換しています。u32;}name関数とoperator関数を実装します。WebAssemblyでStringを返すのは少し面倒です。WebAssemblyの組み込み型にはStringのような複合型がないからです。そのため関数の引数として受け取ったり、戻り値として返すことができません。Rustのwasm-bindgenはStringなどの複合型を簡単に扱えるようにJavaScriptコードを自動生成してくれます。LucetではFFIの相手がJavaScriptではないので、wasm-bindgenは使えません。複合型の扱いは自分で実装することになります。プラグインのインスタンスが使用するメモリ(ゲストメモリ)は、本体側(ホスト側)からでもアクセスできます。プラグイン側の関数がゲストメモリのどこに文字列を格納したのか、そのアドレスを返せば、ホスト側でその文字列にアクセスできます。文字列データのメモリ上のフォーマットは自由に決められますが、プラグインをRust以外の言語で実装することを考慮して、FFIで一般的に使われているC形式のヌル終端文字列を使うことにします。Rust版のcalc-plugin-add側の実装はこうなります。s.into_raw()}str_to_char_ptrはプライベート(非公開)の関数です。この関数はまず文字列リテラルからCStringを作成します。これによりゲストメモリのヒープ領域にヌル終端文字列が作られます。c_char)を取得しています。into_raw()を呼んだことでCStringの所有権が追跡されなくなり、str_to_char_ptr関数からリターンしたあともヌル終端文字列がゲストメモリ上に残ります。ホスト側ではname関数やoperator関数が返した文字列の先頭アドレスを使ってゲストメモリ上のヌル終端文字列にアクセスし、その内容から自分のメモリ上にStringを作成します。(言葉ではわかりにくいので、後日、図を追加しようと思います)ホスト側のコードは以下のようになります。文字列の取り出しはtransfer_string関数が行います。}}本体側のヒープにStringを作成したあとは、ゲスト側のヌル終端文字列は不要になります。プラグインのrelease_string関数を呼び出す事で削除します。calc-plugin-addのrelease_string関数の実装は以下のとおりです。CStringがスコープを抜けるので、使用していたヒープ領域(ゲストメモリ)が解放されるStringを返す関数(C版)C版のcalc-plugin-mulではStringを返す関数は以下のようになります。free(str);}筆者はCの万年初心者なので、これが模範的なコードなのかはあまり自身がありません。コードの解説は以上になります。サンプルコードのビルドと実行サンプルコードをビルドしていきましょう。lucetディレクトリに移動してLucetツールチェインを使えるようにします。pwdlucet/rust-wasm-plugin-examplecalcのビルドまずはcalc(Rustアプリ本体)のビルドです。ターゲットはx86_64-unknown-linux-gnuです。target/release/calccalc-plugin-addのビルド次にcalc-plugin-addをビルドします。このプラグインはRustで書かれており、Rustコンパイラでwasmファイルへとコンパイルします。ターゲットはwasm32-wasiで、calc-plugin-add/.cargo/configで指定してあります。上位のlucetディレクトリにも.cargo/configファイルがあって、それのせいでビルドエラーになってしまいます。そのファイルをいったん削除してからビルドします。.cargo/config)calc-plugin-mulのビルドcalc-plugin-mulをビルドします。このプラグインはCで書かれており、clangでwasmファイルへとコンパイルします。Lucetのツールチェイン(Dockerコンテナ上)にあるwasm32-wasi-clangコンパイラを使います。target/wasm32-wasi/release/calc_plugin_mul.wasmwasmファイルをAOTコンパイルLucetの最適化コンパイラでWebAssemblyをネイティブコードへコンパイルします。target/wasm32-wasi/release/calc_plugin_mul.so共有ライブラリ(soファイル)ができました。必須ではありませんがツールチェインの入ったDockerコンテナを停止します。containerlucet実行それでは実行しましょう。2うまく動作しているようです。まとめLucetを使うとRustなどで書かれたアプリケーション上でWebAssemblyコードを実行できるプラグインとしてwasm形式を採用することで以下のようなメリットがある言語非依存:プラグインをさまざまな言語で開発できるクロスプラットフォーム:プラグインの作者はwasmファイルを提供するだけでよい。プラグインのユーザはそれをさまざまな環境でネイティブコードにコンパイルして実行できるセキュリティ:プラグインがサンドボックス内で実行されるので、他人が開発したプラグインでも、比較的安全に実行できるLucetのエコシステム現状ではStringなどの複合型の扱いが面倒Lucetを対象としたライブラリの充実に期待(複数言語に対応したデータのシリアライズなど)IDLはいまのところCのコード生成のみ(今度、試してみたい)ただし、現状ではWebAssemblyからSIMD命令などを直接使えないので、コンパイラの性能にもよりますが、Rustコンパイラで直接ネイティブコードを作成した場合よりは遅くなりそうです。現状ではWebAssemblyコードはシングルスレッドで実行されます。(tatsuya6502)Erlang/OTPやRustで分散DBを開発してます。最近、Rustの本を書きました。(tatsuya6502)。