wasi-sdkでWASMを試す(ブラウザで実行する)
こんにちは、CX事業本部のうらわです。
以前書いた以下の記事ではwasi-sdk
をダウンロードしてClangでC++のコードをWASMにコンパイルし、wasmtimeを使用してブラウザ外でWASMを実行してみました。
今回は、引き続きwasi-sdk
を利用し、今度はWASMをブラウザで実行してみます。
環境
Macで実施します。webサーバを実行するためにpythonも使用します。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.7 BuildVersion: 19H15 $ python --version Python 3.8.5
本記事のコードは以下のGitHubリポジトリに格納してあります。
実装
まずはsrc/calc/calc.cpp
を作成します。
引数のdouble
型の値を10,000,000倍して四捨五入するという単純なコードです。C++の場合、名前修飾(名前マングリング)をしないようにextern "C"
の記述をすることで、JavaScriptでcalc
という名前で関数を利用できます。
#include <cmath> extern "C" double calc(double a) { return std::round(a * 10000000); }
次に、src/calc/calc.js
を作成します。/build/calc.wasm
を読み込み、ボタンクリックでcalc
関数を実行します。
(async () => { const response = await fetch("/build/calc.wasm"); const bytes = await response.arrayBuffer(); const { instance } = await WebAssembly.instantiate(bytes); console.log(instance); document.getElementById("wasm-button").addEventListener("click", () => { console.log("Call calc():", instance.exports.calc(Math.random())); }); })();
最後に、src/calc/index.html
を作成します。<script>
タグで上記のJavaScriptファイルを読み込みます。
<!DOCTYPE html> <html> <head></head> <body> <h1>calc.cpp wasm sample</h1> <button id="wasm-button">Wasm Calc Click!</button> <script src="calc.js"></script> </body> </html>
以上で実装は完了です。
WASMにコンパイル
以下のコマンドでC++のコードをWASMにコンパイルします。
./wasi-sdk/bin/clang++ \ --sysroot=./wasi-sdk/share/wasi-sysroot \ -nostartfiles \ -Wl,--export-all \ -Wl,--no-entry \ src/calc/calc.cpp -o build/calc.wasm
オプションについては以下の通りです。 しかし、この説明だけだと(C/C++の経験が浅い私にとっては)目的がよくわからないので、これらのオプションを付けない状態でコンパイルするとどうなるか確認してみました。
--sysroot
: wasi-libcを使用するために設定する。-nostartfiles
: コンパイラがリンク時に標準のシステムライブラリを使わないようにする。--export-all
: すべてのシンボルをエクスポートする。--no-entry
: エントリーポイントのシンボルを検索しない。
各オプションがない時の挙動
--sysroot
--sysroot
がない場合、以下のエラーでコンパイルに失敗しました。
$ /path/to/wasi-sdk/bin/clang++ --sysroot=/path/to/wasi-sdk/share/wasi-sysroot -Wl,--export-all -Wl,--no-entry src/calc/calc.cpp -o build/calc.wasm src/calc/calc.cpp:1:10: fatal error: 'cmath' file not found #include <cmath> ^~~~~~~ 1 error generated.
wasi-libc
についてはGitHubリポジトリのREADMEに簡潔な説明がありました。
WASI Libc is a libc for WebAssembly programs built on top of WASI system calls. It provides a wide array of POSIX-compatible C APIs, including support for standard I/O, file I/O, filesystem manipulation, memory management, time, string, environment variables, program startup, and many other APIs.
WebAssembly/wasi-libc: WASI libc implementation for WebAssembly
-nostartfiles
-nostartfiles
がない場合、以下のエラーでコンパイルに失敗しました。
$ /path/to/wasi-sdk/bin/clang++ --sysroot=/path/to/wasi-sdk/share/wasi-sysroot -Wl,--export-all -Wl,--no-entry src/calc/calc.cpp -o build/calc.wasm wasm-ld: error: /path/to/wasi-sdk/share/wasi-sysroot/lib/wasm 32-wasi/libc.a(__main_argc_argv.o): undefined symbol: main clang-11: error: linker command failed with exit code 1 (use -v to see invocation)
-nostartfiles
については以下の情報が参考になります。このオプションがあるとmain
関数が呼ばれるのを防ぐとのことですが、今回のC++のコードにはmain
関数自体が存在しないためundefined symbol: main
のエラーメッセージが表示されます。
When is the gcc flag -nostartfiles used? - Stack Overflow
Undefined symbol compile error with library · Issue #62 · WebAssembly/wasi-sdk
--export-all
/--no-entry
--export-all
がない場合、コンパイルには成功します。
$ /path/to/wasi-sdk/bin/clang++ --sysroot=/path/to/wasi-sdk/share/wasi-sysroot -nostartfiles -Wl,--no-entry src/calc/calc.cpp -o build/calc.wasm
しかし、ブラウザを開いてWASMを試してみると、コンソールに以下のエラーが出ました。JavaScriptからcalc
関数を利用できない状態のようです。
calc.js:7 Uncaught TypeError: instance.exports.calc is not a function at HTMLButtonElement.<anonymous> (calc.js:7)
--no-entry
がない場合、以下のエラーでコンパイルに失敗しました。--no-entry
によって_start
というシンボルを探すのを防ぎます(_start
って何?という件については本記事では触れません。_start C
等で検索するとわかりやすい記事がヒットすると思います)。
$ /path/to/wasi-sdk/bin/clang++ --sysroot=/path/to/wasi-sdk/share/wasi-sysroot -nostartfiles -Wl,--export-all src/calc/calc.cpp -o build/calc.wasm wasm-ld: error: entry symbol not defined (pass --no-entry to suppress): _start clang-11: error: linker command failed with exit code 1 (use -v to see invocation)
なお、これらのオプションについては以下の情報が参考になります。
WebAssembly lld port — lld 13 documentation
動作確認
pythonのhttp.server
でWebサーバを起動します。
python -m http.server
src/calc/index.html
を選択します。画面に表示されるボタンをクリックするたびに、コンソールにWASMによる計算結果が出力されます。
おわりに
簡単なC++のコードをClangでWASMにコンパイルし、ブラウザでJavaScriptから関数を呼び出してみました。 Emscriptenであればブラウザで実行できるWASMに簡単にコンパイルできそうですが、Clangではコンパイルオプションをちゃんと把握しておかないとコンパイルに失敗してしまいました(当然ですが)。 Clangのオプションは最初はわけがわからず大変でしたが、エラーメッセージを元に地道にオプションの意味を調べていくのはC++/WASMの勉強になるかと思います。
参考記事
Compiling C to WebAssembly and Running It - without Emscripten | Depth-First