Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I pass an ArrayBuffer from JS to AssemblyScript/Wasm?

I have a pretty straightforward piece of Typescript code that parses a specific data format, the input is a UInt8Array. I've optimized it as far as I can, but I think this rather simple parser should be able to run faster than I can make it run as JS. I wanted to try out writing it in web assembly using AssemblyScript to make sure I'm not running into any quirks of the Javascript engines.

As I figured out now, I can't just pass a TypedArray to Wasm and have it work automatically. As far as I understand, I can pass a pointer to the array and should be able to access this directly from Wasm without copying the array. But I can't get this to work with AssemblyScript.

The following is a minimal example that shows how I'm failing to pass an ArrayBuffer to Wasm.

The code to set up the Wasm export is mostly from the automatically generated boilerplate:

const fs = require("fs");
const compiled = new WebAssembly.Module(
  fs.readFileSync(__dirname + "/build/optimized.wasm")
);
const imports = {
  env: {
    abort(msgPtr, filePtr, line, column) {
      throw new Error(`index.ts: abort at [${line}:${column}]`);
    }
  }
};
Object.defineProperty(module, "exports", {
  get: () => new WebAssembly.Instance(compiled, imports).exports
});

The following code invokes the WASM, index.js is the glue code above.

const m = require("./index.js");
const data = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
const result = m.parse(data.buffer);

And the AssemblyScript that is compiled to WASM is the following:

import "allocator/arena";

export function parse(offset: usize): number {
  return load<u8>(offset);
}

I get a "RuntimeError: memory access out of bounds" when I execute that code.

The major problem is that the errors I get back from Wasm are simply not helpful to figure this out on my own. I'm obviously missing some major aspects of how this actually works behind the scenes.

How do I actually pass a TypedArray or an ArrayBuffer from JS to Wasm using AssemblyScript?

like image 324
Mad Scientist Avatar asked Feb 17 '19 22:02

Mad Scientist


1 Answers

In AssemblyScript, there are many ways to read data from the memory. The quickest and fastest way to get this data is to use a linked function in your module's function imports to return a pointer to the data itself.

let myData = new Float64Array(100); // have some data in AssemblyScript

// We should specify the location of our linked function
@external("env", "sendFloat64Array")
declare function sendFloat64Array(pointer: usize, length: i32): void;

/**
 * The underlying array buffer has a special property called `data` which
 * points to the start of the memory.
 */
sendFloat64Data(myData.buffer.data, myData.length);

Then in JavaScript, we can use the Float64Array constructor inside our linked function to return the values directly.

/**
 * This is the fastest way to receive the data. Add a linked function like this.
 */
imports.env.sendFloat64Array = function sendFloat64Array(pointer, length) {
  var data = new Float64Array(wasmmodule.memory.buffer, pointer, length);
};

However, there is a much clearer way to obtain the data, and it involves returning a reference from AssemblyScript, and then using the AssemblyScript loader.

let myData = new Float64Array(100); // have some data in AssemblyScript

export function getData(): Float64Array {
  return myData;
}

Then in JavaScript, we can use the ASUtil loader provided by AssemblyScript.

import { instantiateStreaming } from "assemblyscript/lib/loader";

let wasm: ASUtil = await instantiateStreaming(fetch("myAssemblyScriptModule.wasm"), imports);

let dataReference: number = wasm.getData();
let data: Float64Array = wasm.getArray(Float64Array, dataReference);

I highly recommend using the second example for code clarity reasons, unless performance is absolutely critical.

Good luck with your AssemblyScript project!

like image 112
Joshua Tenner Avatar answered Oct 15 '22 22:10

Joshua Tenner