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

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;

18
lib/loader/package.json Normal file
View File

@ -0,0 +1,18 @@
{
"name": "@assemblyscript/loader",
"version": "1.0.0",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"test": "node tests"
},
"files": [
"index.d.ts",
"index.js",
"package.json",
"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);