最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Passing arrays and objects from JavaScript to c++ in Web Assembly - Stack Overflow

programmeradmin8浏览0评论

Ok, I've been pounding against this for a bit. I'm more of a JavaScript and C# focused dev, but I have some experience in c++. My problem is

  1. I need to find a simple way to take a Javascript object and pass it through WebAssembly to c++
  2. I need to do the same with Javascript arrays
  3. I probably need to do the same with Javascript arrays of Javascript objects

So starting with what I have tried on a simple array:

//c++
int testArr(int * arrIn, int length){
  int results = 0;
  for(int i = 0; i < length; i++){
    results += arrIn[i] + 1;
  }
  return results;
}


//javascript
let arr = [20, 50, 90, 62, 98];
console.log(wasmInstance.exports.testArr(arr, arr.length));

So that should take an array of integers, add them plus 1 (basically to test the loop). It returns 5. I expect it to return 325. So looking at typed arrays was the next logical step...

//c++
int testArr(int * arrIn, int length){
  int results = 0;
  for(int i = 0; i < length; i++){
    results += arrIn[i] + 1;
  }
  return results;
}


//javascript
let arr = [20, 50, 90, 62, 98];
let typedArray = new Int32Array(arr);

//test 1
console.log(wasmInstance.exports.testArr(typedArray, arr.length));

//test 2
console.log(wasmInstance.exports.testArr(typedArray.buffer, arr.length));

Test 1 returns, again, 5. Test 2 returns 0.

Now just to see if I can get c++ to return an array:

//c++
int * test(){
  int arr[] = {12, 32, 54, 31};
    return arr;
}

//Javascript
console.log(wasmInstance.exports.test());

Returns -16. Kind of funky and probably due to pointer issues between the two. Now if I try this:

//c++
int test(){
  int arr[] = {12, 32, 54, 31};
    return arr[0];
}

//Javascript
console.log(wasmInstance.exports.test());

Now it returns 12.

And so that is so far is as far as I have gotten on passing arrays, which for the most part does not seem possible. Now, passing objects. God help me. Please be kind on the c++ because its not my strongest language.

//c++
class Vector3{
  public:
    float x;
    float y;
    float z;
    
    Vector3(float X, float Y, float Z){
      x = X;
      y = Y;
      z = Z;
    }
};

int test(Vector3 position){
    return position.x;
}


//javascript
let position = {x: 21, y: 30, z: 12};
console.log(wasmInstance.exports.test(position));

This returns 0 instead of 21;

And now for the unholy trinity, an array of javascript objects...

//c++
class Vector3{
  public:
    float x;
    float y;
    float z;
    
    Vector3(float X, float Y, float Z){
      x = X;
      y = Y;
      z = Z;
    }
};

Vector3 test(Vector3 positions[], int length){
    return positions[0];
}


//javascript
let positions = [{x: 21, y: 30, z: 12},{x:15, y: 24, z: 14}]
console.log(wasmInstance.exports.test(positions, positions.length));

This returns undefined.

So the question is, am I messing up in c++, javascript, wasm, all 3, or what? I've spent 3 days scouring the internet looking for answers and the only thing I can find is declarations that this is possible with no examples or documentation to say HOW this can be done. The best documentation I've found is a DevRant, which still didn't give me an answer on this.

So is this possible and if so, are there any working examples I can follow OR is this not at all possible?

Ok, I've been pounding against this for a bit. I'm more of a JavaScript and C# focused dev, but I have some experience in c++. My problem is

  1. I need to find a simple way to take a Javascript object and pass it through WebAssembly to c++
  2. I need to do the same with Javascript arrays
  3. I probably need to do the same with Javascript arrays of Javascript objects

So starting with what I have tried on a simple array:

//c++
int testArr(int * arrIn, int length){
  int results = 0;
  for(int i = 0; i < length; i++){
    results += arrIn[i] + 1;
  }
  return results;
}


//javascript
let arr = [20, 50, 90, 62, 98];
console.log(wasmInstance.exports.testArr(arr, arr.length));

So that should take an array of integers, add them plus 1 (basically to test the loop). It returns 5. I expect it to return 325. So looking at typed arrays was the next logical step...

//c++
int testArr(int * arrIn, int length){
  int results = 0;
  for(int i = 0; i < length; i++){
    results += arrIn[i] + 1;
  }
  return results;
}


//javascript
let arr = [20, 50, 90, 62, 98];
let typedArray = new Int32Array(arr);

//test 1
console.log(wasmInstance.exports.testArr(typedArray, arr.length));

//test 2
console.log(wasmInstance.exports.testArr(typedArray.buffer, arr.length));

Test 1 returns, again, 5. Test 2 returns 0.

Now just to see if I can get c++ to return an array:

//c++
int * test(){
  int arr[] = {12, 32, 54, 31};
    return arr;
}

//Javascript
console.log(wasmInstance.exports.test());

Returns -16. Kind of funky and probably due to pointer issues between the two. Now if I try this:

//c++
int test(){
  int arr[] = {12, 32, 54, 31};
    return arr[0];
}

//Javascript
console.log(wasmInstance.exports.test());

Now it returns 12.

And so that is so far is as far as I have gotten on passing arrays, which for the most part does not seem possible. Now, passing objects. God help me. Please be kind on the c++ because its not my strongest language.

//c++
class Vector3{
  public:
    float x;
    float y;
    float z;
    
    Vector3(float X, float Y, float Z){
      x = X;
      y = Y;
      z = Z;
    }
};

int test(Vector3 position){
    return position.x;
}


//javascript
let position = {x: 21, y: 30, z: 12};
console.log(wasmInstance.exports.test(position));

This returns 0 instead of 21;

And now for the unholy trinity, an array of javascript objects...

//c++
class Vector3{
  public:
    float x;
    float y;
    float z;
    
    Vector3(float X, float Y, float Z){
      x = X;
      y = Y;
      z = Z;
    }
};

Vector3 test(Vector3 positions[], int length){
    return positions[0];
}


//javascript
let positions = [{x: 21, y: 30, z: 12},{x:15, y: 24, z: 14}]
console.log(wasmInstance.exports.test(positions, positions.length));

This returns undefined.

So the question is, am I messing up in c++, javascript, wasm, all 3, or what? I've spent 3 days scouring the internet looking for answers and the only thing I can find is declarations that this is possible with no examples or documentation to say HOW this can be done. The best documentation I've found is a DevRant, which still didn't give me an answer on this.

So is this possible and if so, are there any working examples I can follow OR is this not at all possible?

Share Improve this question asked Mar 30, 2022 at 17:18 David PardiniDavid Pardini 231 silver badge3 bronze badges 5
  • Some time ago I played around with emscripten. You can try out this example github./werto87/emscripten_webidl – Koronis Neilos Commented Mar 30, 2022 at 17:28
  • Its ok. I think the real answer is that it is not possible to pass arrays, objects, or arrays of objects from Javascript to c++ through Web Assembly or get them back the other way. The reason seems to be that it is not possible to share pointers between the two and there is no way around that. This really means webassembly is limited to simple strings, chars, int, double, float, etc. Anything beyond that is a pipe dream, at least that is how I'm going to look at it until I'm proven otherwise. Thank you for the tool. It didn't solve it, but it did help me narrow it down. – David Pardini Commented Mar 31, 2022 at 14:47
  • String possible but Array is not possible sounds fishy. I updated the example to use an array of c++ objects in java script. Note that its a quick implementation to show that arrays work. – Koronis Neilos Commented Mar 31, 2022 at 20:10
  • hat's not quite right. You can copy javascript arrays from into a std::array objects by using the emscripten::val object type which allows you to store any javascript object. Checkout documentation. There is a emscripten::val::vecFromJSArray function which returns std::vector<T> from JS array – Simanto Rahman Commented Apr 1, 2022 at 2:10
  • yes you can but you should use EMSCRiPTEN_BINDINGS as described in the Emscripten docs emscripten/docs/porting/connecting_cpp_and_javascript/… I will post an answer when i have a bit of time. – kalwalt Commented May 11, 2022 at 21:00
Add a ment  | 

2 Answers 2

Reset to default 6

For me the lightbulb moment came when I realized that passing data back and forth between c/c++ and JavaScript was best done with numbers; specifically, pointers to the data and information about the data (size on the heap). Coming from the JavaScript world and working with pointers/heap is a bit intimidating but I've got a couple examples that I think give a good start.

The snippet below shows a working example of passing an array of numbers from JavaScript to "c++" and "c++" returning the sum of the values back to JavaScript. I use quotes because the c++ code has been piled using emscripten to a file called /build/sum.wasm and everything is "glued" together using a file called /build/sum.js that can be seen included in the /index.html file.

const MAX_TRIES = 10;
let numTries = 0;
const moduleInterval = setInterval(() => {
  if (!Module) {
    numTries++;
  }

  if (numTries >= MAX_TRIES) {
    clearInterval(moduleInterval);
  }

  // Wait for "runtime initialization"
  // Module is defined in build/sum.js
  if (Module && Module.calledRun) {
    clearInterval(moduleInterval);

    const TYPES = {
      i8: { array: Int8Array, heap: "HEAP8" },
      i16: { array: Int16Array, heap: "HEAP16" },
      i32: { array: Int32Array, heap: "HEAP32" },
      f32: { array: Float32Array, heap: "HEAPF32" },
      f64: { array: Float64Array, heap: "HEAPF64" },
      u8: { array: Uint8Array, heap: "HEAPU8" },
      u16: { array: Uint16Array, heap: "HEAPU16" },
      u32: { array: Uint32Array, heap: "HEAPU32" }
    };

    function transferNumberArrayToHeap(array, type) {
      const typedArray = type.array.from(array);
      const heapPointer = Module._malloc(
        typedArray.length * typedArray.BYTES_PER_ELEMENT
      );

      Module[type.heap].set(typedArray, heapPointer >> 2);

      return heapPointer;
    }

    function containsFloatValue(array) {
      return array.some((value) => !Number.isInteger(value));
    }

    function sumArrayValues(array) {
      const hasFloatValue = containsFloatValue(array);
      let pointerToArrayOnHeap;
      try {
        pointerToArrayOnHeap = transferNumberArrayToHeap(
          array,
          hasFloatValue ? TYPES.f32 : TYPES.i32
        );
        return Moduleall(
          hasFloatValue ? "sumFloatArray" : "sumIntArray", // The name of C++ function
          "number", // The return type
          ["number", "number"], // The argument types
          [pointerToArrayOnHeap, array.length] // The arguments
        );
        // Note: The method can also be called directly
        // return Module[hasFloatValue ? '_sumFloatArray' : '_sumIntArray'](arrayOnHeap, array.length);
      } finally {
        Module._free(pointerToArrayOnHeap);
      }
    }

    const sumInt = sumArrayValues([20, 50, 90, 62, 98]);
    const sumFloat = sumArrayValues([20, 50, 90, 62, 98, 0.5]);

    const outputElement = document.querySelector("#output");
    outputElement.innerHTML = `Sum of integers: <strong>${sumInt}</strong>
<br>
Sum of floats: <strong>${sumFloat}</strong>`;
  }
}, 10);
<!DOCTYPE html>
<html lang="en-us">

<head>
  <meta charset="utf-8" />
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Sum</title>
</head>

<body>
  <div id="output"></div>
  <!-- /build/sum.js is a file built using emscripten -->
  <script src="https://w8r7ug.csb.app/build/sum.js"></script>
</body>

</html>

The full working code, original c++ file, and README with the pilation mand can be found here: https://codesandbox.io/s/cpp-javascript-webassembly-basic-example-w8r7ug/index.js.

In short:

  1. Compile the c++ code
em++ -o build/sum.js sum.cpp -s NO_EXIT_RUNTIME=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']" -s "EXPORTED_FUNCTIONS=['_free','_malloc']" -g
  1. In your JavaScript file transfer the data you want to send to the heap using something like this:
function transferNumberArrayToHeap(array, type) {
  const typedArray = type.array.from(array);
  const heapPointer = Module._malloc(
    typedArray.length * typedArray.BYTES_PER_ELEMENT
  );

  Module[type.heap].set(typedArray, heapPointer >> 2);

  return heapPointer;
}
  1. Also in your JavaScript file call the piled c++ function and pass the information about the data (pointer and size).
Moduleall(
    hasFloatValue ? "sumFloatArray" : "sumIntArray", // The name of C++ function
    "number", // The return type
    ["number", "number"], // The argument types
    [pointerToArrayOnHeap, array.length] // The arguments
);
// Or call the method  directly
Module[hasFloatValue ? '_sumFloatArray' : '_sumIntArray'](pointerToArrayOnHeap, array.length);

To answer your question, yes, it is possible to pass plex data from JavaScript to c/c++ and receive data back and above there is a working example of passing an array of numbers and receiving the sum back.




Now I want to provide an example of more plex data including arrays-of-arrays and objects. In the end following this next example makes it much easier to pass plex data back and forth and reason about the code. This is the way that I remend doing so even for a basic example like above.

Using msgpack makes it easy to "exchange data among multiple languages like JSON." Compiling is the same as before. Just add a little bit of information in your c++ about how the data is defined in msgpack encode and decode the data in your JavaScript file and you are good to go.

The full code and README can be found at https://codesandbox.io/s/cpp-javascript-webassembly-msgpack-example-wh8bwy?file=/index.js. The snippet below will also run the code.

const MAX_TRIES = 10;
let numTries = 0;
const moduleInterval = setInterval(() => {
  if (!Module) {
    numTries++;
  }

  if (numTries >= MAX_TRIES) {
    clearInterval(moduleInterval);
  }

  // Wait for "runtime initialization"
  // Module is defined in build/vector3.js
  if (Module && Module.calledRun && msgpack5) {
    clearInterval(moduleInterval);
    
    const msgpack = msgpack5();

    const encodedPositions = msgpack.encode([{
        positionId: "position1",
        x: 21,
        y: 30,
        z: 12
      },
      {
        positionId: "position2",
        x: 15,
        y: 24,
        z: 14
      }
    ]);
    let inputPointer = Module._malloc(encodedPositions.length);
    Module.HEAP8.set(
      encodedPositions,
      inputPointer / encodedPositions.BYTES_PER_ELEMENT
    );

    const outputPointer = Module._malloc(8);
    const processedVector3IdsPointer = Moduleall(
      "processVector3",
      "number", ["number", "number", "number"], [inputPointer, encodedPositions.length, outputPointer]
    );

    // OUTPUT
    let offset = Module.getValue(outputPointer, "i64");
    const processedVector3IdsData = new Uint8Array(
      Module.HEAPU8.subarray(
        processedVector3IdsPointer,
        processedVector3IdsPointer + offset
      )
    );

    const processedVector3Ids = msgpack.decode(processedVector3IdsData);
    console.log("Successfully Processed ids: ", processedVector3Ids);

    Module._free(inputPointer);
    Module._free(outputPointer);
    Module._free(processedVector3IdsPointer);
  }
}, 10);
<!DOCTYPE html>
<html lang="en-us">

<head>
  <meta charset="utf-8" />
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Vector3</title>
</head>

<body>
  <p>See console</p>
  <script src="https://cdnjs.cloudflare./ajax/libs/msgpack5/6.0.0/msgpack5.min.js"></script>
  <!-- /build/vector3.js is a file built using emscripten -->
  <script src="https://wh8bwy.csb.app/build/vector3.js"></script>
</body>

</html>

Here is vector3.cpp for quick reference.

#include <emscripten.h>
#include <msgpack.hpp>
#include <iostream>

struct Position {
  public:
    MSGPACK_DEFINE_MAP(positionId, x, y, z);
    std::string positionId;
    float x;
    float y;
    float z;
};

class Vector3 {
  public:
    float x;
    float y;
    float z;
    
    Vector3(float X, float Y, float Z){
      x = X;
      y = Y;
      z = Z;
    }
};

EMSCRIPTEN_KEEPALIVE
extern "C" char* processVector3(char* inputPointer, int inputSize, char* outputPointer) {
  msgpack::object_handle objectHandle = msgpack::unpack(inputPointer, inputSize);
  msgpack::object object = objectHandle.get();

  std::vector<Position> positions;
  object.convert(positions);

  std::vector<std::string> positionIds;
  for (auto& position : positions) {
    // CREATE VECTOR3 OBJECTS AND DO WORK
    try {
      std::cout << "Attempting to process " << position.positionId << ": " << position.x << " " << position.y << " " << position.z << std::endl;
      Vector3 vector3(position.x, position.y, position.z);
      positionIds.push_back(position.positionId);
      std::cout << "Successfully processed " << position.positionId << ": " << vector3.x << " " << vector3.y << " " << vector3.z << std::endl;
    } catch (std::exception& e) {
      std::cout << e.what() << std::endl;
    }
  }

  // OUTPUT
  msgpack::sbuffer sbuf;
  msgpack::pack(sbuf, positionIds);

  *outputPointer = sbuf.size();
  return sbuf.data();
}

huge thanks for the answer from coderfin,

for people that want a simple demo that showcases passing array between cpp and js, here it is:

example.cpp:

#include <iostream>
#include <emscripten/emscripten.h>
#include <vector>


extern "C" {

    EMSCRIPTEN_KEEPALIVE
    int sumJSArray(int* arr, int size) {
      int sum = 0;
      for (size_t i = 0; i < size; i++)
      {
        sum = sum + arr[i];
      }
      return sum;
    }

    EMSCRIPTEN_KEEPALIVE
    int* getCPPArray(int size) {
      std::vector<int> vec(size);
      // Populate the integer vec with some values
      for (int i = 0; i < size; ++i) {
          vec[i] = i; // Fill the array with values 1, 2, 3, ..., size
      }

      int* array = new int[vec.size()]; // Allocate memory for the array
      std::copy(vec.begin(), vec.end(), array); // Copy vector elements to the array
      return array;
    }


    EMSCRIPTEN_KEEPALIVE
    int* inOutArray(int* arr, int size) {
      std::vector<int> vec(size);
      // Populate the integer vec with some values
      for (int i = 0; i < size; ++i) {
          vec[i] = arr[i] + 10; // Fill the array with values 1, 2, 3, ..., size
      }

      int* array = new int[vec.size()]; // Allocate memory for the array
      std::copy(vec.begin(), vec.end(), array); // Copy vector elements to the array
      return array;
    }

}

testArrayInOut.js:

var factory = require('./example.js');

factory().then((wasmInstance) => {


  // testing getting array from cpp
  const arrLen = 5;
  const cppOutputArrPointer = wasmInstance._getCPPArray(arrLen); 
  var js_output_array = new Uint32Array(wasmInstance.HEAP32.buffer, cppOutputArrPointer, arrLen);
  console.log('returned i32 array from cpp:',js_output_array);


  // testing passing array to cpp
  const TYPES = {
    i8: { array: Int8Array, heap: "HEAP8" },
    i16: { array: Int16Array, heap: "HEAP16" },
    i32: { array: Int32Array, heap: "HEAP32" },
    f32: { array: Float32Array, heap: "HEAPF32" },
    f64: { array: Float64Array, heap: "HEAPF64" },
    u8: { array: Uint8Array, heap: "HEAPU8" },
    u16: { array: Uint16Array, heap: "HEAPU16" },
    u32: { array: Uint32Array, heap: "HEAPU32" }
  };

  const jsInputArr = [3,4];
  const type = TYPES.i32;
  const typedArray = type.array.from(jsInputArr);
  // Allocate memory for the integer array
  const heapPointer = wasmInstance._malloc(typedArray.length * typedArray.BYTES_PER_ELEMENT);
  wasmInstance[type.heap].set(typedArray, heapPointer >> 2);

  // Call the WebAssembly function with the integer array
  const sum = wasmInstance._sumJSArray(heapPointer, jsInputArr.length);
  console.log("result of sum of",jsInputArr,'=',sum);

  // Free the allocated memory
  wasmInstance._free(heapPointer);


  // testing In Out with arrays

  // Allocate memory for the integer array
  const heapPointer2 = wasmInstance._malloc(typedArray.length * typedArray.BYTES_PER_ELEMENT);
  wasmInstance[type.heap].set(typedArray, heapPointer2 >> 2);

  // Call the WebAssembly function with the integer array
  const cppOutputArrPointer2 = wasmInstance._inOutArray(heapPointer2, jsInputArr.length); 
  const js_output_array2 = new Uint32Array(wasmInstance.HEAP32.buffer, cppOutputArrPointer2,  jsInputArr.length);
  console.log('returned i32 array from cpp:',js_output_array2);
  // Free the allocated memory
  wasmInstance._free(heapPointer2);

});

to test:

  1. put example.cpp and testArrayInOut.js in the same directory
  2. generate wasm by running emcc example.cpp -o example.js -sMODULARIZE -s "EXPORTED_FUNCTIONS=['_free','_malloc']"
  3. now run node testArrayInOut.js and you should see the console log result:
returned i32 array from cpp: Uint32Array(5) [ 0, 1, 2, 3, 4 ]
result of sum of [ 3, 4 ] = 7
returned i32 array from cpp: Uint32Array(2) [ 13, 14 ]

I put together the above demo along with how to use boost/gsl and typescript in this repo: https://github./cyavictor88/wasm-cpp/

发布评论

评论列表(0)

  1. 暂无评论