diff --git a/lib/demangle/README.md b/lib/demangle/README.md new file mode 100644 index 00000000..96672013 --- /dev/null +++ b/lib/demangle/README.md @@ -0,0 +1,39 @@ +![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. diff --git a/lib/demangle/index.js b/lib/demangle/index.js index 023e453a..05422085 100644 --- a/lib/demangle/index.js +++ b/lib/demangle/index.js @@ -1,18 +1,57 @@ -/** - * @file AssemblyScript demangler. - */ - module.exports = demangle; -/** - * Demangles module exports to a friendly object structure compatible with WebIDL and TypeScript - * definitions. - */ +/** Demangles AssemblyScript module exports to a friendly object structure. */ function demangle(exports) { var root = {}; - for (let i in exports) { - if (exports.hasOwnProperty(i)) { - // TODO + 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; diff --git a/lib/demangle/package.json b/lib/demangle/package.json new file mode 100644 index 00000000..445e8b77 --- /dev/null +++ b/lib/demangle/package.json @@ -0,0 +1,13 @@ +{ + "name": "@assemblyscript/demangle", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "node tests" + }, + "files": [ + "index.js", + "package.json", + "README.md" + ] +} diff --git a/lib/demangle/tests/index.js b/lib/demangle/tests/index.js new file mode 100644 index 00000000..4ba0733d --- /dev/null +++ b/lib/demangle/tests/index.js @@ -0,0 +1,45 @@ +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));