diff --git a/NOTICE b/NOTICE index a557707c9a..93193d2f4e 100644 --- a/NOTICE +++ b/NOTICE @@ -9,6 +9,7 @@ under the licensing terms detailed in LICENSE: * Norton Wang * Alan Pierce * Palmer +* Aron Homberg Portions of this software are derived from third-party works licensed under the following terms: diff --git a/lib/loader/index.d.ts b/lib/loader/index.d.ts index 6607683299..143356c3d2 100644 --- a/lib/loader/index.d.ts +++ b/lib/loader/index.d.ts @@ -10,6 +10,17 @@ interface ImportsObject { } } +export declare type JSWASMSafeInteropTypedArray = + Int8Array|Uint8Array|Uint8ClampedArray| + Int16Array|Uint16Array| + Int32Array|Uint32Array|Float32Array| + Float64Array; + +export interface TypedArrayRef { + ptr: number; + view: T; +} + /** Utility mixed in by the loader. */ interface ASUtil { /** An 8-bit signed integer view on the memory. */ @@ -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(length: number, type: T): TypedArrayRef; } /** Instantiates an AssemblyScript module using the specified imports. */ diff --git a/lib/loader/index.js b/lib/loader/index.js index 2f152393e6..d1eb966c7c 100644 --- a/lib/loader/index.js +++ b/lib/loader/index.js @@ -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) { + + 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; }, @@ -84,7 +131,8 @@ function instantiate(module, imports) { get F32() { checkMem(); return F32; }, get F64() { checkMem(); return F64; }, newString, - getString + getString, + newTypedArray }); } diff --git a/lib/loader/tests/assembly/index.ts b/lib/loader/tests/assembly/index.ts index 71ed4548b4..3f47219c57 100644 --- a/lib/loader/tests/assembly/index.ts +++ b/lib/loader/tests/assembly/index.ts @@ -44,3 +44,163 @@ export class Car { memory.free(changetype(this)); } } + +// TypedArray + +export function processInt8Array(ptr: usize, length: i32): void { + + const inputPtr: Pointer = new Pointer(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 = new Pointer(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 = new Pointer(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 = new Pointer(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 = new Pointer(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 = new Pointer(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 = new Pointer(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 = new Pointer(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 = new Pointer(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 { + + // FIXME: does not inline, always yields a trampoline + @inline constructor(offset: usize = 0) { + return changetype>(offset); + } + + @inline get offset(): usize { + return changetype(this); + } + + @inline get value(): T { + if (isReference()) { + return changetype(changetype(this)); + } else { + return load(changetype(this)); + } + } + + @inline set value(value: T) { + if (isReference()) { + if (isManaged()) ERROR("Unsafe unmanaged set of a managed object"); + if (value === null) { + memory.fill(changetype(this), 0, offsetof()); + } else { + memory.copy(changetype(this), changetype(value), offsetof()); + } + } else { + store(changetype(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): Pointer { + return changetype>(changetype(this) + changetype(other)); + } + + @inline @operator("-") sub(other: Pointer): Pointer { + return changetype>(changetype(this) - changetype(other)); + } + + @inline @operator.prefix("++") inc(): Pointer { + // 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() ? offsetof() : sizeof(); + return changetype>(changetype(this) + size); + } + + @inline @operator.prefix("--") dec(): Pointer { + const size = isReference() ? offsetof() : sizeof(); + return changetype>(changetype(this) - size); + } + + @inline @operator("[]") get(index: i32): T { + const size = isReference() ? offsetof() : sizeof(); + return load(changetype(this) + (index * size)); + } + + @inline @operator("[]=") set(index: i32, value: T): void { + const size = isReference() ? offsetof() : sizeof(); + store(changetype(this) + (index * size), value); + } +} \ No newline at end of file diff --git a/lib/loader/tests/build/untouched.wasm b/lib/loader/tests/build/untouched.wasm index 0c010359a5..90c2cff945 100644 Binary files a/lib/loader/tests/build/untouched.wasm and b/lib/loader/tests/build/untouched.wasm differ diff --git a/lib/loader/tests/index.js b/lib/loader/tests/index.js index 7aa0557249..f7718345eb 100644 --- a/lib/loader/tests/index.js +++ b/lib/loader/tests/index.js @@ -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"); @@ -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); \ No newline at end of file