Skip to content

Add 'Loader' example #1247

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions examples/loader/README.md
Original file line number Diff line number Diff line change
@@ -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)<br />
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)<br />
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)<br />
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)<br />
The import declarations of our custom console API used in Example 3.

To rerun the tests:

```
$> npm test
```
127 changes: 127 additions & 0 deletions examples/loader/assembly/index.ts
Original file line number Diff line number Diff line change
@@ -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<Int32Array>();

// 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<string>(length);
for (let i = 0; i < length; ++i) {
b[i] = a[i].toUpperCase();
}
return b;
}

export const ArrayOfStrings_ID = idof<string[]>();

// 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();
}
}
}
9 changes: 9 additions & 0 deletions examples/loader/assembly/myConsole.ts
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions examples/loader/assembly/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../../../std/assembly.json",
"include": [
"./**/*.ts"
]
}
3 changes: 3 additions & 0 deletions examples/loader/build/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.wasm
*.wasm.map
*.asm.js
22 changes: 22 additions & 0 deletions examples/loader/index.js
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
);
17 changes: 17 additions & 0 deletions examples/loader/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
151 changes: 151 additions & 0 deletions examples/loader/tests/index.js
Original file line number Diff line number Diff line change
@@ -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<Int32Array>`) 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 :)