WebAssembly:JavaScriptの配列をC/C++の関数に渡す
配列の要素の総和を計算する関数を例にとって、JavaScript の配列を C/C++ の関数に渡す方法について紹介します。 今回、WebAssembly にコンパイルするツールとして Emscripten SDK と WebAssembly Studio を使っていますが、ツールによって配列の渡し方が異なります。
注)Emscripten SDK は C++ に対応していますが、WebAssembly Studio は C++ に対応していません。
注)ブラウザは Chrome か Firefox をご利用ください。IE11 は WebAssembly に対応していません。
♦ Emscripten SDK を使った場合
Emscripten SDK で JavaScript の配列を C++ の関数に渡すには、JavaScript 側でヒープ領域を確保し、そこに配列をコピーし、確保したヒープ領域の先頭アドレスと配列の要素数を C++ の関数に渡します。
D:\emscripten\main.html D:\emscripten\main.js D:\emscripten\module.cpp D:\emscripten\module.js(module.cppのコンパイルで生成される) D:\emscripten\module.wasm(module.cppのコンパイルで生成される)
<!doctype html>
<html>
<body>
<div>
<input type="button" value="calculate" onclick="calculate('Output');">
<output id="Output"></output>
</div>
<script src="main.js"></script>
<script src="module.js"></script>
</body>
</html>
function calculate(output) {
const length = 10;
const float64Array = new Float64Array(length).fill(0);
for (const i in float64Array) {
float64Array[i] = 0.1 * i;
}
document.getElementById(output).innerHTML = sumFloat64Array(float64Array);
}
function sumFloat64Array(float64Array) {
let pointer = null;
try {
pointer = Module._malloc(Float64Array.BYTES_PER_ELEMENT * float64Array.length);
if (pointer == null) throw new Error("Memory allocation failed.");
Module.HEAPF64.set(float64Array, pointer / Float64Array.BYTES_PER_ELEMENT);
const sumDoubleArray = Module.cwrap("sumDoubleArray", "number", ["number", "number"]);
return sumDoubleArray(pointer, float64Array.length);
}
catch (error) {
console.error(error.message);
return NaN;
}
finally {
if (pointer != null) Module._free(pointer);
}
}
#include <emscripten.h>
extern "C" EMSCRIPTEN_KEEPALIVE double sumDoubleArray(double doubleArray[], int length) {
double sum = 0;
for (int i = 0; i < length; i++) {
sum += doubleArray[i];
}
return sum;
}
em++ -std=c++17 -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap']" -s EXPORTED_FUNCTIONS="['_malloc', '_free']" module.cpp -o module.js
注)コンパイラオプションを -s EXPORTED_FUNCTIONS="['_malloc', '_free', '_sumDoubleArray']" にすると、cwrap を使わずに Module._sumDoubleArray で関数を呼び出せるようになります。尚、関数名の前にアンダースコア _ を付けておかないとコンパイル時にエラーになるので注意してください。
♦ WebAssembly Studio を使った場合
WebAssembly Studio で JavaScript の配列を C の関数に渡すには、instance.exports.memory.buffer 内に配列をコピーする領域(下記のプログラムでは float64Buffer)を確保し、そこに配列をコピーし、この float64Buffer の先頭アドレス(offset)と配列の要素数を C の関数に渡します。
注)instance.exports.memory.buffer は、JavaScript 側の記憶領域ではなく、WebAssembly 側の記憶領域です。C/C++ で定義したグローバル変数はこの buffer に格納されます。
D:\webassembly\main.html D:\webassembly\main.js D:\webassembly\main.c D:\webassembly\main.wasm(main.cのコンパイルで生成される)
<!doctype html>
<html>
<body>
<div>
<input type="button" value="calculate" onclick="calculate('Output');">
<output id="Output"></output>
</div>
<script src="main.js"></script>
</body>
</html>
let exports;
fetch("main.wasm")
.then(response => response.arrayBuffer())
.then(bytes => WebAssembly.compile(bytes))
.then(module => WebAssembly.instantiate(module))
.then(instance => exports = instance.exports)
.catch(console.error);
function calculate(output) {
const length = 10;
const float64Array = new Float64Array(length).fill(0);
for (const i in float64Array) {
float64Array[i] = 0.1 * i;
}
document.getElementById(output).innerHTML = sumFloat64Array(float64Array);
}
function sumFloat64Array(float64Array) {
let pointer = null;
try {
const offset = 0; // Float64Arrayのoffsetなので、8の倍数を設定します。
const float64Buffer = new Float64Array(exports.memory.buffer, offset, float64Array.length);
for (const i in float64Array) {
float64Buffer[i] = float64Array[i];
}
return exports.sumDoubleArray(offset, float64Array.length);
}
catch (error) {
console.error(error.message);
return NaN;
}
}
注)exports をグローバル変数として外に出しています。(非同期処理でこういうことをするのは邪道かも知れませんが...)
今回のように catch(console.error) 以降が全て関数定義の場合は問題ないのですが、catch(console.error) 以降のステートメントから直接的、或いは間接的に exports を呼び出そうとすると、当然「exports が未定義です」というエラーが発生するので、上記構文の取扱いには充分注意してください。
注)今回は C 側でグローバル変数を定義していなかったので offset(配列の先頭アドレス)を 0 に設定しましたが、グローバル変数を定義している場合は、offset を 0 に設定して buffer にデータを書き込むとグローバル変数の領域を書き換える恐れがあるので、offset をグローバル変数がある領域のアドレスよりも大きなアドレスに設定してください。 こちらの記事 JavaScriptでC/C++コードを実行してネイティブアプリのように高速にする では、初期メモリのサイズを offset にとり、そのあとでメモリを1ページ分(65536バイト)増やしています。
注)型付き配列の offset は、型のバイト数の倍数を設定します。(Float32Array の場合は 4 の倍数、Float64Array の場合は 8 の倍数です)
#define WASM_EXPORT __attribute__((visibility("default")))
WASM_EXPORT double sumDoubleArray(double doubleArray[], int length) {
double sum = 0;
for (int i = 0; i < length; i++) {
sum += doubleArray[i];
}
return sum;
}
main.c のコンパイル手順は、WebAssembly Studioでコンパイルする手順 を参考にしてください。
♦ 結び
今回は JavaScript 側で配列を用意し、その配列を C/C++ の関数に渡すというやり方をしました。Emscripten SDK の方は、標準ライブラリにはない関数(cwrap、malloc、free)を使っているので、wasm ファイル以外に js ファイル(module.js)が別途必要になりますが、ヒープ領域を利用することができます。一方、WebAssembly Studio の方は標準ライブラリだけで wasm ファイルや関数を呼び出せるのですが、ヒープ領域を利用できないため WebAssembly 側の記憶領域の offset を手動で設定する必要があり面倒です。ということで双方に一長一短があります。
また今回とは違い、C/C++ 側で配列とアクセッサー(ゲッターとセッター)を用意し、JavaScript からアクセッサーを呼び出して配列を操作するという方法も考えられます。 特に C++ の場合は、std::vector などのコンテナとアルゴリズムを利用すれば配列を容易に操作することができるので、現状ではその方が簡単かなと思っています。
早く、DLL を呼び出すのと同じぐらい簡潔な記述で C/C++ の関数を呼び出せるようになればいいのですが...
♦ 補遺:ArrayBuffer と Typed Array
今、型付き配列(Typed Array)array を次のように定義すると、
const array = new Float64Array(buffer, offset, length);
buffer 内の offset の位置(配列の先頭アドレス)から 8 × length バイトの記憶領域に配列名 array でアクセスできるようになります。 array の要素への代入が、buffer への書き込みになります。new を使っていますが、新たに記憶領域を確保する訳ではありません。但し、次の場合は、新たに記憶領域を確保します。
const array = new Float64Array(length);
以下の例を参考にしてみてください。
const buffer = new ArrayBuffer(24); // 24バイトの記憶領域を新たに確保
const a = new Float64Array(buffer, 0, 3);
const b = new Float64Array(buffer, 8, 2);
a[0] = 0.0; // bufferへの書き込み
a[1] = 0.1; // bufferへの書き込み
a[2] = 0.2; // bufferへの書き込み
console.log(b[0]); // -> 0.1
console.log(b[1]); // -> 0.2
a[0] a[1] a[2]
├───────────────┼───────────────┼───────────────┤
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│ 0│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│ 9│10│11│12│13│14│15│16│17│18│19│20│21│22│23│ = buffer
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
├───────────────┼───────────────┤
b[0] b[1]
参考記事
- Webassembly Tutorial
- JS側で作成したTyped ArrayをWASM側に渡す
- JavaScriptでC/C++コードを実行してネイティブアプリのように高速にする
- Pragmatic compiling of C++ to WebAssembly
- WebAssembly:Pass arrays to C++
- Float64Array:ArrayBuffer