Initial loader that unifies utils and demangle

This commit is contained in:
dcodeIO 2018-05-19 13:38:47 +02:00
parent d678807286
commit 98a0aa863d
13 changed files with 429 additions and 270 deletions

View File

@ -1,39 +0,0 @@
![AS](https://avatars1.githubusercontent.com/u/28916798?s=48) demangle
======================
Demangles AssemblyScript module exports to a friendly object structure compatible with WebIDL and TypeScript definitions.
Usage
-----
```js
var module = require("@assemblyscript/demangle")(myWasmModule.instance.exports);
// use module as defined in the .d.ts
```
Converting a memory offset (`this` value) to a class instance, i.e. where a class instance is returned by a WebAssembly function:
```js
var thisValue = wasmFunctionReturningAClassInstance();
var myClass = MyClass.wrap(thisValue);
```
Converting a class instance to a memory offset (`this` value), i.e. where calling a WebAssembly function that expects a class instance:
```js
var thisValue = myClass.this;
wasmFunctionExpectingAClassInstance(thisValue);
```
How does it work?
-----------------
AssemblyScript modules expose their exported elements by their internal name, which is a JSDoc-style fully qualified name indicating layers of nesting, and this module is able to recreate the original object structure from those names.
* A `.` seperates a parent from a static child
* A `#` separates a parent from an instance child
* The `get:` prefix indicates a getter
* The `set:` prefix indicates a setter
Note that the compiler generates implicit getters and setters for instance fields for convenience. Support for instance members is achieved by generating wrappers that prepend the `this` value (offset of the instance in memory returned by the constructor) as the first argument when calling an instance method, getter or setter.

View File

@ -1,58 +0,0 @@
module.exports = demangle;
/** Demangles AssemblyScript module exports to a friendly object structure. */
function demangle(exports) {
var root = {};
for (let internalName in exports) {
if (!exports.hasOwnProperty(internalName)) continue;
let elem = exports[internalName];
let parts = internalName.split(".");
let curr = root;
while (parts.length > 1) {
let part = parts.shift();
if (!curr.hasOwnProperty(part)) curr[part] = {};
curr = curr[part];
}
let name = parts[0];
let hash = name.indexOf("#");
if (hash >= 0) {
let className = name.substring(0, hash);
let classElem = curr[className];
if (typeof classElem === "undefined" || !classElem.prototype) {
let ctor = function(...args) {
return ctor.wrap(ctor.prototype.constructor(...args));
};
ctor.prototype = {};
ctor.wrap = function(thisValue) {
return Object.create(ctor.prototype, { "this": { value: thisValue, writable: false } });
};
if (classElem) Object.getOwnPropertyNames(classElem).forEach(name =>
Object.defineProperty(ctor, name, Object.getOwnPropertyDescriptor(classElem, name))
);
curr[className] = ctor;
}
name = name.substring(hash + 1);
curr = curr[className].prototype;
if (/^(get|set):/.test(name)) {
if (!curr.hasOwnProperty(name = name.substring(4))) {
let getter = exports[internalName.replace("set:", "get:")];
let setter = exports[internalName.replace("get:", "set:")];
Object.defineProperty(curr, name, {
get: function() { return getter(this.this); },
set: function(value) { setter(this.this, value); }
});
}
} else curr[name] = function(...args) { return elem(this.this, ...args); };
} else {
if (/^(get|set):/.test(name)) {
if (!curr.hasOwnProperty(name = name.substring(4))) {
Object.defineProperty(curr, name, {
get: exports[internalName.replace("set:", "get:")],
set: exports[internalName.replace("get:", "set:")]
});
}
} else curr[name] = elem;
}
}
return root;
}

View File

@ -1,45 +0,0 @@
var assert = require("assert");
var inspect = require("util").inspect;
var demangle = require("..");
var __this = 8;
var __usualDoors = 3;
var __doors = -1;
var exports = demangle({
"vroom": function() { console.log("vroom", arguments); },
"Car.MAX_DOORS": 5,
"Car.get:usualDoors": function() { console.log("Car#get:usualDoors", arguments); return __usualDoors; },
"Car.set:usualDoors": function(value) { console.log("Car#set:usualDoors", arguments); __usualDoors = value; },
"Car#constructor": function(this_, doors) { console.log("Car#constructor", arguments); __doors = doors; return __this; },
"Car#openDoors": function(this_) { console.log("Car#openDoors", arguments); return true; },
"Car#get:doors": function(this_) { console.log("Car#get:doors", arguments); return __doors; },
"Car#set:doors": function(this_, value) { console.log("Car#set:doors", arguments); __doors = value; }
});
console.log(inspect(exports, true));
exports.vroom(1, 2, 3);
var Car = exports.Car;
assert(Car.usualDoors == 3);
exports.Car.usualDoors = exports.Car.usualDoors + 2;
assert(Car.usualDoors == 5);
var car = new exports.Car(2);
assert(car.this == 8);
assert(car.openDoors() == true);
assert(car.doors == 2);
car.doors = car.doors + 2;
assert(car.doors == 4);
console.log(inspect(car, true));
var wrappedCar = exports.Car.wrap(16);
assert(wrappedCar.this == 16);
console.log(inspect(wrappedCar, true));

132
lib/loader/README.md Normal file
View File

@ -0,0 +1,132 @@
![AS](https://avatars1.githubusercontent.com/u/28916798?s=48) loader
======================
A convenient loader for AssemblyScript modules. Demangles module exports to a friendly object structure compatible with WebIDL and TypeScript definitions and provides some useful utility to read/write data from/to memory.
Usage
-----
```js
const loader = require("@assemblyscript/loader");
...
```
API
---
* **instantiate**<`T`>(module: `WebAssembly.Module`, imports?: `WasmImports`): `ASModule<T>`<br />
Instantiates an AssemblyScript module using the specified imports.
* **instantiateBuffer**<`T`>(buffer: `Uint8Array`, imports?: `WasmImports`): `ASModule<T>`<br />
Instantiates an AssemblyScript module from a buffer using the specified imports.
* **instantiateStreaming**<`T`>(response: `Response`, imports?: `WasmImports`): `Promise<ASModule<T>>`<br />
Instantiates an AssemblyScript module from a response using the sspecified imports.
* **demangle**<`T`>(exports: `WasmExports`): `T`<br />
Demangles an AssemblyScript module's exports to a friendly object structure. You usually don't have to call this manually as instantiation does this implicitly.
**Note:** `T` above can either be omitted if the structure of the module is unknown, or can reference a `.d.ts` as produced by the compiler with the `-d` option.
Instances of `ASModule` are automatically populated with some useful utility:
* **I8**: `Int8Array`<br />
An 8-bit signed integer view on the memory.
* **U8**: `Uint8Array`<br />
An 8-bit unsigned integer view on the memory.
* **I16**: `Int16Array`<br />
A 16-bit signed integer view on the memory.
* **U16**: `Uint16Array`<br />
A 16-bit unsigned integer view on the memory.
* **I32**: `Int32Array`<br />
A 32-bit signed integer view on the memory.
* **U32**: `Uint32Array`<br />
A 32-bit unsigned integer view on the memory.
* **F32**: `Float32Array`<br />
A 32-bit float view on the memory.
* **F64**: `Float64Array`<br />
A 64-bit float view on the memory.
* **newString**(str: `string`): `number`<br />
Allocates a new string in the module's memory and returns its pointer. Requires `allocate_memory` to be exported from your module's entry file, i.e.:
```js
import "allocator/tlsf";
export { allocate_memory, free_memory };
```
* **getString**(ptr: `number`): `string`<br />
Gets a string from the module's memory by its pointer.
Examples
--------
### Instantiating a module
```js
// From a module provided as a buffer, i.e. as returned by fs.readFileSync
const myModule = loader.instatiateBuffer(fs.readFileSync("myModule.wasm"), myImports);
// From a response object, i.e. as returned by window.fetch
const myModule = await loader.instantiateStreaming(fetch("myModule.wasm"), myImports);
```
### Reading/writing basic values to/from memory
```js
var ptrToInt8 = ...;
var value = myModule.U16[ptrToInt8]; // alignment of log2(1)=0
var ptrToInt16 = ...;
var value = myModule.U16[ptrToInt16 >>> 1]; // alignment of log2(2)=1
var ptrToInt32 = ...;
var value = myModule.U32[ptrToInt32 >>> 2]; // alignment of log2(4)=2
var ptrToFloat32 = ...;
var value = myModule.F32[ptrToFloat32 >>> 2]; // alignment of log2(4)=2
var ptrToFloat64 = ...;
var value = myModule.F64[ptrToFloat64 >>> 3]; // alignment of log2(8)=3
// Likewise, for writing
myModule.U16[ptrToInt8] = newValue;
myModule.U16[ptrToInt16 >>> 1] = newValue;
myModule.U32[ptrToInt32 >>> 2] = newValue;
myModule.F32[ptrToFloat32 >>> 2] = newValue;
myModule.F64[ptrToFloat64 >>> 3] = newValue;
```
**Note:** Make sure to reference the views as shown above as these will automatically be updated when the module's memory grows.
### Allocating/obtaining strings to/from memory
```js
// Allocating a string, i.e. to be passed to an export expecting one
var str = "Hello world!";
var ptr = module.newString(str);
// Disposing a string that is no longer needed (requires free_memory to be exported)
module.free_memory(ptr);
// Obtaining a string, i.e. as returned by an export
var ptrToString = ...;
var str = module.getString(ptrToString);
```
### Usage with TypeScript definitions produced by the compiler
```ts
import MyModule from "myModule"; // pointing at the d.ts
const myModule = loader.instatiateBuffer<MyModule>(fs.readFileSync("myModule.wasm"), myImports);
```
**Hint:** You can produce a `.d.ts` for your module with the `-d` option on the command line.

47
lib/loader/index.d.ts vendored Normal file
View File

@ -0,0 +1,47 @@
import "@types/webassembly-js-api";
/** WebAssembly imports with two levels of nesting. */
interface ImportsObject {
[key: string]: {},
env: {
memory?: WebAssembly.Memory,
table?: WebAssembly.Table,
abort?: (msg: number, file: number, line: number, column: number) => void
}
}
/** Utility mixed in by the loader. */
interface ASUtil {
/** An 8-bit signed integer view on the memory. */
readonly I8: Uint8Array,
/** An 8-bit unsigned integer view on the memory. */
readonly U8: Uint8Array,
/** A 16-bit signed integer view on the memory. */
readonly I16: Uint16Array,
/** A 16-bit unsigned integer view on the memory. */
readonly U16: Uint16Array,
/** A 32-bit signed integer view on the memory. */
readonly I32: Uint32Array,
/** A 32-bit unsigned integer view on the memory. */
readonly U32: Uint32Array,
/** A 32-bit float view on the memory. */
readonly F32: Float32Array,
/** A 64-bit float view on the memory. */
readonly F64: Float64Array,
/** Allocates a new string in the module's memory and returns its pointer. */
newString(str: string): number;
/** Gets a string from the module's memory by its pointer. */
getString(ptr: number): string;
}
/** Instantiates an AssemblyScript module using the specified imports. */
export declare function instantiate<T extends {}>(module: WebAssembly.Module, imports?: ImportsObject): ASUtil & T;
/** Instantiates an AssemblyScript module from a buffer using the specified imports. */
export declare function instantiateBuffer<T extends {}>(buffer: Uint8Array, imports?: ImportsObject): ASUtil & T;
/** Instantiates an AssemblyScript module from a response using the sspecified imports. */
export declare function instantiateStreaming<T extends {}>(response: Response, imports?: ImportsObject): Promise<ASUtil & T>;
/** Demangles an AssemblyScript module's exports to a friendly object structure. */
export declare function demangle<T extends {}>(exports: {}): T;

163
lib/loader/index.js Normal file
View File

@ -0,0 +1,163 @@
"use strict";
/** Instantiates an AssemblyScript module using the specified imports. */
function instantiate(module, imports) {
// Set up the imports object
if (!imports) imports = {};
if (!imports.env) imports.env = {};
if (!imports.env.abort) imports.env.abort = function(mesg, file, line, colm) {
mesg = mem ? getString(mesg) : "";
file = mem ? getString(file) : "<instantiate>";
throw Error("abort: " + mesg + " at " + file + ":" + line + ":" + colm);
}
// Instantiate the module and obtain its (flat) exports
var instance = new WebAssembly.Instance(module, imports);
var exports = instance.exports;
// Provide views for all sorts of basic values
var mem, I8, U8, I16, U16, I32, U32, F32, F64;
/** Updates memory views if memory has grown meanwhile. */
function checkMem() {
// see: https://github.com/WebAssembly/design/issues/1210
if (mem !== exports.memory.buffer) {
mem = exports.memory.buffer;
I8 = new Int8Array(mem);
U8 = new Uint8Array(mem);
I16 = new Int16Array(mem);
U16 = new Uint16Array(mem);
I32 = new Int32Array(mem);
U32 = new Uint32Array(mem);
F32 = new Float32Array(mem);
F64 = new Float64Array(mem);
}
}
checkMem();
/** Allocates a new string in the module's memory and returns its pointer. */
function newString(str) {
var dataLength = str.length;
var ptr = exports.allocate_memory(4 + (dataLength << 1));
var dataOffset = (4 + ptr) >>> 1;
checkMem();
U32[ptr >>> 2] = dataLength;
for (let i = 0; i < dataLength; ++i) U16[dataOffset + i] = str.charCodeAt(i);
return ptr;
}
/** Gets a string from the module's memory by its pointer. */
function getString(ptr) {
checkMem();
var dataLength = U32[ptr >>> 2];
var dataOffset = (ptr + 4) >>> 1;
var dataRemain = dataLength;
var parts = [];
const chunkSize = 1024;
while (dataRemain > chunkSize) {
let last = U16[dataOffset + chunkSize - 1];
let size = last >= 0xD800 && last < 0xDC00 ? chunkSize - 1 : chunkSize;
let part = U16.subarray(dataOffset, dataOffset += size);
parts.push(String.fromCharCode.apply(String, part));
dataRemain -= size;
}
return parts.join("") + String.fromCharCode.apply(String, U16.subarray(dataOffset, dataOffset + dataRemain));
}
// Demangle exports and provide the usual utility on the prototype
return demangle(exports, {
get I8() { checkMem(); return I8; },
get U8() { checkMem(); return U8; },
get I16() { checkMem(); return I16; },
get U16() { checkMem(); return U16; },
get I32() { checkMem(); return I32; },
get U32() { checkMem(); return U32; },
get F32() { checkMem(); return F32; },
get F64() { checkMem(); return F64; },
newString,
getString
});
}
exports.instantiate = instantiate;
/** Instantiates an AssemblyScript module from a buffer using the specified imports. */
function instantiateBuffer(buffer, imports) {
var module = new WebAssembly.Module(buffer);
return instantiate(module, imports);
}
exports.instantiateBuffer = instantiateBuffer;
/** Instantiates an AssemblyScript module from a response using the specified imports. */
async function instantiateStreaming(response, imports) {
var module = await WebAssembly.compileStreaming(response);
return instantiate(module, imports);
}
exports.instantiateStreaming = instantiateStreaming;
/** Demangles an AssemblyScript module's exports to a friendly object structure. */
function demangle(exports, baseModule) {
var module = baseModule ? Object.create(baseModule) : {};
function hasOwnProperty(elem, prop) {
return Object.prototype.hasOwnProperty.call(elem, prop);
}
for (let internalName in exports) {
if (!hasOwnProperty(exports, internalName)) continue;
let elem = exports[internalName];
let parts = internalName.split(".");
let curr = module;
while (parts.length > 1) {
let part = parts.shift();
if (!hasOwnProperty(curr, part)) curr[part] = {};
curr = curr[part];
}
let name = parts[0];
let hash = name.indexOf("#");
if (hash >= 0) {
let className = name.substring(0, hash);
let classElem = curr[className];
if (typeof classElem === "undefined" || !classElem.prototype) {
let ctor = function(...args) {
return ctor.wrap(ctor.prototype.constructor(...args));
};
ctor.prototype = {};
ctor.wrap = function(thisValue) {
return Object.create(ctor.prototype, { "this": { value: thisValue, writable: false } });
};
if (classElem) Object.getOwnPropertyNames(classElem).forEach(name =>
Object.defineProperty(ctor, name, Object.getOwnPropertyDescriptor(classElem, name))
);
curr[className] = ctor;
}
name = name.substring(hash + 1);
curr = curr[className].prototype;
if (/^(get|set):/.test(name)) {
if (!hasOwnProperty(curr, name = name.substring(4))) {
let getter = exports[internalName.replace("set:", "get:")];
let setter = exports[internalName.replace("get:", "set:")];
Object.defineProperty(curr, name, {
get: function() { return getter(this.this); },
set: function(value) { setter(this.this, value); },
enumerable: true
});
}
} else curr[name] = function(...args) { return elem(this.this, ...args); };
} else {
if (/^(get|set):/.test(name)) {
if (!hasOwnProperty(curr, name = name.substring(4))) {
Object.defineProperty(curr, name, {
get: exports[internalName.replace("set:", "get:")],
set: exports[internalName.replace("get:", "set:")],
enumerable: true
});
}
} else curr[name] = elem;
}
}
return module;
}
exports.demangle = demangle;

View File

@ -1,13 +1,18 @@
{ {
"name": "@assemblyscript/demangle", "name": "@assemblyscript/loader",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.js",
"types": "index.d.ts",
"scripts": { "scripts": {
"test": "node tests" "test": "node tests"
}, },
"files": [ "files": [
"index.d.ts",
"index.js", "index.js",
"package.json", "package.json",
"README.md" "README.md"
] ],
"dependencies": {
"@types/webassembly-js-api": "0.0.1"
}
} }

View File

@ -0,0 +1,46 @@
import "allocator/arena";
export const COLOR: string = "red";
export function strlen(str: string): i32 {
return str.length;
}
export namespace math {
export function add(a: i32, b: i32): i32 {
return a + b;
}
}
export class Car {
static readonly MAX_DOORS: i32 = 5;
static usualDoors: i32 = 3;
numDoors: i32;
private doorsOpen: bool = false;
get isDoorsOpen(): bool { return this.doorsOpen; }
set isDoorsOpen(value: bool) { this.doorsOpen = value; }
constructor(numDoors: i32) {
this.numDoors = numDoors;
}
openDoors(): bool {
if (this.doorsOpen) return false;
this.doorsOpen = true;
return true;
}
closeDoors(): bool {
if (!this.doorsOpen) return false;
this.doorsOpen = false;
return true;
}
dispose(): void {
free_memory(changetype<usize>(this));
}
}
export { allocate_memory, free_memory };

Binary file not shown.

34
lib/loader/tests/index.js Normal file
View File

@ -0,0 +1,34 @@
var fs = require("fs");
var assert = require("assert");
var inspect = require("util").inspect;
var loader = require("..");
var buffer = fs.readFileSync(__dirname + "/build/untouched.wasm");
var module = loader.instantiateBuffer(buffer, {});
console.log(inspect(module, true, 100, true));
// should inherit the usual utility
var proto = Object.getPrototypeOf(module);
assert(proto.I8 instanceof Int8Array);
assert(proto.U8 instanceof Uint8Array);
assert(proto.I16 instanceof Int16Array);
assert(proto.U16 instanceof Uint16Array);
assert(proto.I32 instanceof Int32Array);
assert(proto.U32 instanceof Uint32Array);
assert(proto.F32 instanceof Float32Array);
assert(proto.F64 instanceof Float64Array);
assert(typeof proto.newString === "function");
assert(typeof proto.getString === "function");
// should export memory
assert(module.memory instanceof WebAssembly.Memory);
// should be able to get an exported string
assert.strictEqual(module.getString(module.COLOR), "red");
// should be able to allocate and work with a new string
var str = "Hello world!𤭢";
var ptr = module.newString(str);
assert.strictEqual(module.getString(ptr), str);
assert.strictEqual(module.strlen(ptr), str.length);

View File

@ -1,17 +0,0 @@
![AS](https://avatars1.githubusercontent.com/u/28916798?s=48) utils
=================
Utilities for working with [AssemblyScript](http://assemblyscript.org) modules.
Usage
-----
```js
import utils from "@assemblyscript/utils";
var myModule = ...;
var helpers = utils(myModule);
var str = helpers.string(myModule.exportReturningAString());
```

View File

@ -1,103 +0,0 @@
function utils(module) {
var i8,
u8 = new Uint8Array(0),
i16,
u16,
i32,
u32,
f32,
f64;
function maybeUpdate() {
var mem = module.memory.buffer;
if (mem.byteLength === u8.length)
return;
i8 = Int8Array(mem);
u8 = Uint8Array(mem);
i16 = Int16Array(mem);
u16 = Uint16Array(mem);
i32 = Int32Array(mem);
u32 = Uint32Array(mem);
f32 = Float32Array(mem);
f64 = Float64Array(mem);
}
var helpers = {
i8: function(ptr) {
maybeUpdate();
return i8[ptr];
},
u8: function(ptr) {
maybeUpdate();
return u8[ptr];
},
i16: function(ptr) {
maybeUpdate();
return i16[ptr >>> 1];
},
u16: function(ptr) {
maybeUpdate();
return u16[ptr >>> 1];
},
i32: function(ptr) {
maybeUpdate();
return i32[ptr >>> 2];
},
u32: function(ptr) {
maybeUpdate();
return u32[ptr >>> 2];
},
i64: function(ptr) {
maybeUpdate();
return {
low: i32[ptr >>> 2],
high: i32[(ptr >>> 2) + 1]
};
},
u64: function(ptr) {
maybeUpdate();
return {
low: u32[ptr >>> 2],
high: u32[(ptr >>> 2) + 1]
};
},
bool: function(ptr) {
maybeUpdate();
return u8[ptr] === 1;
},
f32: function(ptr) {
maybeUpdate();
return f32[ptr >>> 2];
},
f64: function(ptr) {
maybeUpdate();
return f64[ptr >>> 3];
},
string: function(ptr) {
maybeUpdate();
var len = u32[ptr >>> 2];
var off = (ptr >>> 1) + 2;
return String.fromCharCode.apply(String, u16.subarray(off, off + len));
}
};
return helpers;
};
Object.defineProperties(module.exports = utils, {
__esModule: { value: true },
default: { value: utils }
});

View File

@ -1,6 +0,0 @@
{
"name": "@assemblyscript/utils",
"version": "0.5.0",
"description": "Utilities for working with AssemblyScript modules.",
"license": "Apache-2.0"
}