wasi-sdkでWASMを試す(ブラウザで実行する)

wasi-sdkを利用してC++のコードWSAMにコンパイルしブラウザで実行してみます。
2021.03.30

こんにちは、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という名前で関数を利用できます。

src/calc/calc.cpp

#include <cmath>

extern "C" double calc(double a) {
    return std::round(a * 10000000);
}

次に、src/calc/calc.jsを作成します。/build/calc.wasmを読み込み、ボタンクリックでcalc関数を実行します。

src/calc/calc.js

(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ファイルを読み込みます。

src/calc/index.html

<!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