I would like to test WebAssembly for doing some complex array calculations.
So I've written a simple C++ function adding two int
arrays containing 3 elements each :
// 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];
}
}
}
And compiled this with :
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
Which generates among others, a js
and a wasm
file. I load these with the following html page :
<!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>
But somehow, the result
array is always [0, 0, 0]
.
I have tried a variety of things, including calling the function with ccall()
(see emscripten docs ) and it seems that I cannot get an array passed as argument of my wasm compiled function.
For example, with the following C++ function :
extern "C" {
int first(int * arr) {
return arr[0];
}
}
Result when called in JavaScript is a random-ish integer, instead of the expected value from the array I passed as argument.
What am I missing?
NB : I know pretty much nothing about C++, so all apologies if this is a beginner question related to my C++ ignorance ...
Your question is very similar to this one: WebAssembly only supports i32
/ i64
/ f32
/ f64
value types as well as i8
/ i16
for storage.
This means that you can't pass in pointers. What you're doing is totally sane when you're coming from a C++ point of view (no need to apologize for ignorance!), but it's just not how WebAssembly's boundary works. That surprising to C++ experts too.
As in the string question, you need to either:
set(size_t index, int value)
).ArrayBuffer
to JavaScript, and write directly into the ArrayBuffer
the values you want.You can do the latter with the same code I proposed in the other answer:
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);
Coming from C++ you're probably wondering: "but how do pointers work?". Above I explain that WebAssembly ↔ JavaScript you can't pass pointers around! Inside WebAssembly pointers are represented as simple i32
values. Empscripten relies on LLVM to do this, and since WebAssembly presents itself as ILP32 with a 4GiB maximum heap size it Just Works.
It does have interesting implications for indirect function calls and function pointers! I'll leave that for another question ;-)
This does however mean that JavaScript can "talk" about pointers to WebAssembly: an i32
is an i32
. If you know a value is somewhere in the heap then you can pass that i32
out to JavaScript, and JavaScript can modify it and pass it back in to WebAssembly. If JavaScript has access to the heap's ArrayBuffer
then having an i32
allows you to know where in the heap things are, and modify the heap as you would from C++.
The WebAssembly heap is different from most C++ heaps though: it doesn't have access to executable pages, nor does it have access to the call stack (or rather, most of the call stack: compilers such as LLVM may "spill" some address-taken values to the heap instead of using WebAssembly's locals). This is basically what Harvard architectures do (as opposed to von Neumann).
So what is your hello._array_add(result, a, b)
doing? Coercing a
and b
from arrays using ToInteger
. That becomes 0
, which in WebAssembly is a valid heap location! You're accessing a very unexpected part of your heap!
So thanks to other similar questions :
Pass array to C function with emscripten
How to handle passing/returning array pointers to emscripten compiled code?
And API docs :
https://kripken.github.io/emscripten-site/docs/api_reference/preamble.js.html#getValue
I have figured it out. To examplify how to pass an array to wasm function / get an array back, I have implemented a simple array copy in 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;
}
}
Which you can compile like this :
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
And run in the browser like this :
<!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>
Note the arrayToPtr
and ptrToArray
functions here ... they are the ones doing the work of passing / returning array.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With