【WebAssembly】C/C++からJavaScriptで実装した関数を実行する

f:id:shogonir:20170514025514p:plain

目次

  1. この記事の目的
  2. C/C++でコールバック関数の定義のみ書く
  3. JavaScriptでコールバック関数の実装を書く
  4. emccで –pre-js オプションでビルドする
  5. HTMLから読み込んで実行する
  6. まとめ

 

1. この記事の目的

C/C++からJavaScriptで実装した関数を実行するサンプルを紹介します。
今回はwasm側で動的に確保した配列の領域を、JSのコールバック関数から読み込んで表示します。

次のようなサンプルを実装していこうと思います。
JSからwasmに配列長を渡すと、その長さでwasm側で動的に配列を確保しフィボナッチ数列を格納します。
その配列の先頭ポインタと配列長をJSのコールバック関数に渡します。
コールバック関数では、wasm側のメモリにアクセスしてコンソールに表示します。

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

 

2. C/C++でコールバック関数の定義のみ書く

C++で下記の2つの関数を書きます。

  • consoleLogIntArray(int * pointer, int size)
    • ポインタと配列長を受け取ってコンソールに出力するコールバック関数
  • fibonacciSeries(int n)

 
実際に実装すると下記のようになります。
fibonacci.cppというファイルに書き込みます。
 

extern "C" {
    
    extern void consoleLogIntArray(int * pointer, int length);

    void fibonacciSeries(int n) {
        int * fibonacci = new int[n];
        int a=1, b=1, tmp;
        for (int i=0; i<n; ++i) {
            fibonacci[i] = a;
            tmp = a + b;
            a = b;
            b = tmp;
        }
        consoleLogIntArray(fibonacci, n);
        delete[] fibonacci;
    }
}

 
コールバック関数の方は定義のみ書いておきます。
fibonacciSeries(int)のほうでは、コールバック関数を呼び出して配列が要らなくなってから解放しています。

 

3. JavaScriptでコールバック関数の実装を書く

先ほどのC++のコードだけではconsoleLogIntArray(int *, int)の実装がありません。
このまま実装を与えないままビルドして、fibonacciSeries(int)を実行すると下記のようなエラーが起きます。
 

xxx.js:194 missing function: xxx
failed to asynchronously prepare wasm: abort(-1) at Error
Uncaught (in promise) abort(-1) at Error

 
実装を与えるためにJavaScriptでconsoleLogIntArray(int *, int)を実装します。
callbacks.jsという名前で下記のソースを保存します。
 

var _consoleLogIntArray = function(pointer, size) {
  var position = pointer / 4;
  var int32Array = new Int32Array(size);
  int32Array.set(module.HEAP32.subarray(position, position + size));
  console.log(int32Array);
}

 
wasm側のメモリへのアクセスはemscriptenによってラップされています。
それがmodule.HEAP32です。これはwasm側のメモリ全部をInt32Arrayとして同期しています。
32bit(4byte)のint型配列なので、ポインタの数値を4で割ることHEAP32での先頭インデックスを求めています。

それと、関数名の先頭にアンダースコアをつけるのをお忘れなく。
単体のJSファイルにemscripten前提のコードを書くのはいかがなものか、というツッコミはなしでお願いします。

ちなみにHEAP32以外のメモリアクセスは下記の通りです。

用途 名前
char, boolean HEAP8
short HEAP16
int, *(あらゆる型のポインタ) HEAP32
float HEAPF32
double HEAPF64

 

4. emccで –pre-js オプションでビルドする

–pre-js callbacks.js をつけてビルドします。
 

emcc fibonacci.cpp \
    -s WASM=1 \
    -s "MODULARIZE=1" \
    -s "EXPORTED_FUNCTIONS=['_fibonacciSeries']" \
    --pre-js callbacks.js \
    -o fibonacci.js

 
このようにビルドすると fibonacci.wasm, fibonacci.js が自動生成されます。
fibonacci.jsを見てみると、最初の方にcallbacks.jsがそのままコピーされています。
 

var Module = function(Module) {
  Module = Module || {};
  var Module = Module;

var _consoleLogIntArray = function(pointer, size) {
  var position = pointer / 4;
  var int32Array = new Int32Array(size);
  int32Array.set(module.HEAP32.subarray(position, position + size));
  console.log(int32Array);
}
...

 
こうすることで、このModule関数を実行すると、wasm読み込みの際にコールバック関数の実装が流し込まれます。

 

5. HTMLから読み込んで実行する

index.html を下記のように書きます。

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

var module;

fetch('fibonacci.wasm')
  .then(response => response.arrayBuffer())
  .then(buffer => new Uint8Array(buffer))
  .then(binary => {
    var moduleArgs = {
      wasmBinary: binary,
      onRuntimeInitialized: function () {
        var fibonacciSeries = module.cwrap('fibonacciSeries', null, ['number']);
        fibonacciSeries(10);
      }
    };
    module = Module(moduleArgs);
  });

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

 
このHTMLについて少し説明します。
onRuntimeInitializedのタイミングで、cwrapで関数を取り出して実行しています。
引数は10なので、10項のフィボナッチ数列がコンソールに表示されるはずです。
http-serverを実行してブラウザで確認すると、下記の画像のようになるはずです。

 
f:id:shogonir:20170519005804p:plain

 

6. まとめ

wasm側からJSのコールバック関数を呼ぶ方法をサンプルで紹介しました。
配列のように複数の数値を渡すことができ、配列の領域の解放も同じスコープで行える利点があります。 ただ、ビルド時にJSの関数の実装を流し込む必要があるため、取り回しは悪いかもしれません。