簡単で小さなWebAssembly

 Emscriptenの紹介ページを見ると、巨大な実行用HTMLやらグルーJSやらがくっついているhello worldのサンプルが載っている。 しかし、WebAssembly(以下面倒なのでwasm)を使いたい場合というのは、時間のかかるデータ処理などをC言語でやらせて、結果をJavaScriptで表示するという用途が圧倒的に多いはずで、C言語でブラウザにhello Worldを表示したいわけではない。 もっと気の利いた小さいサンプルが欲しい、というのがこのページの趣旨。

Cの場合

 そう考えると、取り合えず何かデータをC言語側に渡し、戻り値を受け取ってconsole.logで表示できればそれで十分なわけだ。 最近のブラウザでは驚くほど短く書ける。 C言語のソースはこんなの。

#ifdef EMSCRIPTEN
#include 
#else
#define EMSCRIPTEN_KEEPALIVE
#endif

EMSCRIPTEN_KEEPALIVE
int foo(int x) { return x * 2; }

 emccは基本的にはgccなんかと同じコンパイラドライバで、放っておくとリンクまで走る。 この時にどこからも参照されていない関数があるとリンク結果から取り除かれてしまう。 これを防ぐためにEMSCRIPTEN_KEEPALIVEというマクロを指定するが、このマクロはemscripten.hで定義されていて、このヘッダは普通のGCCやClangの環境にはないわけだ。

 そこでEMSCRIPTENマクロで切り替えている。 このマクロはどこで知ったかというと、emccもLLVMの仲間なので、clangと同じようなコマンドラインオプションが使え、マクロ一覧の調べかたもほぼ同じである。 emcc -dM -E -x c /dev/nullと打ってgrepすれば、EMSCRIPTENというマクロがすぐに見つかるだろう。 もし将来このマクロがなくなったとしても、emccを実行するときに-DEMSCRIPTENを指定すればいいだけだ。 EMSCRIPTENマクロがなければ、EMSCRIPTEN_KEEPALIVEが空になり、他の環境でもそのままコンパイルできる。 何度も打つのは面倒なので、実際に使う際にはヘッダにまとめておくといいだろう。

 これをfoo.cという名前で保存して、emcc -o foo.wasm --no-entry foo.cとするとfoo.wasmファイルができる。 手元の環境ではたったの630バイトである。 グルーJSも実行用HTMLも生成されない。wasmだけができる。 --no-entryというのはmain関数を呼び出すコードを生成しない、という意味で、要するにライブラリっぽいバイナリを生成するためのオプションである。 このオプションを指定しない場合はmain関数が必要で、main関数から参照されているものは全て残るが、コードが巨大になる。 このオプションを指定するとmainは不要になるが、放っておくと全部の関数が削除されてしまう。 そこでEMSCRIPTEN_KEEPALIVEを指定したわけだ。

 これをHTMLからJavaScriptで読み込む。

<!doctype html>
<meta charset="utf-8">
<title>foo</title>
<script>
WebAssembly.instantiateStreaming(fetch("foo.wasm")).then(wasm => {
    console.log(wasm.instance.exports.foo(10));
});
</script>

 これを開発コンソールを開いた状態のブラウザに読み込ませると、コンソールに20と表示されるだろう。 PHPのビルトインサーバーでもなんでもいいのでサーバー経由で読み込ませた方が問題が少ない。 PHPのビルトインサーバーはwasmファイルをちゃんとapplication/wasmで返してくれる。 ローカルファイルを直接指定する場合、fetchno-corsを指定しないとダメなことが多い。

 余談だが、このファイルはHTML5としても正しい文法になっているはずである。 必須なのはhtmlheadmeta charsettitlebodyだが、htmlheadbodyは開始タグ・終了タグとも省略できるので、手書きするときはこれが正しいHTML5でなおかつ一番短い状態のはずである(よい子は真似をしないように)。 そして、書かなければならないソースはこのふたつで全部である

 …つか、このHTMLファイル、bodyが空だな。 我ながらヒドいw

C++の場合

 とりあえずサンプルはこれ。 まずはJavaScriptから。

 いわゆる2次遅れ系というやつで、時系列データのフィルタにも使えるし、UI要素を動かすときもQ(あるいは減衰係数)を調整すれば減衰振動してくれる、何かと使い道が多い処理である。 本当は双一次変換とかプリワーピングとか色々あるのだが、ここではLCRフィルタを数値演算で積算しているだけである。 buildClassとかはなんかクラス作ってるんだなー、程度の知識で大丈夫。 詳しくはプロトタイプによる継承などを見てもらえば。

 実行すると黒い縦棒がびよんびよん動くが、だんだん勢いがなくなっていく。 これが減衰振動というやつだが、動いているのは背景を真っ黒に塗られ、ただ一文字[が書いてあるspan要素である。 インラインスタイルでpositionabsoluteにしてあって、JavaScript側からe.style.leftを設定すると希望の位置に動かせる、というシカケになっている。 単位としてvwを付けているので、ウィンドウの大きさが変わっても常にウィンドウの真ん中を中心にして振動する。 SVGでグラフを書いてもいいが、動きがあるものの確認にはこれが一番手っ取り早いのではないだろうか。 最近のブラウザはプロトタイピングツールとしても優秀である。 $RAFrequestAnimationFrame$IDdocument.getDocumentByIdのことだったりとかなり酷いことをやってるけど。 そこら辺はcommon.jsを見ていただければ。

 これをC++で書くとこうなる。

このソースは大雑把に上半分と下半分に分かれている。 上半分はJavaScriptをC++のコードとして実装した部分である。 下半分はC++のクラスをCから使えるようにラッパーを被せている部分である。 Emscriptenではクラス丸ごとのエクスポートは(今のところ)できないので、メンバ関数ごとにエクスポートすることになる。 そうすると、C++の名前変換(name mangling)がかかってしまい、JavaScriptからインポートするのに苦労するので、extern "C"を付けて名前変換が起きないようにしている。 IIRLPF2_createのような関数を作り、IIRLPF2クラスをnewしてそのポインタを返すのも、C言語の場合と同じである。 ポインタはヒープ先頭からのオフセットを示す整数としてJavaScript側に返され、wasm側がそれをポインタとして受け取れば、元と同じメモリブロックを参照できる、という寸法である。 EMSCRIPTEN_KEEPALIVEはCの関数にだけ付けておけばよい。 他の関数はCの関数から参照されているので、すべて自動的に残る。

 これをemcc -o iir-lpf2.wasm --no-entry iir-lpf2.cppとするとiir-lpf2.wasmができるので、それを読み込むHTMLがこれ。

プロミスを待つ部分と、C言語ラッパーを呼ぶ部分が違うくらいで、残りはJavaScriptの場合とほとんど同じである。 今はサンプルということで必要最低限のコードしか書いてないが、実用的にはデストラクタやdeleteの呼び出しが必要であることに注意。 JavaScriptにはデストラクタがないので、注意しないとメモリリークし放題になる。

 なお、newを使った途端にHTML側のwasmのロードでエラーが出る場合はEmscriptenのバージョンが古い。 今のところ、Debianのものは古い。 したがってUbuntuのものも古い。 MacのHomebrewは大丈夫である。 ちょっと追いかけたら3.1.59から仕様の変更が入ったようだ。 emsdkを使って最新のEmscriptenをインストールするか、wasm側がJavaScript側の関数をインポートしているために出るエラーなので、wasm-disあたりでインポートしているシンボルを調べて、JavaScript側で空の関数を定義しておけばよい。 instantiateStreamingの第二引数にオプションとして指定することになるが、個人的にはもう二度とやらない気がするので詳細は割愛。

 これをJavaScript側でもクラスとして扱えるようにするとこうなる。

wasm読み込みのプロミス待ちが必要になるのと、initにwasmのインスタンスを渡す必要がある以外は、JavaScriptとまったく同じになる。

 wasmを読み込んでエクスポートされた関数を持ってくるところは、将来的にクラスが増えたとしても共通になるので、使いまわせるようにまとめてしまおう。

WasmLoaderはちょっとしたメタクラスっぽくなっており、loadを呼ぶと読み込んだwasmオブジェクトを保持するためのクラスを返す。 このクラスを基底クラスとして派生クラスを作り、派生クラスから基底クラスのコンストラクタをきちんと呼び出せば、this.wasmにエクスポートオブジェクトが入る仕掛けである。 このオブジェクトは静的メンバで構わないのだが、this.wasmとアクセスした方が楽なのでインスタンスメンバにしてある。

 load関数では先にクラスを作ってローカル変数clsに保持しておき、instantiateStreamingのプロミスが解決するとwasmオブジェクトが得られるので、それをクラスclsの静的メンバとして設定している。 load関数自身はclsをそのまま返している。 cls.wasmを設定するのはプロミスの中だから、この時点ではまだcls.wasmは設定されていないわけだ。 このプロミスが解決するまではinitの中でthis.wasmが設定できないので別途プロミスを待つ必要があり、そのための関数がreadyである。 複数のwasmを読み込んだ場合はプロミスも複数になるため、Promise.allで全部のプロミスが解決するまで待つようになっている。 逆に、ひとつのwasmに複数のクラスが詰め込まれている場合はload関数の戻り値を適当な変数にとっておいて使いまわせばよい。 残りのコードは(字下げを除いて)純粋なJavaScript版とまったく同じになる。

 なお、今回は独自のbuildClassという変な関数を使ってクラスを作っているが、最近のJavaScriptのclass構文でも匿名クラスを作って変数に代入することができるので、同様に実装できるはずである。

 こうやってグルーコードって増えていくんだなー、と実感した今日この頃。


20 Jan 2025: 「C++の場合」を追加

14 Jan 2025: 新規作成

ご意見・ご要望の送り先は あかもず仮店舗 の末尾をご覧ください。

Copyright (C) 2024-2025 akamoz.jp