Emscriptenの紹介ページを見ると、巨大な実行用HTMLやらグルーJSやらがくっついているhello worldのサンプルが載っている。 しかし、WebAssembly(以下面倒なのでwasm)を使いたい場合というのは、時間のかかるデータ処理などをC言語でやらせて、結果をJavaScriptで表示するという用途が圧倒的に多いはずで、C言語でブラウザにhello Worldを表示したいわけではない。 もっと気の利いた小さいサンプルが欲しい、というのがこのページの趣旨。
そう考えると、取り合えず何かデータを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
で返してくれる。
ローカルファイルを直接指定する場合、fetch
にno-cors
を指定しないとダメなことが多い。
余談だが、このファイルはHTML5としても正しい文法になっているはずである。
必須なのはhtml
・head
・meta charset
・title
・body
だが、html
・head
・body
は開始タグ・終了タグとも省略できるので、手書きするときはこれが正しいHTML5でなおかつ一番短い状態のはずである(よい子は真似をしないように)。
そして、書かなければならないソースはこのふたつで全部である。
…つか、このHTMLファイル、body
が空だな。
我ながらヒドいw
とりあえずサンプルはこれ。 まずはJavaScriptから。
いわゆる2次遅れ系というやつで、時系列データのフィルタにも使えるし、UI要素を動かすときもQ(あるいは減衰係数)を調整すれば減衰振動してくれる、何かと使い道が多い処理である。
本当は双一次変換とかプリワーピングとか色々あるのだが、ここではLCRフィルタを数値演算で積算しているだけである。
buildClass
とかはなんかクラス作ってるんだなー、程度の知識で大丈夫。
詳しくはプロトタイプによる継承などを見てもらえば。
実行すると黒い縦棒がびよんびよん動くが、だんだん勢いがなくなっていく。
これが減衰振動というやつだが、動いているのは背景を真っ黒に塗られ、ただ一文字[
が書いてあるspan要素である。
インラインスタイルでposition
がabsolute
にしてあって、JavaScript側からe.style.left
を設定すると希望の位置に動かせる、というシカケになっている。
単位としてvw
を付けているので、ウィンドウの大きさが変わっても常にウィンドウの真ん中を中心にして振動する。
SVGでグラフを書いてもいいが、動きがあるものの確認にはこれが一番手っ取り早いのではないだろうか。
最近のブラウザはプロトタイピングツールとしても優秀である。
$RAF
はrequestAnimationFrame
、$ID
はdocument.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