EmscriptenでC/C++をWebAssemblyにコンパイルする

ここではEmscriptenを使ってC/C++をWebAssembly(.wasm)にコンパイルし、WASMモジュール(動的ライブラリ)やスタンドアロンWASMを作成する方法について紹介します。

 

Emscripten SDKのインストール

まず最初に下記のツールをインストールします。次に公式マニュアルの手順通りにすれば、WindowsEmscripten SDKをインストールできます。

〈D:\sdkの直下にEmscripten SDKをインストールする手順〉
Windowsコマンドプロンプト)
cd /d d:\sdk
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
git pull
emsdk install latest
emsdk activate latest

 

WASMモジュール(動的ライブラリ)の生成

example.cのようにmain関数を含まない場合は、下記のようにコンパイルしてWASMモジュールを生成することができます。

example.c

double add(double x, double y) { return x + y; }
double mul(double x, double y) { return x * y; }
 

 


set path=d:\sdk\emsdk\upstream\emscripten;%path%
emcc -Os -s SIDE_MODULE=1 example.c -o example.wasm

注)-Osはサイズの最適化(optimization for size)です。実行速度を最適化する場合は、-O1、-O2、-O3を使ってください。詳しくはこちらを参照してください。GCCコンパイラのOptimization Levelのベンチマーク結果はこちらです。

C++(.cpp)の場合は、下記のように em++ を使います。

example.cpp

auto add(double x, double y) -> double { return x + y; }
auto mul(double x, double y) -> double { return x * y; }
 

 


set path=d:\sdk\emsdk\upstream\emscripten;%path%
em++ -std=c++17 -Os -s SIDE_MODULE=1 example.cpp -o example.wasm

注)上記のem++の場合コンパイルはうまくいくのですが、実行時に呼び出しでエラー(emccの方は問題ありません)になります。原因はaddとmulの関数名が別の関数名_Z3addddと_Z3mulddに書き換えられるからです。 そこでWABT(WebAssembly Binary Toolkit)のwasm2watコマンドを使って.wasm(バイナリ形式)から.wat(テキスト形式)に変換し、エディタで関数名を元のaddとmulに戻します。 それから再びWABTのwat2wasmコマンドを使って.watから.wasmに変換すれば、関数を呼び出せるようになります。もちろん書き換えられた関数名で呼び出すようにしても構いません。ちょっと面倒ですが仕方ありません。

〈書き換えられた関数名を元に戻す〉

wasm2wat example.wasm -o example.wat
"_Z3adddd" → "add"
"_Z3muldd" → "mul"
wat2wasm example.wat -o example.wasm

 

Node.jsでWASMモジュールを実行

生成されたexample.wasmは、Node.jsから下記のように呼び出すことができます。

main.mjs

import * as fs from "fs";
const example = await WebAssembly.instantiate(fs.readFileSync("example.wasm"));
console.log(example.instance.exports.add(1, 1));
console.log(example.instance.exports.mul(1, 1));
 

 

〈Node.jsで実行〉

node main.mjs

 

ブラウザでWASMモジュールを実行

次のようにするとscript要素の中でWASMモジュールを呼び出すことができます。

main.html

<!doctype html>
<html>
<body>
<script>
(async () => {
  const response = await fetch("example.wasm");
  const buffer = await response.arrayBuffer();
  const example = await WebAssembly.instantiate(buffer);
  document.body.innerHTML += `<p>${example.instance.exports.add(1, 1)}</p>`;
  document.body.innerHTML += `<p>${example.instance.exports.mul(1, 1)}</p>`;
})();
</script>
</body>
</html>
 

 

但し、このHTMLファイルは単独では実行できないので、HTMLファイルがあるディレクトリでWebサーバーを起動しブラウザからアクセスします。

Pythonの簡易Webサーバーを起動〉

python -m http.server 8080

 

〈ブラウザからWebサーバーにアクセス〉

(ブラウザのアドレスバー)
http://localhost:8080/main.html



スタンドアロンWASMの生成

main.cppのようにmain関数を含む場合は、次のようにコンパイルしてスタンドアロンWASMを生成することができます。

main.cpp

#include <iostream>
auto main() -> int { std::cout << "Hello!" << std::endl; }
 

 


set path=d:\sdk\emsdk\upstream\emscripten;%path%
em++ -std=c++17 -Os -s STANDALONE_WASM main.cpp -o main.wasm

 

生成されたmain.wasmは、WasmerWasmtimeなどのWASM実行エンジンを使って実行できます。

〈Wasmerで実行〉

wasmer main.wasm

 

まとめ

Node.jsのC++ addonsを使ってモジュールを作るよりもはるかに簡単にWASMモジュールを作ることができます。ただ、実行速度はネイティブに比べかなり遅い場合があります。

 

参考記事

注)わかりやすくするためにタイトルを変更している記事があります。