From f02a71dd09732686691b41552167e6125136cfec Mon Sep 17 00:00:00 2001 From: Aron Homberg Date: Mon, 17 Sep 2018 21:56:38 +0200 Subject: [PATCH] Added support for memory based TypedArray interop between WASM and JS contexts --- NOTICE | 1 + lib/loader/index.d.ts | 13 +++ lib/loader/index.js | 50 +++++++- lib/loader/tests/assembly/index.ts | 160 ++++++++++++++++++++++++++ lib/loader/tests/build/untouched.wasm | Bin 4896 -> 7318 bytes lib/loader/tests/index.js | 78 +++++++++++++ 6 files changed, 301 insertions(+), 1 deletion(-) 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 0c010359a581826cc9dcfb027e3cca70e23a5efd..90c2cff945fe26c743138061cdb78fadf6be3b54 100644 GIT binary patch literal 7318 zcmeHMNpl;=74BsQ068ErB54U0CA$GoyGUp=MafBwj%3D;qhiXbRORB5U=p$kVj&i~ z%qm&PA(capsmd?NAtzVv`3Lz!xhmi5=^g+SFoa#q$>GxIep`Rf|{ zdGJCG1_NeBK6t?gW?(NEdm#q2z!*)xBbo1_`74^gV)F4L7=alsgp{5qd|#w6^?jD2 ze>0dxIy0Ql&8Iw(@+D_H^>BW`vfRn(TzFFLZamX}c>Z)}=kwpm^dhZ~dPmj9 z({{VtlbIX{rQLQ5x<|YHc9-QsqI^~BKW-g0jSzciNWD?g*0Ph@?-`+;L+l(HDhZBK z&k=}H8v>EJB?3{}ZM1u}h_DCX5OOZ?Y5hT)ozWYa+XkSG}^n>evM_E!pm;0#^$q@->o66 zESuq*^M5Tp`nMEfz+Wrzu~*3jiYfk>F?@)+<_FP2Xz?s!YF@W|E!>PGg_SgFGFD4D zH90Em#r%xnYixI*Wv3vqt*jSdM9G7KP(r`z6oek=H@u}d#!jQ)LB`t@79mWTh0u{H z#k5zqe4TMKLYTFZ24N;v%R7WwC#=JGwi3_sZbqgqjhf1jE>=btVYJYB0@bO%<(>Dv z^1lD2`tzR(zNp9wyj441MO5IR|4_m~a=^}j$p3d~UltHe2!@(IeM~Ikm$eeR(bWxtvT!=O4 z6rIqVmguZx0sG$s2(F8v>r2jc;e3DtA-Ntp*Hf_rboU@8Bph?zsYR}jYc9r`#`ST{ zsjiD3;X0h66KV6g?3|Jg9Uy1Osl3CTj-eT+z(n30GjM8=Q{$SsSkpK)t~u2yPEOJJ z2xmS|XL-H<-~{}duY*BL!0ehR*xrH}4;d6qWM(iY*P=k;0J`~lDS;4hMM6iJGs z;K2hQ1yxeIsDF|GrJ9vJkJFZlsIaN)ry0-UlD#0Xf z$EE~Bl|Ut!U2`RE2^Q5}_I{sE*GR&3CzG}AxcmpqKGq}ir0fXCNSiZ zF9zBVGv?LF)a8iJW9FgqFEacoZfbR-xX!#F3qwdZ??5Ivj|GTcg@Q{_15xwfLPW;P zp{0{S&_jmHs{*8R+33OV!Bl_}*jQ}TZ!;F;;*}D3rwFz<@PQV-Lv@VC89)l9;-7qs zni-L$|M0vP&G3RUheKweh0vU^jWkcU{ceKoi(0h8)p6S~s2{303Q9EU8wi7S7%L_4 zsxfrobD3_tm|(j^4zEqO?KSE*aV1&YSShiE^@lD|R}*Ysd%_s2 z{4KnmXr33W{v+N ze8g5?H}=0Qqj^>9wyKSC)YG94Gf!_>wo{e9YIwFy)$W$jy=qm<&!0!QpS%85CfZ=!8^@e0CL5ua*Zd^t#0W~s z%BDk>%};Q_a28hkwO+qhuJ(Gh=9i6QWMi-PZ8_?hE?%C3` zR2oiSI9&=3=dVoM+4g49#Esg(?>obS}<&DY5dBS%KHbOK~Kd3PfYkauPf1IW9lX56B@myGh|=7X_}-%sA&hy&$=8N`6{gVQr^zYf~; Vd;My+AAN;}4dQRrw%+(1w delta 270 zcmbPcxj>DZA+b1@k%57MQI&lnw~7R_qy&%yqMA8=RQ8F^Nn( znk6D8;G9^bT%1~Bl~7}@F-7+4$> z7?hd4SsWO+m01-T6&Mv5vXq&C$g#e-P?5==L6KR3NrBl>paiH!foZaUfQTWB0<#jU z0*exx0;>X>m$V|Q0;>aqBZCJ}0ni*UgMpiyTY*W1Re_P8+mT^%pumpFIwF?bH(6Qo h5_3}-FS1H(E)ZG9q{UiXl2}v%adJA6lf^MS0RWlhIEMfL 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