Chrome / Firefox / Edge / Safari いずれにおいても、ブラウザ上でプログラムを実行したいときにはJavaScript が使われる。
JavaScriptが登場した当初、その役割はHTMLに飾りつけをする程度だった。
今では本当に立派になったなぁー。
ずっと見守っていたから「推し活」、「擬似子育て」感を感じるわ。
2000年頃、文系に質問されたけど「JavaとJavaScriptの違い」を答えられなくてゴメンよ……。
別に「推し」だからといってJavaScriptを使いこなせる訳じゃない。
前回はボタンクリックなどをトリガにしてwasmファイルを呼び出した。
今度はJavaScriptからWASM(C++)を呼び出しの幾つかの実験を行なった。
wasmの関数をJSから呼び出す方法(1/2)
注意する点は次の通り……と書いてある。
- C++で実装する場合はextern “C”する
- ビルド時のEXPORTED_FUNCTIONSの時だけ関数名の先頭にアンダースコアをつける
- cwrapで関数を取り出すときはonRuntimeInitializedかmainが発火してから
1 2 3 4 5 6 7 8 9 10 |
int getValue(int a) { return a; } extern "C" { int add(int a, int b) { return getValue(a) + getValue(b); } } |
JS側は次のようになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>wasmtest</title> </head> <body> <script type="text/javascript" src="hello2.js"></script> <script type='text/javascript'> var module; fetch('hello2.wasm') .then(response => response.arrayBuffer()) .then(buffer => new Uint8Array(buffer)) .then(binary => { Module({ wasmBinary: binary, }).then(result =>{ module = result; var add = module.cwrap('add', 'number', ['number', 'number']); var point = add(2, 3); console.log(point); }); }); </script> </body> </html> |
ここで、wasm関数を取り出すにはmodue.cwrap()を実行する。
このcwrapの返り値はC/C++で実装したfunctionになっている。
分かりやすいサイトだな、と思いつつビルド&実行したら「Uncaught TypeError: module.cwrap is not a function」というエラーではまった……。
同じブログ見て同じようにエラーに出くわした人のブログも発見!
結論として、ビルドオプションに次のオプションの追記で解決。
ビルドオプション | 意味 |
---|---|
EXTRA_EXPORTED_RUNTIME_METHODS | ランタイムメソッドをExportする(JS側のModuleで使用するメソッドを列挙) |
MODULARIZE | JSファイル全体が関数となり、wasmの読み込みをより明示的に行える |
1 2 3 4 5 6 7 8 |
emsdk-main/upstream/emscripten/em++ -s EXPORTED_FUNCTIONS="['_add']" -s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap']" -g hello2.cpp -s WASM=1 -s "MODULARIZE=1" -o hello2.js |
ブラウザ上での実行結果は次の通り。
上手く動いた。
けど私には複雑でよく分からない……。
JavaScriptからC++を呼び出す(2/2)
もう少し簡素化できるっぽい。
コンパイラオプションに
1 |
-s EXPORTED_FUNCTIONS="['_関数名']" |
を追加すると、cwrap を使わずに 「Module._関数」 でWASM関数を呼び出せるようになるらしい。
C/C++を使う目的は複雑な高速処理がやりたいからなので、今度はポインタを使ってみよう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#include <emscripten.h> #include <string.h> #include <stdio.h> #define bZero(a, c, d) memset(a, 0, sizeof(c) * (d)) class Board { public: int board[8][8]; Board(){ bZero(board, board, 1); board[3][3] = board[4][4] = -1; board[3][4] = board[4][3] = 1; } } board; extern "C" { EMSCRIPTEN_KEEPALIVE int testFunction(int *arr, int size) { int row = size; int col = size; for (int i = 0; i < row; ++i) { for (int j = 0; j < col; ++j) { //copy the data from 2D array to 1D array arr[i * col + j] = board.board[i][j]; printf("%d, ", arr[i * col + j]); } printf("\n"); } printf("\n"); return 0; } } |
C/C++で定義した関数をJavaScript側で使用するには EMSCRIPTEN_KEEPALIVE をC/C++の関数名の前に追加する。
これによりEmscripten がコンパイル時に関数名を忘れない(関数名が生き続ける(keep-aliveする))。
また、JavaScript側(抜粋)では次のように呼び出す。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
var size = 8; try { // Create example data to test float_multiply_array var input_array = new Int32Array(size * size); // Get data byte size, allocate memory on Emscripten heap, and get pointer var nByte = input_array.BYTES_PER_ELEMENT; var length = input_array.length; var ptr = Module._malloc(length * nByte); if (ptr == null) throw new Error("Memory allocation failed."); Module.HEAP32.set(input_array, ptr / nByte); Module._testFunction(ptr, size); // Module.setValue と Module.getValue を使わないで、Int8Array を使う var output_array = new Int32Array(Module.HEAP32.buffer, ptr, length); console.log('input_array: ', input_array); console.log('output_array: ', output_array); Module._free(ptr); } catch (error) { console.error(error.message); } finally { if (ptr != null) Module._free(ptr); } |
WebAssemblyに渡すデータはWebAssemblyのヒープに割り当てる必要がある。
JavaScriptのデータを、Module._mallocで確保した領域に転送する。
- まず_mallocでメモリを用意する
- C/C++で定義した関数名の先頭にアンダースコアをつけたModule._testFunctionを呼び出す
- 最後にJavaScript側で値を取得する
なお、ポインターから数値型配列取得はHEAP8、HEAPU8、HEAP16、HEAPU16、HEAP32、HEAPU32、HEAPF32、HEAPF64から行える。
ビルドオは次の通り。
「-s “MODULARIZE=1″」は不要。
1 2 3 4 5 |
emsdk-main/upstream/emscripten/em++ -s EXPORTED_FUNCTIONS="['_malloc', '_free']" -g hello2.cpp -s WASM=1 -o hello2.js |
結果は次のとおり。
期待通りに動いてる。
fopenは使えるのか?
JavaScriptをブラウザから使用する場合、セキュリティによりローカルへのテキストファイルへのアクセスが制限されている。
WebAssemblyでfileにアクセスするには preload または embedded という手法を用いなければならないらしい。
preloadをすることで、ブラウザー内に仮想的なファイルシステムをつくりアクセス可能になる……と書いてあった。
C++側のコードは次のようになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// ボタンが押された extern "C" { EMSCRIPTEN_KEEPALIVE void clicked1() { puts("clicked1"); setElementInnerHTML("contents", "<p style='color:red'>Hello, world</p>"); std::string value = getElementValue("input1"); puts(value.c_str()); FILE *fp; /* ファイルポインタの宣言 */ char s[256]; char c; if ((fp = fopen("text.txt", "r")) == NULL) { /* ファイルのオープン */ printf("file open error!!\n"); } /* fetc()で取得 */ printf("Read by fetc() ----------------------------------------\n"); while((c = fgetc(fp)) != EOF){ putchar(c); } fclose(fp); } } |
そして空の「test.txt」を作成しておき、次のようなコマンドでビルドする。
1 2 3 4 5 6 7 |
emsdk-main/upstream/emscripten/em++ -s EXPORTED_FUNCTIONS="['_malloc']" -g hello2.cpp -s WASM=1 -o hello.js --preload-file test.txt |
ログには正しくファイルが読み込まれた結果が出力された。
でもファイルには何も書かれてない。
決められたファイルの中身を読み込む事はできるけど、書き込んだり動的にファイルを読み込んだり……は出来ないっぽい。
memory access out of bounds
再帰関数が呼び出されて数回目に
1 |
memory access out of bounds |
で死んだ……。
再帰関数野デバッグは面倒臭いので一気にテンション下がったが、Javascript側からC++側に非情に大きいデータの受け渡しで、このエラーが発生したというブログを見つけた。
とりあえずビルドオプションを追加して詳細を見てみる。
to make it easier to diagnose things like this, using -s ASSERTIONS=2
と書いてあった。
さっそく「-s ASSERTIONS=2」をつけてビルド。
1 2 3 4 5 6 7 8 |
emsdk-main/upstream/emscripten/em++ -s EXPORTED_FUNCTIONS="['_malloc', '_free']" -g hello2.cpp -s WASM=1 -o hello.js -s ASSERTIONS=2 |
そして実行。
Aborted(stack overflow (Attempt to set SP to 0xfffffe20, with stack limits [0x00000000 – 0x00010000]). If you require more stack space build with -sSTACK_SIZE=
)
スタックサイズが足りないからビルドオプションつけて増やせとの要求が表示された。
ビルドオプションに追加してみる。
1 2 3 4 5 6 7 8 |
emsdk-main/upstream/emscripten/em++ -s EXPORTED_FUNCTIONS="['_malloc', '_free']" -g hello2.cpp -s WASM=1 -s STACK_SIZE=512mb -s ASSERTIONS=2 -o hello2.js |
すると次のようなエラー。
em++: error: INITIAL_MEMORY must be larger than STACK_SIZE, was 16777216 (STACK_SIZE=536870912)
エラーに従いINITIAL_MEMORYもビルドオプションに追加する。
1 2 3 4 5 6 7 8 9 |
emsdk-main/upstream/emscripten/em++ -s EXPORTED_FUNCTIONS="['_malloc', '_free']" -g hello2.cpp -s WASM=1 -s INITIAL_MEMORY=700mb -s STACK_SIZE=512mb -s ASSERTIONS=2 -o hello2.js |
とりあえず実行エラーは消えた。
適切な数値はどうやって知れるのかな……。
おわりに
やりたかった事は実現出来そうだけど、色々とハマってよく分かって無い部分も多い。
同じような問題でハマる人もいると思うので備忘録として載せておく。