Skip to content
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

Support for TypedArray interop between WASM and JS #268

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 <me@nortonwang.com>
* Alan Pierce <alangpierce@gmail.com>
* Palmer <pengliao@live.cn>
* Aron Homberg <info@aron-homberg.de>

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
50 changes: 49 additions & 1 deletion lib/loader/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,53 @@ 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/Typed_arrays
if (typedArrayCtor === Int8Array || typedArrayCtor === Uint8Array || typedArrayCtor === Uint8ClampedArray) {
Copy link
Member

@MaxGraey MaxGraey Sep 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use length * typedArrayCtor.BYTES_PER_ELEMENT. If BYTES_PER_ELEMENT doesn't exist in typedArrayCtor throw "Unsupported TypedArray constructor"


bytesToAllocate = length;

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

bytesToAllocate = length * 2;

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

bytesToAllocate = length * 4;

} else if (typedArrayCtor === Float64Array) {

bytesToAllocate = length * 8;

} 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)
view = new typedArrayCtor(mem, ptr, length);

return {

// address in module's heap memory
ptr: ptr,

// TypedArray instance reference
view: view
};
}

// Demangle exports and provide the usual utility on the prototype
return demangle(exports, {
get I8() { checkMem(); return I8; },
Expand All @@ -84,7 +131,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] / 2;
}
}

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

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

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

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

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

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

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

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

// 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.
78 changes: 78 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,79 @@ 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(2, Int8Array);
int8Array.view.set([-128, 127]);
assert(int8Array.view instanceof Int8Array);
module.processInt8Array(int8Array.ptr, int8Array.view.length);
assert.strictEqual(int8Array.view[0], -64);
assert.strictEqual(int8Array.view[1], 63);

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

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

// Int16Array
var int16Array = module.newTypedArray(2, Int16Array);
int16Array.view.set([-32768, 32767]);
assert(int16Array.view instanceof Int16Array);
module.processInt16Array(int16Array.ptr, int16Array.view.length);
assert.strictEqual(int16Array.view[0], -16384);
assert.strictEqual(int16Array.view[1], 16383);

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

// Float32Array
var float32Array = module.newTypedArray(2, Float32Array);
float32Array.view.set([1.2e-38, 3.4e38]);
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], 5.999999890533535e-39);
assert.strictEqual(float32Array.view[1], 1.6999999760721821e+38);

// Int32Array
var int32Array = module.newTypedArray(2, Int32Array);
int32Array.view.set([-2147483648, 2147483647]);
assert(int32Array.view instanceof Int32Array);
module.processInt32Array(int32Array.ptr, int32Array.view.length);
assert.strictEqual(int32Array.view[0], -1073741824);
assert.strictEqual(int32Array.view[1], 1073741823);

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

// Float64Array
var float64Array = module.newTypedArray(2, Float64Array);
float64Array.view.set([5.0e-324, 1.80e307]);
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], 0);
assert.strictEqual(float64Array.view[1], 9e+306);