wasm3ランタイムにオレオレAPIを生やす実験
組み込み野郎とwasm3
Web方面じゃないwasmの話です。
どちらかといえば組み込み方面です。
組み込みシステムでも使えそうなwasmランタイムにwasm3というものがあります。これを使えば組み込みのショボい環境でも割と安全にユーザープロセスっぽいことができるのでは?と思いました。
組み込みなので、wasm側に数値計算してもらって結果だけ返してもらっても何も嬉しくありません。そんなことするんだったらCで直接書いたほうが(手間的にも、実行速度的にも)よっぽど早いです。
そうではなく、システムの重要な部分は保護しつつ、各種IPをふわっと制御するためのお気楽APIをwasmから呼び出したいのです。
というわけで、wasm3にオレオレAPIを生やしてwasm側から呼び出す実験をしてみました。
記事の内容は一般的なwasmの話題ではなく、wasm3に特化したものになります。
やりたいこと
というわけで、今回のやりたいことは「wasm3ランタイム側に自作関数(kuso_code())を定義し、この関数をwasmバイナリ側から呼び出して動かしたい」です。
やってみる
ちゃんとwasmの仕様を勉強したほうがいいのは百も承知ですが、私は短気なのでいきなりwasm3のコードをいじります。wasm3はC言語で実装されているから多分なんとかなると思います(組み込み業界の人だいたいC言語できる説に基づく)。どうしてもわかんないことがあれば調べるつもりです。
自分のwasm理解度(合ってるかどうか怪しい)
wasm実行環境と外界のやりとりについて自分の理解度は低く、wasm実行環境と外界の間では
- 明示的にインポートしたりエクスポートした関数のみアクセス可能
- 複雑なデータ構造は線形メモリとやらで受け渡しする
くらいしか知りません。
オレオレAPIをwasm実行環境から呼び出すための調査
インポート
というわけで、関数をインポートする処理が分かれば後は好きに動かせるんだろという雑な理解に基づき、wasm3を調べます。
インポートに関してはwasm3のコードをざっくり眺めてみると、source/m3_api_libc.cあたりが参考になりそうです。
m3_LinkLibCという関数があり、ここでいくつかの関数を実行環境にくっつけているようです。
見ると_
とか_catch
とか謎のマクロがありますが、こいつらは軽く無視します。
m3_LinkRawFunction()
が大事な関数のようです。
m3_LinkRawFunction (module, env, "_debug", "i(*i)", &m3_libc_print)
どうやらこれは
m3_LinkRawFunction(くっつける実行環境, モジュール名, 関数名, 引数と返値, C言語での関数実体へのポインタ)
のようです。
あー、ここを参考にkuso_code()
用を作ればいいわけですね。了解です。
やりたいことの前半部分「wasm3ランタイム側に自作関数(kuso_code())を定義し、」はこれでよい気がします。
めでたしめでたしです。
wasm実行環境の動かし方
次は後半部分「この関数をwasmバイナリ側から呼び出して動かしたい」です。
動かない実行環境に価値はありません。動いてナンボです。
wasm3はコマンドラインから動かせるランタイムを作ることができ、その場合のmain()はplatforms/app/main.cになります。
このコマンドライン版wasm3は実行以外にもいろいろな機能を持ったデキる奴なので処理が長く読むのが面倒です。
骨子だけを知りたいので、もうちょっと簡単なやつを探します。
platforms/embedded/arduino/src/main.cppが簡単でした。みんな大好きArduino。このmain()は簡単です。
wasm3実行環境の動かし方(雑な感じ)
ものすごく雑にまとめると以下のようになります。
/* まずは環境を生成 */
IM3Environment env = m3_NewEnvironment ();
/* 環境からランタイムを生成 */
IM3Runtime runtime = m3_NewRuntime (env, 1024, NULL);
/* wasmバイナリを読み込む */
IM3Module module;
/* wasm_program はwasmバイナリへのポインタ、program_sizeはバイナリのサイズ */
m3_ParseModule (env, &module, wasm_program, program_size);
m3_LoadModule (runtime, module);
/* 関数のインポート処理 */
m3_LinkRawFunction (module, "env", "kuso_code", "i(*i)", &kuso_code)
/* wasmプログラム側からエクスポートされている関数を探す */
m3_FindFunction (&f, runtime, "fib");
/* wasmプログラムの関数を実行する */
m3_CallV(f);
/* リソース開放 */
/* m3_FreeModule(module); */ /* moduleをfreeしてはいけない */
m3_FreeRuntime(runtime);
m3_FreeEnvironment(env);
Tips
最後の解放時にm3_FreeModule()を呼ぶと不幸が起きます。(segvが発生したりなど)
ここの説明によると、m3_LoadModule()
実行後はmoduleの所有権がruntime側に移るので、moduleを開放してはいけないようです。
おためし実装
Linux(Ubuntu-22.04)で動作確認しています。
clangとlldが必要です。
動かし方
sudo apt install clang lld
git clone https://github.com/kaz399/wasm3-local-experiment.git
cd wasm3-local-experiment
git submodule update --init
make test
実装
kuso_code()
はlocal-apiディレクトリで定義しています。
kuso_code.c
が関数本体です。
kuso_code.h
はwasmバイナリ側を作るときに参照するkuso_code()のヘッダです。
kuso_kode_link.h
はwasm3でkuso_code()のインポート処理を行う関数のヘッダです。こちらはwasm3のビルド時に必要で、wasmバイナリを作るときには参照しません。
kuso_code()は文字列へのポインタと長さを引数に取り、受け取った文字列を表示するだけの超適当な関数です。
int32_t kuso_code(const char *str, uint32_t size);
関数本体はこんな感じです。wasm3的なお作法に従って(既存実装で参考になりそうな部分をコピペして)書きました。
m3ApiRawFunction(kuso_code) {
m3ApiReturnType(uint32_t)
m3ApiGetArgMem(void *, i_ptr) m3ApiGetArg(uint32_t, i_size)
m3ApiCheckMem(i_ptr, i_size);
puts(i_ptr);
m3ApiReturn(i_size);
}
M3Result local_LinkKusoCode(IM3Module module) {
M3Result result = m3Err_none;
result = m3_LinkRawFunction(module, "env", "kuso_code", "i(*i)", &kuso_code);
if (result == m3Err_functionLookupFailed) {
result = m3Err_none;
}
return result;
}
wasm実行環境を動かすのはminimal-main.cです。minimalとか言いながら、やたらエラーメッセージを出しまくったせいで肥大しています。
動作結果
デバッグログを出しているのでゴチャゴチャしていますが、末尾から7行手前に
hello kuso code!
が出ています。大成功です。
./wasm3_with_kuso_code ./wasm-code/test_kuso_code.wasm
WASM3:START
m3_NewEnvironment
m3_NewRuntime
m3_ParseModule
parse | load module: 571 bytes
parse | load module: 571 bytes
parse | ** Type [4]
parse | type 0: (i32, i32) -> i32
parse | type 1: () ->
parse | type 2: (i32) -> i32
parse | type 3: () -> i32
parse | ** Import [1]
parse | kind: 0 'env.kuso_code'
module | added function: 0; sig: 0
parse | ** Function [3]
module | added function: 1; sig: 1
module | added function: 2; sig: 2
module | added function: 3; sig: 3
parse | ** Memory [1]
parse | ** Global [7]
parse | global: [0] i32 mutable: 1
parse | global: [1] i32 mutable: 0
parse | global: [2] i32 mutable: 0
parse | global: [3] i32 mutable: 0
parse | global: [4] i32 mutable: 0
parse | global: [5] i32 mutable: 0
parse | global: [6] i32 mutable: 0
parse | ** Export [10]
parse | index: 0; kind: 2; export: 'memory';
parse | index: 1; kind: 0; export: '__wasm_call_ctors';
parse | index: 2; kind: 0; export: 'test_kuso_code';
parse | index: 3; kind: 0; export: 'test';
parse | index: 1; kind: 3; export: '__dso_handle';
parse | index: 2; kind: 3; export: '__data_end';
parse | index: 3; kind: 3; export: '__global_base';
parse | index: 4; kind: 3; export: '__heap_base';
parse | index: 5; kind: 3; export: '__memory_base';
parse | index: 6; kind: 3; export: '__table_base';
parse | ** Code [3]
parse | ** Data [1]
parse | segment [0] memory: 0; expr-size: 4; size: 17
parse | ** Custom: 'name'
parse | ** Custom: 'producers'
m3_LoadModule
m3_FindFunction
exec function function:0x5641b937add0
hello kuso code! ★☆★ ここ!!
m3_FreeRuntime
module | freeing module: test (funcs: 4; segments: 1)
m3_FreeEnvironment
WASM3:END
free wasm.data
return code:0
補足:Cからwasmバイナリを作る
wasmバイナリのソースコードはこのディレクトリにあります。
今回はCからwasmを作っています。
Cからwasmバイナリを作るのは、こんな感じでやります。
clang --target=wasm32 foo.c
wasm-ld --no-entry --export-all --allow-undefined foo.o -o foo.wasm
おわりに
超適当にやってみましたが、思ったよりハマらず動かせました。
これでwasmからMCUのGPIOやI2Cなど、いろいろなペリフェラルを動かせるな気がしてきました。
(割り込みハンドラやコールバックにwasm側の関数を使いたいときはどうすればいいの?とか真面目に使うことを考えると解決すべき疑問はまだまだありますが。。)
そのうち本物のMCU上でも動かしてみたいです。
おしまい。
Discussion