【WebAssembly】C/C++で実装された既存の資産をincludeする

f:id:shogonir:20170514025514p:plain

目次

  1. この記事の目的
  2. includeされる側のクラスを実装
  3. ビルド用のスクリプトを準備
  4. includeする側の実装
  5. HTMLからwasmを読み込み実行する
  6. まとめ

 

1. この記事の目的

この記事ではC++でwasmの実装を行う際に、C++で書かれた既存の資産をincludeする方法を紹介します。
それほど大した情報ではありませんが、自分はハマったので紹介します。

今回は下記のような例でサンプルコードを紹介します。

  • C++で書かれた既存の資産に、2つの整数の和を返すスタティックメソッドが実装されている
  • wasmのソースコードから上記のメソッドを呼び出す

 
GitHubソースコードを上げました。

github.com

 

2. includeされる側のクラスを実装

2つの整数の和を計算する既存の資産が、以下のように実装されていたとします。
私は正直あまりC++が得意ではないのですが、よくある構成になっていると思います。

まずはヘッダーファイル。 (adder.h)

class Adder
{
public:
    static int add(int, int);
};

 
次に実装。 (adder.cpp)

#include "adder.h"

int Adder::add(int a, int b)
{
    return a + b;
}

 

3. ビルド用のスクリプトを準備

まずはC++のコードをwasmに変換するcpp2wasm.shを下記の通り保存します。
コンパイルするファイルがinclude.cppで、add(int, int)が公開される前提です。

emcc include.cpp \
    -s WASM=1 \
    -s "MODULARIZE=1" \
    -s "EXPORTED_FUNCTIONS=['_add']" \
    -s "DEMANGLE_SUPPORT=1" \
    -o include.js

 
上記のスクリプトをDockerコンテナで実行するためのcompile.shを下記の通り保存します。

docker run --rm -t -v $(pwd):/src gifnksm/emscripten-incoming sh cpp2wasm.sh

 
これで、compile.shを実行することでinclude.js, include.wasmに変換できるはずです。

 

4. includeする側の実装

それではincludeする側、wasmの実装を行います。

#include "adder.h"

extern "C" {
    
    int add(int a, int b) {
        return Adder::add(a, b);
    }
}

 
この状態でcompile.shを実行すると以下の警告文が出力されます。

warning: unresolved symbol: _ZN5Adder3addEii

 
HTMLからwasmを読み込んでadd(int, int)を呼び出すと、下記のようなエラーになります。

missing function: _ZN5Adder3addEii
failed to asynchronously prepare wasm: abort(-1) at Error
Uncaught (in promise) abort(-1) at Error

 
Adder::add(int, int)の定義しかないので、コンパイル時に未定義だと警告されます。
警告を無視してHTMLからwasmの読み込みを行うと、Adder::add(int, int)の実装がないのでエラーが出ます。
これは、JSのコールバック関数を定義したときのエラーと似ています。
今回はあとからJSでの実装を流し込まないので、Adder::add(int, int)の実装もinclude.cppから見えるようにしないといけません。

解決するには、cpp2wasm.shを修正します。

emcc include.cpp adder.cpp \
    -s WASM=1 \
    -s "MODULARIZE=1" \
    -s "EXPORTED_FUNCTIONS=['_add']" \
    -o include.js

 
emccのコンパイル対象にadder.cppも追加しました。
こうすることでadder.hにあるAdder::add(int, int)の実装を解決することができます。
(コメントにて情報を提供してくださったueshitaさん、ありがとうございます)

 

5. HTMLからwasmを読み込み実行する

HTMLは下記のようになります。
onRuntimeInitializedのタイミングでadd(int, int)を取り出し、実行しています。
wasmの読み込みや関数の取り出し方についてはこちらを参照してください。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <title>include</title>
  </head>
  <body>
    <script src="include.js"></script>
    <script>

var module;

fetch('include.wasm')
  .then(response => response.arrayBuffer())
  .then(buffer => new Uint8Array(buffer))
  .then(binary => {
    var moduleArgs = {
      wasmBinary: binary,
      onRuntimeInitialized: function () {
        var add = module.cwrap('add', 'number', ['number', 'number']);
        console.log(add(2, 3));
      }
    };
    module = Module(moduleArgs);
  });

    </script>
  </body>
</html>

 
http-serverを実行してこのHTMLを表示すると、コンソールに2と3の和である5が表示されています。

f:id:shogonir:20170520224741p:plain

 

6. まとめ

今回はC++でwasmの実装を行う際に、既存の資産を実装する方法を紹介しました。
includeする際は、対象のcppファイルをコンパイル対象にすることで実装と定義の両方が見える必要があります。