Skip to content

Added support for TypedArray interop between WASM and JS contexts #267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ under the licensing terms detailed in LICENSE:
* Norton Wang <[email protected]>
* Alan Pierce <[email protected]>
* Palmer <[email protected]>
* Aron Homberg <[email protected]>

Portions of this software are derived from third-party works licensed under
the following terms:
Expand Down
13 changes: 13 additions & 0 deletions lib/loader/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ interface ImportsObject {
}
}

export declare type JSWASMSafeInteropTypedArray =
Int8Array|Uint8Array|Uint8ClampedArray|
Int16Array|Uint16Array|
Int32Array|Uint32Array|Float32Array|
Float64Array;

export interface TypedArrayRef<T extends JSWASMSafeInteropTypedArray> {
ptr: number;
view: T;
}

/** Utility mixed in by the loader. */
interface ASUtil {
/** An 8-bit signed integer view on the memory. */
Expand All @@ -36,6 +47,8 @@ interface ASUtil {
newString(str: string): number;
/** Gets a string from the module's memory by its pointer. */
getString(ptr: number): string;
/** Allocates a new TypedArray in the module's memory and returns it's pointer and TypedArray object instance reference */
newTypedArray<T extends JSWASMSafeInteropTypedArray>(length: number, type: T): TypedArrayRef<T>;
}

/** Instantiates an AssemblyScript module using the specified imports. */
Expand Down
54 changes: 53 additions & 1 deletion lib/loader/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,57 @@ function instantiate(module, imports) {
return parts.join("") + String.fromCharCode.apply(String, U16.subarray(dataOffset, dataOffset + dataRemain));
}

/** Allocates a new TypedArray in the module's memory and returns it's pointer and TypedArray object instance reference */
function newTypedArray(length, typedArrayCtor) {

let bytesToAllocate = 0,
ptr,
view;

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
if (typedArrayCtor === Int8Array || typedArrayCtor === Uint8Array || typedArrayCtor === Uint8ClampedArray) {

bytesToAllocate = 1 + (length << 1);

} else if (typedArrayCtor === Int16Array || typedArrayCtor === Uint16Array) {

bytesToAllocate = 2 + (length << 1);

} else if (typedArrayCtor === Float32Array || typedArrayCtor === Int32Array || typedArrayCtor === Uint32Array) {

bytesToAllocate = 4 + (length << 1);

} else if (typedArrayCtor === Float64Array) {

bytesToAllocate = 8 + (length << 1);

} else {

throw new Error("Unsupported TypedArray constructor. Please refer to a supported TypedArray subclass.");
}

// allocate memory in WASM heap
ptr = exports["memory.allocate"](bytesToAllocate);

checkMem();

// construct an instance of TypedArray which points to the module's memory (WASM module owns the data)
// thus, the memory of the WASM module is "viewed" from JS context and data can be exchanged
// bi-directional just by:
// - using the TypedArray subclass instance API (e.g. Float32Array, Int8Array) on JS side
// - read/write from memory in WASM module using new Pointer<T>(ptr) on WASM side (e.g. new Pointer<f32>(ptr))
view = new typedArrayCtor(mem, ptr, length);

return {

// address in module's heap memory (AS type and usage hint: usize/Pointer<T>)
ptr: ptr,

// TypedArray object instance reference (JS type: TypedArray)
view: view
};
}

// Demangle exports and provide the usual utility on the prototype
return demangle(exports, {
get I8() { checkMem(); return I8; },
Expand All @@ -84,7 +135,8 @@ function instantiate(module, imports) {
get F32() { checkMem(); return F32; },
get F64() { checkMem(); return F64; },
newString,
getString
getString,
newTypedArray
});
}

Expand Down
160 changes: 160 additions & 0 deletions lib/loader/tests/assembly/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,163 @@ export class Car {
memory.free(changetype<usize>(this));
}
}

// TypedArray

export function processInt8Array(ptr: usize, length: i32): void {

const inputPtr: Pointer<i8> = new Pointer<i8>(ptr);

for (let i: i32 = 0; i < length; i++) {
// mutate the values directly in memory
inputPtr[i] = inputPtr[i] - 1;
}
}

export function processUint8Array(ptr: usize, length: i32): void {

const inputPtr: Pointer<u8> = new Pointer<u8>(ptr);

for (let i: i32 = 0; i < length; i++) {
// mutate the values directly in memory
inputPtr[i] = inputPtr[i] - 1;
}
}

export function processUint8ClampedArray(ptr: usize, length: i32): void {

const inputPtr: Pointer<u8> = new Pointer<u8>(ptr);

for (let i: i32 = 0; i < length; i++) {
// mutate the values directly in memory
inputPtr[i] = inputPtr[i] - 1;
}
}

export function processInt16Array(ptr: usize, length: i32): void {

const inputPtr: Pointer<i16> = new Pointer<i16>(ptr);

for (let i: i32 = 0; i < length; i++) {
// mutate the values directly in memory
inputPtr[i] = inputPtr[i] - 1;
}
}

export function processUint16Array(ptr: usize, length: i32): void {

const inputPtr: Pointer<u16> = new Pointer<u16>(ptr);

for (let i: i32 = 0; i < length; i++) {
// mutate the values directly in memory
inputPtr[i] = inputPtr[i] - 1;
}
}

export function processFloat32Array(ptr: usize, length: i32): void {

const inputPtr: Pointer<f32> = new Pointer<f32>(ptr);

for (let i: i32 = 0; i < length; i++) {
// mutate the values directly in memory
inputPtr[i] = inputPtr[i] - 1;
}
}

export function processInt32Array(ptr: usize, length: i32): void {

const inputPtr: Pointer<i32> = new Pointer<i32>(ptr);

for (let i: i32 = 0; i < length; i++) {
// mutate the values directly in memory
inputPtr[i] = inputPtr[i] - 1;
}
}

export function processUint32Array(ptr: usize, length: i32): void {

const inputPtr: Pointer<u32> = new Pointer<u32>(ptr);

for (let i: i32 = 0; i < length; i++) {
// mutate the values directly in memory
inputPtr[i] = inputPtr[i] - 1;
}
}

export function processFloat64Array(ptr: usize, length: i32): void {

const inputPtr: Pointer<f64> = new Pointer<f64>(ptr);

for (let i: i32 = 0; i < length; i++) {
// mutate the values directly in memory
inputPtr[i] = inputPtr[i] - 1;
}
}

// A pointer arithmetic experiment
class Pointer<T> {

// FIXME: does not inline, always yields a trampoline
@inline constructor(offset: usize = 0) {
return changetype<Pointer<T>>(offset);
}

@inline get offset(): usize {
return changetype<usize>(this);
}

@inline get value(): T {
if (isReference<T>()) {
return changetype<T>(changetype<usize>(this));
} else {
return load<T>(changetype<usize>(this));
}
}

@inline set value(value: T) {
if (isReference<T>()) {
if (isManaged<T>()) ERROR("Unsafe unmanaged set of a managed object");
if (value === null) {
memory.fill(changetype<usize>(this), 0, offsetof<T>());
} else {
memory.copy(changetype<usize>(this), changetype<usize>(value), offsetof<T>());
}
} else {
store<T>(changetype<usize>(this), value);
}
}

// FIXME: in general, inlining any of the following always yields a block. one could argue that
// this helps debuggability, or that it is unnecessary overhead due to the simplicity of the
// functions. a compromise could be to inline a block consisting of a single 'return' as is,
// where possible.
@inline @operator("+") add(other: Pointer<T>): Pointer<T> {
return changetype<Pointer<T>>(changetype<usize>(this) + changetype<usize>(other));
}

@inline @operator("-") sub(other: Pointer<T>): Pointer<T> {
return changetype<Pointer<T>>(changetype<usize>(this) - changetype<usize>(other));
}

@inline @operator.prefix("++") inc(): Pointer<T> {
// FIXME: this should take alignment into account, but then would require a new builtin to
// determine the minimal alignment of a struct by evaluating its field layout.
const size = isReference<T>() ? offsetof<T>() : sizeof<T>();
return changetype<Pointer<T>>(changetype<usize>(this) + size);
}

@inline @operator.prefix("--") dec(): Pointer<T> {
const size = isReference<T>() ? offsetof<T>() : sizeof<T>();
return changetype<Pointer<T>>(changetype<usize>(this) - size);
}

@inline @operator("[]") get(index: i32): T {
const size = isReference<T>() ? offsetof<T>() : sizeof<T>();
return load<T>(changetype<usize>(this) + (<usize>index * size));
}

@inline @operator("[]=") set(index: i32, value: T): void {
const size = isReference<T>() ? offsetof<T>() : sizeof<T>();
store<T>(changetype<usize>(this) + (<usize>index * size), value);
}
}
Binary file modified lib/loader/tests/build/untouched.wasm
Binary file not shown.
69 changes: 69 additions & 0 deletions lib/loader/tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ assert(proto.F32 instanceof Float32Array);
assert(proto.F64 instanceof Float64Array);
assert(typeof proto.newString === "function");
assert(typeof proto.getString === "function");
assert(typeof proto.newTypedArray === "function");

// should export memory
assert(module.memory instanceof WebAssembly.Memory);
assert(typeof module.memory.free === "function");
assert(typeof module.memory.allocate === "function");

// should be able to get an exported string
assert.strictEqual(module.getString(module.COLOR), "red");
Expand All @@ -33,3 +35,70 @@ var str = "Hello world!𤭢";
var ptr = module.newString(str);
assert.strictEqual(module.getString(ptr), str);
assert.strictEqual(module.strlen(ptr), str.length);

// should be able to allocate and work with typed arrays

// Int8Array
var int8Array = module.newTypedArray(10, Int8Array);
int8Array.view.set([-2, 4, 6, 8]);
assert(int8Array.view instanceof Int8Array);
module.processInt8Array(int8Array.ptr, int8Array.view.length);
assert.strictEqual(int8Array.view[0], -3);

// Uint8Array
var uint8Array = module.newTypedArray(10, Uint8Array);
uint8Array.view.set([2, 4, 6, 8]);
assert(uint8Array.view instanceof Uint8Array);
module.processUint8Array(uint8Array.ptr, uint8Array.view.length);
assert.strictEqual(uint8Array.view[0], 1);

// Uint8ClampedArray
var uint8ClampedArray = module.newTypedArray(10, Uint8ClampedArray);
uint8ClampedArray.view.set([2, 4, 6, 8]);
assert(uint8ClampedArray.view instanceof Uint8ClampedArray);
module.processUint8ClampedArray(uint8ClampedArray.ptr, uint8ClampedArray.view.length);
assert.strictEqual(uint8ClampedArray.view[0], 1);

// Int16Array
var int16Array = module.newTypedArray(10, Int16Array);
int16Array.view.set([2, 4, 6, 8]);
assert(int16Array.view instanceof Int16Array);
module.processInt16Array(int16Array.ptr, int16Array.view.length);
assert.strictEqual(int16Array.view[0], 1);

// Uint16Array
var uint16Array = module.newTypedArray(10, Uint16Array);
uint16Array.view.set([2, 4, 6, 8]);
assert(uint16Array.view instanceof Uint16Array);
module.processUint16Array(uint16Array.ptr, uint16Array.view.length);
assert.strictEqual(uint16Array.view[0], 1);

// Float32Array
var float32Array = module.newTypedArray(10, Float32Array);
float32Array.view.set([2.22, 4.44, 6.66, 8.88]);
assert(float32Array.view instanceof Float32Array);
module.processFloat32Array(float32Array.ptr, float32Array.view.length);
// beware of floating point arithmetic... -> see f64 below
assert.strictEqual(float32Array.view[0], 1.2200000286102295);

// Int32Array
var int32Array = module.newTypedArray(10, Int32Array);
int32Array.view.set([2, 4, 6, 8]);
assert(int32Array.view instanceof Int32Array);
module.processInt32Array(int32Array.ptr, int32Array.view.length);
assert.strictEqual(int32Array.view[0], 1);

// Uint32Array
var uint32Array = module.newTypedArray(10, Uint32Array);
uint32Array.view.set([2, 4, 6, 8]);
assert(uint32Array.view instanceof Uint32Array);
module.processUint32Array(uint32Array.ptr, uint32Array.view.length);
assert.strictEqual(uint32Array.view[0], 1);

// Float64Array
var float64Array = module.newTypedArray(10, Float64Array);
float64Array.view.set([2.22, 4.44, 6.66, 8.88]);
assert(float64Array.view instanceof Float64Array);
module.processFloat64Array(float64Array.ptr, float64Array.view.length);
// beware of floating point arithmetic... -> as expected much higher precision than f32
assert.strictEqual(float64Array.view[0], 1.2200000000000002);