diff --git a/examples/loader/README.md b/examples/loader/README.md new file mode 100644 index 0000000000..5872f82b58 --- /dev/null +++ b/examples/loader/README.md @@ -0,0 +1,38 @@ +Loader Basics +============= + +An [AssemblyScript](http://assemblyscript.org) example. Utilizes the [loader](https://docs.assemblyscript.org/basics/loader) to perform various common tasks on the WebAssembly/JavaScript boundary, like passing along strings and arrays. + +Instructions +------------ + +Install the dependencies, build the WebAssembly module and verify that everything works: + +``` +$> npm install +$> npm run asbuild +$> npm test +``` + +The example consists of several files showing the different perspectives, in recommended reading order: + +* [assembly/index.ts](./assembly/index.ts)
+ The AssemblyScript sources we are going to compile to a WebAssembly module. + Contains the implementations we are going to call from JavaScript. + +* [tests/index.js](./tests/index.js)
+ A test loading our WebAssembly node module that will utilize the loader to + pass strings and arrays between WebAssembly and JavaScript. + +* [index.js](./index.js)
+ Instantiates the WebAssembly module and exposes it as a node module. Also + provides the imported functions used in Example 3. + +* [assembly/myConsole.ts](./assembly/myConsole.ts)
+ The import declarations of our custom console API used in Example 3. + +To rerun the tests: + +``` +$> npm test +``` diff --git a/examples/loader/assembly/index.ts b/examples/loader/assembly/index.ts new file mode 100644 index 0000000000..946e2243b8 --- /dev/null +++ b/examples/loader/assembly/index.ts @@ -0,0 +1,127 @@ +// Example 1: Passing a string from WebAssembly to JavaScript. + +// Under the hood, the following yields a WebAssembly function export returning +// the pointer to a string within the module's memory. To obtain its contents, +// we are going to read it from memory on the JavaScript side. + +// see: tests/index.js "Test for Example 1" + +export function getHello(): string { + return "Hello world (I am a WebAssembly string)"; +} + +// Example 2: Passing a string from JavaScript to WebAssembly. + +// Similarly, we'll call the following function with a pointer to a string in +// the module's memory from JavaScript. To do so, the string will first be +// allocated on the JavaScript side, while holding on to a reference to it. + +// see: tests/index.js "Test for Example 2" + +export function sayHello(s: string): void { + console.log(" " + s); // see Example 3 +} + +// Example 3: Calling a JavaScript import with a WebAssembly string. + +// see: assembly/myConsole.ts + +import * as console from "./myConsole"; + +// Example 4: Passing an array from WebAssembly to JavaScript. + +// Analogous to the examples above working with strings, the following function +// will return a pointer to an array within the module's memory. We can either +// get a live view on it to modify, or obtain a copy. + +// see: tests/index.js "Test for Example 4" + +export function getMyArray(size: i32): Int32Array { + var arr = new Int32Array(size); + for (let i = 0; i < size; ++i) { + arr[i] = i; + } + return arr; +} + +// Example 5: Passing an array from JavaScript to WebAssembly. + +// Likewise, we can also allocate an array on the JavaScript side and pass it +// its pointer to WebAssembly, then doing something with it. + +// see: tests/index.js "Test for Example 5" + +export function computeSum(a: Int32Array): i32 { + console.time("sum"); // see Example 3 + var sum = 0; + for (let i = 0, k = a.length; i < k; ++i) { + sum += a[i]; + } + console.timeEnd("sum"); // see Example 3 + return sum; +} + +// See the comments in test/index.js "Test for Example 5" for why this is +// necessary, and how to perform an Int32Array allocation using its runtime id. +export const Int32Array_ID = idof(); + +// Example 6: WebAssembly arrays of WebAssembly strings. + +// Let's get a little more serious with a combined example. We'd like to pass an +// array of strings from JavaScript to WebAssembly, create a new array with all +// strings converted to upper case, return it to JavaScript and print its contents. + +// see: tests/index.js "Test for Example 6" + +export function capitalize(a: string[]): string[] { + var length = a.length; + var b = new Array(length); + for (let i = 0; i < length; ++i) { + b[i] = a[i].toUpperCase(); + } + return b; +} + +export const ArrayOfStrings_ID = idof(); + +// Example 7: Using custom classes. + +// The loader also understands exports of entire classes, and with the knowledge +// obtained in the previous examples it becomes possible to interface with a +// more complex program like the following in a nearly natural way. + +// see: tests/index.js "Test for Example 7" + +export namespace Game { + export class Player { + name: string; + position: Position | null; + constructor(name: string) { + this.name = name; + this.position = new Position(); + } + move(x: i32, y: i32): void { + var position = assert(this.position); + position.x += x; + position.y += y; + } + kill(): void { + this.position = null; + } + toString(): string { + var position = this.position; + if (position) { + return this.name + " @ " + position.toString(); + } else { + return this.name + " @ AWAITING ASSIGNMENT"; + } + } + } + export class Position { + x: i32 = 0; + y: i32 = 0; + toString(): string { + return this.x.toString() + "/" + this.y.toString(); + } + } +} diff --git a/examples/loader/assembly/myConsole.ts b/examples/loader/assembly/myConsole.ts new file mode 100644 index 0000000000..4701132746 --- /dev/null +++ b/examples/loader/assembly/myConsole.ts @@ -0,0 +1,9 @@ +// Example 3: Calling JavaScript imports with WebAssembly strings. + +// Let's declare a `myConsole` import, with member functions we can call with +// WebAssembly strings. Our imports, defined in the top-level index.js, will +// translate the calls to JavaScript's console API using the loader. + +export declare function log(s: string): void; +export declare function time(s: string): void; +export declare function timeEnd(s: string): void; diff --git a/examples/loader/assembly/tsconfig.json b/examples/loader/assembly/tsconfig.json new file mode 100644 index 0000000000..449ca07c76 --- /dev/null +++ b/examples/loader/assembly/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../../std/assembly.json", + "include": [ + "./**/*.ts" + ] +} \ No newline at end of file diff --git a/examples/loader/build/.gitignore b/examples/loader/build/.gitignore new file mode 100644 index 0000000000..22b2ed20a7 --- /dev/null +++ b/examples/loader/build/.gitignore @@ -0,0 +1,3 @@ +*.wasm +*.wasm.map +*.asm.js diff --git a/examples/loader/index.js b/examples/loader/index.js new file mode 100644 index 0000000000..5b65d56af5 --- /dev/null +++ b/examples/loader/index.js @@ -0,0 +1,22 @@ +const fs = require("fs"); +const loader = require("@assemblyscript/loader"); +const myModule = module.exports = loader.instantiateSync(fs.readFileSync(__dirname + "/build/optimized.wasm"), + + // These are the JavaScript imports to our WebAssembly module, translating + // from WebAssembly strings, received as a pointer into the module's memory, + // to JavaScript's console API as JavaScript strings. + { + // Example 3: Calling JavaScript imports with WebAssembly strings. + myConsole: { + log(messagePtr) { // Called as `console.log` in assembly/index.ts + console.log(myModule.__getString(messagePtr)); + }, + time(labelPtr) { // Called as `console.time` in assembly/index.ts + console.time(myModule.__getString(labelPtr)); + }, + timeEnd(labelPtr) { // Called as `console.timeEnd` in assembly/index.ts + console.timeEnd(myModule.__getString(labelPtr)); + } + } + } +); diff --git a/examples/loader/package.json b/examples/loader/package.json new file mode 100644 index 0000000000..dac32d3820 --- /dev/null +++ b/examples/loader/package.json @@ -0,0 +1,17 @@ +{ + "name": "@assemblyscript/loader-basics-example", + "version": "1.0.0", + "private": true, + "scripts": { + "asbuild:untouched": "asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --validate --sourceMap --debug", + "asbuild:optimized": "asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --validate --sourceMap --optimize", + "asbuild": "npm run asbuild:untouched && npm run asbuild:optimized", + "test": "node tests" + }, + "dependencies": { + "@assemblyscript/loader": "^0.9.4" + }, + "devDependencies": { + "assemblyscript": "^0.9.4" + } +} diff --git a/examples/loader/tests/index.js b/examples/loader/tests/index.js new file mode 100644 index 0000000000..40db09c757 --- /dev/null +++ b/examples/loader/tests/index.js @@ -0,0 +1,151 @@ +// Load the node module exporting our WebAssembly module +const myModule = require("../index"); + +// Obtain the runtime helpers for +const { + // memory management + __allocString, __allocArray, + // garbage collection + __retain, __release, + // and interop + __getString, __getArray, __getArrayView +} = myModule; + +// Test for Example 1: Passing a string from WebAssembly to JavaScript. +{ + console.log("Example 1:"); + + // Obtain a pointer to our string in the module's memory. Note that `return`ing + // a string, or any other object, from WebAssembly to JavaScript automatically + // retains a reference for us, the caller, to release when we are done with it. + const ptr = myModule.getHello(); + + // Print its contents + console.log(" " + __getString(ptr)); + + __release(ptr); // we are done with the returned string but + // it might still be alive in WebAssembly +} + +// Test for Example 2: Passing a string from JavaScript to WebAssembly. +{ + console.log("Example 2:"); + + // Allocate a string in the module's memory and retain a reference to our allocation + const ptr = __retain(__allocString("Hello world (I am a JavaScript string)")); + + // Pass it to our WebAssembly export, which is going to print it using our custom console + myModule.sayHello(ptr); + + __release(ptr); // we are done with the allocated string but + // it might still be alive in WebAssembly +} + +// Test for Example 4: Passing an array from WebAssembly to JavaScript. +{ + console.log("Example 4:"); + + // Obtain a pointer to our array in the module's memory. Note that `return`ing + // an object from WebAssembly to JavaScript automatically retains a reference + // for us, the caller, to release when we are done with it. + const ptr = myModule.getMyArray(10); + + // Obtain a live view on it + const view = __getArrayView(ptr); + console.log(" " + view + " (view)"); + + // Obtain a copy of it (modifying the live view does not modify the copy) + const copy = __getArray(ptr); + console.log(" " + copy + " (copy)"); + + __release(ptr); // we are done with the array +} + +// Test for Example 5: Passing an array from JavaScript to WebAssembly. +{ + console.log("Example 5:"); + + // Allocate a new array in WebAssembly memory and get a view on it. Note that + // we have to specify the runtime id of the array type we want to allocate, so + // we export its id (`idof`) from the module to do so. + const ptr = __retain(__allocArray(myModule.Int32Array_ID, [ 1, 2, 3 ])); + const view = __getArrayView(ptr); + const copy = __getArray(ptr); + + // Compute its sum + console.log(" Sum of " + view + " is " + myModule.computeSum(ptr)); + + // Modify the first element in place, and compute the new sum + view[0] = 42; + console.log(" Sum of " + view + " is " + myModule.computeSum(ptr)); + + // The initial copy remains unchanged and is not linked to `ptr` + console.log(" Unmodified copy: " + copy); + + __release(ptr); // we are done with our allocated array but + // it might still be alive in WebAssembly +} + +// Test for Example 6: WebAssembly arrays of WebAssembly strings. +{ + console.log("Example 6:"); + + // Allocate a new array, but this time its elements are pointers to strings. + // Note: Allocating an array of strings or other objects will automatically + // take care of retaining references to its elements, but the array itself + // must be dealt with as usual. + const inPtr = __retain(__allocArray(myModule.ArrayOfStrings_ID, [ "hello", "world" ].map(__allocString))); + + // Provide our array of lowercase strings to WebAssembly, and obtain the new + // array of uppercase strings before printing it. + const outPtr = myModule.capitalize(inPtr); + console.log(" Uppercased: " + __getArray(outPtr).map(__getString)); + + __release(inPtr); // release our allocation and release + __release(outPtr); // the return value. you know the drill! + + // Note that Example 6 is not an especially efficient use case and one would + // typically rather avoid the overhead and do this in JavaScript directly. +} + +// Test for Example 7: Using custom classes. +{ + console.log("Example 7:"); + + // Create a new player. Note that the loader makes a nice object structure + // of our exports, here a class `Player` within the `Game` namespace. So + // let's call the `Player` constructor (this is also an allocation): + let player; + { + const namePtr = __retain(__allocString("Gordon Freeman")); + player = new myModule.Game.Player(namePtr); + __release(namePtr); + } + console.log(" Player (new): " + __getString(player.toString())); + + // Move them and log again + player.move(10, 20); + console.log(" Player (moved): " + __getString(player.toString())); + + // Obtaining just the position. Note that we can `wrap` any pointer with + // the matching class within the object structure made by the loader. + { + const positionPtr = player.position; // calls a getter (implicit `return`) + const position = myModule.Game.Position.wrap(positionPtr); + console.log(" Position (wrapped): " + position.x + "/" + position.y); + + position.x -= 100; + position.y += 200; + console.log(" Position (moved): " + __getString(position.toString())); + + __release(positionPtr); // we are done with the returned object + } + + // Finish 'em + player.kill(); + console.log(" Player (finished): " + __getString(player.toString())); + + __release(player); // a tidy house, a tidy mind. +} + +// Interested in all the details? https://docs.assemblyscript.org/details :)