Using AssemblyScript with Webpack and TypeScript

Published on Wednesday, November 25th, 2020

I've recently been playing around with AssemblyScript for increasing the performance of my game engine Excalibur. It's a great language for WebAssembly, it's a subset of TypeScript (sounds familiar 😂) that compiles to WebAssembly. It was my first choice because I'm very familiar with TypeScript, but there are great resources out there for C++ and Rust to WebAssembly.

For my setup, I want to build my main app in TypeScript, my wasm in AssemblyScript and use a bundler to make it all happen. Turns out this was a bit tricky.

TLDR show me the code 😎

Enter AssemblyScript

Getting AssemblyScript installed and compiling is fast with npm

> npm install --save-dev --save-exact assemblyscript

I built a small wasm adder built in AssemblyScript to demostrate, it should look pretty familiar for peeps used to TypeScript. I'll be writing a more in-depth post on using AssemblyScript, but I recommend reading more. The only piece I will say, is that wasm only understands specialized number types and AssemblyScript has builtins to help with these. In this case below i32 stands for signed 32-bit integer.

// wasm-adder.ts
export function add(a: i32, b: i32): i32 {
return a + b;
}

You also need to setup a tsconfig.json nearby to pull in those special AssemblyScript types with extends.

//tsconfig.json
{
"extends": "assemblyscript/std/assembly.json",
"include": [
"wasm-adder.ts"
]
}

With all this setup it can be built into wasm on the commandline with:

> asc --optimize -b ./wasm/wasm-adder.wasm -t ./wasm/wasm-adder.wat ./wasm/wasm-adder.ts

Which when compiled to the wasm text format .wat looks like this. The final .wasm is a binary file!

 (func $wasm/assemblyscript/wasm-adder/add (param $0 i32) (param $1 i32) (result i32)
local.get $0
local.get $1
i32.add
)

Webpack Setup

I'm a big fan of Parcel for quick easy dev, but it's WebAssembly support isn't there yet. I tried to make my setup work in parcel but couldn't make it happen. Hopefully in Parcel 2 🤞

The Webpack setup here wasn't too bad. Here I've setup the typescript loading using the popular ts-loader and the excellent wasm-loader from ballercat (who arguably has the best GitHub username ever 😎).

// webpack.config.js
const path = require('path');
module.exports = {
// The 'first' file to start the import chain
entry: './main.ts',

// Local dev setings
mode: 'development',
devtool: 'source-map',
devServer: {
contentBase: path.join(__dirname, '.'),
compress: true,
port: 9000,
},

// Output bunddle into the dist folder
output: {
library: 'main',
libraryTarget: 'umd',
path: path.resolve(__dirname, "dist"),
},

resolve: {
// Add `.ts` and `.tsx` as a resolvable extension (means you can leave the file extension off)
// I purposley didn't put ".wasm" here to illustrate the file being loaded
extensions: [".ts", ".tsx", ".js"]
},
module: {
rules: [
// all files with a `.ts` or `.tsx` extension will be handled by `ts-loader`
{
test: /\.tsx?$/,
// Tells webpack how to interpret TypeScript files into JavaScript-land
loader: "ts-loader"
},
{
test: /\.wasm$/,
// Tells webpack how to interpret wasm files into JavaScript-land
loader: "wasm-loader"
}
]
}
}

AssemblyScript + Webpack

AssemblyScript doesn't have an official Webpack loader, but there are a few out there by others (buyer beware hasn't been updated in a while). However, I'm building my AssemblyScript manually, mostly because I have need to configure a few of the newer compiler options in my eventual use case.

Here I use the AssemblyScript compiler, asc, directly in my npm scripts. The AssemblyScript compiler has a lot of options the ones I have here specificy the -b binary, -t web assembly text file (for debugging), -d the TypeScript declaration file (this will be important for strong typing our wasm module), and finally the --optimize to do optimizations on the final generated assembly.

Below the idea is to build the wasm with the AssemblyScript compiler, then use the webpack wasm-loader to bundle it.

// package.json
"scripts": {
"build": "npm run build:assembly && webpack",
"build:assembly": "asc --optimize -b ./wasm/wasm-adder.wasm -t ./wasm/wasm-adder.wat -d ./wasm/types.d.ts ./wasm/assemblyscript/wasm-adder.ts",
// For development the wasm is only built once
// The wasm will need to be rebuilt each time there isn't an asc --watch command yet
"start": "npm run build:assembly && webpack serve"
}

Configuring your AssemblyScript and wasm-loader Types

One cool trick when importing non-TypeScript files is to setup a files.d.ts declaration at the root of your project. The declare module '*.wasm' caters to the wasm-loader's runtime JavaScript interface which provides a builder function with the optional WebAssembly imports. For AssemblyScript, you generally need to provide an importsObject directly with an env containingchunk of WebAssembly memory and an abort function.

// files.d.ts
declare module '*.wasm' {
// Strongly type the exports with T
function wasmBuilderFunc<T>(importsObject?: WebAssembly.Imports): Promise<{instance: WebAssembly.Instance & { exports: T }}>;

export = wasmBuilderFunc;
}

With Webpack and all the types configured, you can import the wasm file directly like so in main.ts!

// main.ts
// wasmBuilder for https://github.com/ballercat/wasm-loader webpack loader
import wasmBuilder from './wasm/wasm-adder.wasm';

wasmBuilder({
env: {
memory: new WebAssembly.Memory({ initial: 1}),
abort: () => {}
}
}).then(wasm => { ... });

One slightly goofy thing about the AssemblyScript declaration file is that it's the types of the instantiated WebAssembly module instance's exports, not the value directly out of the import which gives us a wasm-loader builder at runtime. So I renamed the AssemblyScript type declartion file to something else types.d.ts to not confuse TypeScript types and import those types separately (note in the package.json build:assembly). This approach was inspired by the way AssemblyScript imports the types using the small assemblyscript file loader if your interested.

// wasmBuilder for https://github.com/ballercat/wasm-loader webpack loader
import wasmBuilder from './wasm/wasm-adder.wasm';

// TypeScript type-only import
import type AdderModule from './wasm/types';

wasmBuilder<typeof AdderModule>(...).then(wasm => {
const Adder = wasm.instance.exports; // Full type support!
const results = Adder.add(1, 2);
document.write('Huzzah! 1 + 2 = ' + results) // Pardon the document.write 😢
});

Yay strong typing on wasm modules using AssemblyScript and Webpack!

Hope this helps my TypeScript and WebAssembly peeps out there!

Cheers! Erik