web-dev-qa-db-fra.com

Passer un tableau JavaScript en argument à une fonction WebAssembly

Je voudrais tester WebAssembly pour effectuer des calculs de tableau complexes.

J'ai donc écrit une simple fonction C++ ajoutant deux tableaux int contenant chacun 3 éléments:

// 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];
  }
}

}

Et compilé ceci avec:

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

Ce qui génère entre autres un fichier js et wasm. Je les charge avec la page html suivante:

<!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>

Mais d'une manière ou d'une autre, le tableau result est toujours [0, 0, 0].

J'ai essayé une variété de choses, y compris l'appel de la fonction avec ccall() (voir documentation emscripten ) et il semble que je ne puisse pas faire passer un tableau comme argument de ma fonction compilée par wasm.

Par exemple, avec la fonction C++ suivante:

extern "C" {

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

}

Le résultat lorsqu'il est appelé en JavaScript est un entier aléatoire, au lieu de la valeur attendue du tableau que j'ai passé en argument.

Qu'est-ce que je rate?

[~ # ~] nb [~ # ~] : Je ne connais pratiquement rien en C++, donc toutes mes excuses s'il s'agit d'une question de débutant liée à ma L'ignorance C++ ...

18
sebpiq

Votre question est très similaire à celle-ci : WebAssembly ne prend en charge que i32/i64/f32/f64types de valeurs ainsi que i8/i16 pour le stockage.

Cela signifie que vous ne pouvez pas passer de pointeurs. Ce que vous faites est totalement sain d'esprit lorsque vous venez d'un point de vue C++ (pas besoin de vous excuser pour l'ignorance!), Mais ce n'est tout simplement pas comment fonctionne la frontière de WebAssembly. Cela surprend aussi les experts C++.

Comme dans la question des chaînes, vous devez soit:

  • Copiez le tableau un par un en appelant une exportation une fois par entrée (telle que set(size_t index, int value)).
  • Exposez le tas de votre instance WebAssembly en tant que ArrayBuffer dans JavaScript et écrivez directement dans le ArrayBuffer les valeurs souhaitées.

Vous pouvez faire ce dernier avec le même code que j'ai proposé dans l'autre réponse:

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);

Venant de C++, vous vous demandez probablement: "mais comment fonctionnent les pointeurs?". Ci-dessus, j'explique que WebAssembly ↔ JavaScript, vous ne pouvez pas passer de pointeurs! Les pointeurs WebAssembly sont représentés sous la forme de simples valeurs i32. Empscripten s'appuie sur LLVM pour ce faire, et comme WebAssembly se présente comme ILP32 avec une taille de segment maximale de 4 Go, il fonctionne simplement.

Cela a des implications intéressantes pour les appels de fonction indirecte et les pointeurs de fonction! Je laisse ça pour une autre question ;-)

Cela signifie cependant que JavaScript peut "parler" de pointeurs vers WebAssembly: un i32 Est un i32. Si vous savez qu'une valeur se trouve quelque part dans le tas, vous pouvez transmettre ce i32 À JavaScript, et JavaScript peut le modifier et le renvoyer à WebAssembly. Si JavaScript a accès au ArrayBuffer du tas, avoir un i32 Vous permet de savoir où se trouvent les tas et de modifier le tas comme vous le feriez à partir de C++.

Cependant, le tas WebAssembly est différent de la plupart des tas C++: il n'a pas accès aux pages exécutables, ni à la pile d'appels (ou plutôt, la plupart de la pile d'appels: des compilateurs tels que LLVM peuvent "renverser" une adresse -prises de valeurs au tas au lieu d'utiliser les sections locales de WebAssembly). C'est essentiellement ce que font les architectures Harvard (par opposition à von Neumann).


Alors, que fait votre hello._array_add(result, a, b)? Contrainte a et b à partir de tableaux à l'aide de ToInteger. Cela devient 0, Qui dans WebAssembly est un emplacement de segment de mémoire valide! Vous accédez à une partie très inattendue de votre tas!

18
JF Bastien

Merci donc à d'autres questions similaires:

Passer le tableau à la fonction C avec emscripten

Comment gérer le passage/le retour de pointeurs de tableau vers le code compilé emscripten?

Et les documents API:

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

Je l'ai compris. Pour examiner comment passer un tableau à la fonction wasm/récupérer un tableau, j'ai implémenté une simple copie de tableau en 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;
}

}

Que vous pouvez compiler comme ceci:

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

Et exécutez dans le navigateur comme ceci:

<!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>

Notez les fonctions arrayToPtr et ptrToArray ici ... ce sont elles qui font le travail de passage/retour de tableau.

9
sebpiq