web-dev-qa-db-ja.com

JavaScript配列を引数としてWebAssembly関数に渡します

複雑な配列計算を行うためにWebAssemblyをテストしたいと思います。

したがって、それぞれ3つの要素を含む2つのint配列を追加する単純なC++関数を作成しました。

_// hello.cpp
extern "C" {

void array_add(int * summed, int* a, int* b) {
  for (int i=0; i < 3; i++) {
    summed[i] = a[i] + b[i];
  }
}

}
_

そしてこれをコンパイルしました:

_emcc hello.cpp -s WASM=1 -s "MODULARIZE=1" -s "EXPORT_NAME='HELLO'" -s "BINARYEN_METHOD='native-wasm'" -s "EXPORTED_FUNCTIONS=['_array_add']" -o build/hello.js_

これにより、jsおよびwasmファイルが生成されます。これらを次のhtmlページでロードします。

_<!doctype html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <script type="text/javascript" src="build/hello.js"></script>
    <script type="text/javascript">
      function reqListener () {
        // Loading wasm module
        var arrayBuffer = oReq.response
        HELLO['wasmBinary'] = arrayBuffer
        hello = HELLO({ wasmBinary: HELLO.wasmBinary })

        // Calling function
        var result = new Int32Array(3)
        var a = new Int32Array([1, 2, 3])
        var b = new Int32Array([4, 5, 2])
        hello._array_add(result, a, b)
        console.log('result', result)
      }

      var oReq = new XMLHttpRequest();
      oReq.responseType = "arraybuffer";
      oReq.addEventListener("load", reqListener);
      oReq.open("GET", "build/hello.wasm");
      oReq.send();
    </script>
  </head>
  <body>

  </body>
</html>
_

しかし、どういうわけか、result配列は常に_[0, 0, 0]_です。

ccall()emscripten docs を参照)で関数を呼び出すなど、さまざまなことを試しましたが、wasmコンパイル済み関数の引数として配列を渡すことができないようです。

たとえば、次のC++関数の場合:

_extern "C" {

int first(int * arr) {
  return arr[0];
}

}
_

JavaScriptで呼び出されたときの結果は、引数として渡した配列からの期待値ではなく、ランダムな整数です。

何が欠けていますか?

[〜#〜] nb [〜#〜]:C++についてほとんど何も知らないので、これが私の初心者の質問である場合、すべての謝罪C++の無知...

18
sebpiq

あなたの質問は this one に非常に似ています:WebAssemblyは_i32_/_i64_/_f32_/_f64_ value types のみをサポートしています=ストレージ用の_i8_/_i16_と同様に。

つまり、ポインタを渡すことはできません。あなたがしていることは、あなたがC++の観点から来ているとき(無知について謝罪する必要はありません!)、まったく正気ですが、WebAssemblyの境界が機能する方法ではありません。 C++の専門家にとっても驚くべきことです。

文字列の質問と同様に、次のいずれかを行う必要があります。

  • エントリごとにエクスポートを呼び出すことで、配列を一度に1つずつコピーします(set(size_t index, int value)など)。
  • WebAssemblyインスタンスのヒープをArrayBufferとしてJavaScriptに公開し、必要な値をArrayBufferに直接書き込みます。

あなたは私が他の答えで提案したのと同じコードで後者を行うことができます:

_const bin = ...; // WebAssembly binary, I assume below that it imports a memory from module "imports", field "memory".
const module = new WebAssembly.Module(bin);
const memory = new WebAssembly.Memory({ initial: 2 }); // Size is in pages.
const instance = new WebAssembly.Instance(module, { imports: { memory: memory } });
const arrayBuffer = memory.buffer;
const buffer = new Uint8Array(arrayBuffer);
_

C++から来ると、「ポインタはどのように機能するのでしょうか?」上記では、WebAssembly↔JavaScriptではポインタを渡すことができないことを説明しました。 WebAssembly内部のポインターは、単純な_i32_値として表されます。 EmpscriptenはLLVMに依存してこれを実行します。WebAssemblyは、自身を4GiBの最大ヒープサイズを持つILP32として提示するため、機能します。

これは、間接的な関数呼び出しと関数ポインターに興味深い影響を及ぼします。私は別の質問にそれを残します;-)

ただし、これは、JavaScriptがWebAssemblyへのポインターについて「話す」ことができることを意味します。_i32_は_i32_です。値がヒープ内のどこかにあることがわかっている場合は、その_i32_をJavaScriptに渡すことができ、JavaScriptはそれを変更してWebAssemblyに戻すことができます。 JavaScriptがヒープのArrayBufferにアクセスできる場合、_i32_を使用すると、ヒープ内のどこにあるかを把握し、C++から行うようにヒープを変更できます。

WebAssemblyヒープは、ほとんどのC++ヒープとは異なりますが、実行可能ページへのアクセス権も、呼び出しスタック(または、ほとんどの呼び出しスタック)へのアクセス権もありません。LLVMなどのコンパイラーがアドレスを「こぼす」場合があります。 -WebAssemblyのローカルを使用する代わりに、ヒープに取得した値)。これは基本的にハーバードアーキテクチャが行うことです(フォンノイマンとは対照的です)。


では、hello._array_add(result, a, b)は何をしていますか? aを使用して配列からbおよびToIntegerを強制します。これは_0_になり、WebAssemblyでは有効なヒープの場所です!ヒープの予想外の部分にアクセスしています!

18
JF Bastien

他の同様の質問に感謝します:

emscriptenを使用して配列をC関数に渡す

emscriptenコンパイル済みコードへの配列ポインタの受け渡しを処理する方法

そしてAPIドキュメント:

https://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html#getValue

私はそれを理解しました。 wasm関数に配列を渡す方法/配列を取得する方法を例示するために、C++で単純な配列のコピーを実装しました。

#include <stdint.h>

extern "C" {

int* copy_array(int* in_array, int length) {
  int out_array[length];
  for (int i=0; i<length; i++) {
    out_array[i] = in_array[i];
  }
  return out_array;
}

}

次のようにコンパイルできます:

emcc wasm_dsp.cpp -s WASM=1 -s "MODULARIZE=1" -s "EXPORT_NAME='WasmDsp'" -s "BINARYEN_METHOD='native-wasm'" -s "EXPORTED_FUNCTIONS=['_copy_array']" -o build/wasm_dsp.js

そして、このようにブラウザで実行します:

<!doctype html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <script type="text/javascript" src="build/wasm_dsp.js"></script>
    <script type="text/javascript">
      function reqListener () {
        // Loading wasm module
        var arrayBuffer = oReq.response
        WasmDsp['wasmBinary'] = arrayBuffer
        wasmDsp = WasmDsp({ wasmBinary: WasmDsp.wasmBinary })

        var inArray = new Int32Array([22, 44, 66, 999])
        var nByte = 4
        copyArray = wasmDsp.cwrap('copy_array', null, ['number', 'number']);

        // Takes an Int32Array, copies it to the heap and returns a pointer
        function arrayToPtr(array) {
          var ptr = wasmDsp._malloc(array.length * nByte)
          wasmDsp.HEAP32.set(array, ptr / nByte)
          return ptr
        }

        // Takes a pointer and  array length, and returns a Int32Array from the heap
        function ptrToArray(ptr, length) {
          var array = new Int32Array(length)
          var pos = ptr / nByte
          array.set(wasmDsp.HEAP32.subarray(pos, pos + length))
          return array
        }

        var copiedArray = ptrToArray(
          copyArray(arrayToPtr(inArray), inArray.length)
        , inArray.length)

        console.log(copiedArray)
      }

      var oReq = new XMLHttpRequest();
      oReq.responseType = "arraybuffer";
      oReq.addEventListener("load", reqListener);
      oReq.open("GET", "build/wasm_dsp.wasm");
      oReq.send();
    </script>
  </head>
  <body>

  </body>
</html>

ここでarrayToPtrおよびptrToArray関数に注意してください...これらは、配列の受け渡し/戻りの作業を行うものです。

9
sebpiq