TL;DR
- コンパイル済みの wasm ファイルに後からデータを注入し、注入したデータをその wasm から読むことができた
- あらかじめローカルでビルドしておいた wasm に対し、ブラウザ上で動的にリソースを追加することが可能になる
- サンプルコードのリポジトリはこちら
はじめに
Rust から WebAssembly の memory.init 命令を使いたかった話 という記事を書きましたが、その後日談です。
上記記事中で、 memory_init
命令を使用したい箇所をダミー関数として括りだしておき、後から本物の memory.init
を内容を注入するというアイディアを書きましたが、それがうまく行ったのでその報告です。
サンプルコードの実行
サンプルコードとして、注入された文字列とその長さを読み取って出力するだけの wasm ファイルと、その wasm ファイルに文字列を注入するプログラムを用意しました。ここではそれを実行してみます。
Rustの開発環境一式(ネイティブおよび wasm
ターゲット)は揃っているものとします。筆者の手元ではWindows上の rustc 1.72.0-nightly (5ea666864 2023-06-27)
で検証を行いました。
-
cargo
cargo-wasiおよびwasmtime1をインストールします。 -
サンプルコードを適当な場所に
clone
します - データ注入の対象となるwasmをビルドします
a.crates/tartget-wasm/
以下に移動します
b.cargo wasi build -r
を実行します - データ注入を行います
a. リポジトリのルートに移動し、cargo run -- (注入元wasm) (生成先) (表示文字列)
を実行します
例:cargo run -- .\target\wasm32-wasi\release\target-wasm.wasi.wasm .\hello.wasi.wasm "hello world!"
b. 上記コマンドで生成された wasm ファイルを wasmtime などで実行します
c. a. で与えた表示文字列が出力されれば成功です
技術的な解説
先の記事でも書いたとおり、Rust単体では WebAssembly の memory.init
命令を直に使用することができません。
そのため、データ注入側で memory.init
を使ってデータをメモリ上にロードする処理を追加してあげる必要があります。
被注入側
被注入側のコード(target-wasm
)には、 memory_init
という関数を設けています。注入側のプログラムはこの関数をプレースホルダとして利用し、この関数を書き換えることで上記の処理を追加します。
memory_init
には __core_memory_init
というエクスポート名が指定してあります。これはビルド時に関数名がwasmファイル内から消されてしまう場合が多いため、注入側がこの関数をエクスポート名から識別できるようにするためです。
また、memory_init
は内部で __core_memory_init_dummy
というインポート関数を呼び出しています。これは memory_init
やその引数を最適化等から防ぐ意図があります。
注入側
注入側のコード(wasm-data-injection-sample
)では、まずエクスポートされている関数に、そのエクスポート名をつけ直しています。これは上記の、関数名が消去されている状況に対応するためです。
次に __core_memory_init
に対して、実際に WebAssembly の memory.init
命令を叩くコードを追加します。
最後に後片付けとして、最適化防止用のダミーである __core_memory_init_dummy
を消去したり、memory_init
の不要なエクスポート指定を外したりしています。
最後に
注入側のコードも当然wasmにコンパイルできるため、あらかじめビルドしておいたwasmファイルに対し、ブラウザ上で動的にリソースを追加したりすることができます。
これはブラウザで完結するゲーム制作ツールやコンパイラ、自己解凍書庫などに応用できるのではと考えています。
-
wasmer など、WASIに準拠する他のランタイムでも構いません ↩