Skip to main content

Typescript and the WASM Rust Tutorial

· 9 min read
Tony Hallam
Application Consultant @ EPCC

Investigations of the Rust WASM tutorial with Typescript.

I had been looking for an interesting project to play around with Rust for a while and I found one (more on that in the future). But I had to first get acquainted with Rust again and more importantly with its cross compilation to the WASM runtime. For this there is the Rust WASM tutorial which is a really nice comprehensive guide/intro to the topic.

My repository of the final topic is here. And the Typescript version of the tutorial is demonstrated on the Game of Life Page.

Working through the example as it is was fairly straight forward and the Javascript implementation worked with no changes required to the content at all. However, I like typing and I was interested to see where I could go with the Typescript implementation, including some upgrades to the code and this is where things got a little bit interesting.

There are a few things to note about the Rust WASM tutorial beforehand however:

  • The versions used by the templates (e.g. create-wasm-pack) are quite old. Node spits out a lot of warnings. Upgrading creates some compatibility issues which I had to address during my conversion to TypeScript.
  • wasm-pack the package generator for Rust to WASM isn't 100% complete yet when it comes to TypeScript, which also caused some issues.

Setting Up

Follow the tutorial instructions for setup, including installing Rust and Node (or pnpm). For TypeScript you will need to add the typescript node package.

$ pnpm install -g typescript

Project setup

I wanted to place all my working for this example in a single repository. Hence I've moved the different parts of the example into their own subfolders, for example, the Rust code is under the rs tree.

$ tree -L 1 ./wasm-rust-gol
wasm-rust-gol/
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── rs
├── www
├── www.new
└── www.ts

The three versions of www represent different iterations of the Javascript/Typescript conversion.

  • www: The original Javascript implementation which is faithful to the Tutorial.
  • www.new: Updated packages including the webpack bundler but still in Javascript.
  • www.ts: Update packages and converted to TypeScript.

Updating the Rust packages

The Rust package versions in the tutorial are a little out of date. So the first step was to update some values in the Cargo.toml file of the rs package. Important changes are

[package]
edition = "2021"

[dependencies]
wasm-bindgen = "0.2.93"

[dev-dependencies]
wasm-bindgen-test = "0.3.43"

The edition keyword manages the backwards compatibility of individual Rust crates. I'm not limited to using 2018 as in the tutorial template so I've updated this to 2021. The wasm- dependencies have also moved on since the tutorial template was written and I've updated these to the latest stable releases.

When building the package I want to specify the output directory to outside the source tree. For me, this keeps the source tree clean and means I can package everything into a single directory. Although I've specified the --target flag here, it isn't strictly necessary, wasm-pack build uses bundler as the default target.

$ wasm-pack build --out-dir ../dist/wasm-rust-gol --target bundler

Updating Javascript

Creating a fresh project

After getting the original example working per the tutorial I wanted to make a few changes which have been captured under ./www.new in the repo. In this version of the example I have regenerated the Webpack package using the Webpack CLI.

$ mkdir www.new
$ cd www.new
$ pnpm init
$ pnpm add --save-dev webpack webpack-cli
$ pnpm webpack init

Follow the prompts selecting ES6, webpack-dev-server=Y, simplify-html=Y, PWA=n, CSS only, PostCSS=n, extra-CSS-for-every-file=n, prettier-config=Y, and package-manager=pnpm or alter individual values to suit your use case. This will generate the www.new directory with the webpack templated files matching your responses to the prompts.

$ tree www.new
www.new
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── src
│ └── index.js
└── webpack.config.js

There are some key differences to the original www project structure. The Javascript source code now sits under www.new/src, there is significantly more content in the webpack.config.js file and all of the webpack dependencies needed for your project will be added to the package.json. You can edit some of the fields in the package.json to suit your project (e.g. version, description and name.)

Importantly the local WASM package compiled previously needs to be added to the dependencies which will allow us to import the package in the Javascript of our application.

"devDependencies": {...},
"dependencies": {
"wasm-rust-gol": "file:../dist/wasm-rust-gol"
}

Create gol.js

I decided to push all of the game of life Javascript into a separate file under ./src/gol.js. This is the same as for the tutorial example. In the example the memory variable imported from WASM looks like

import { memory } from "wasm-rust-gol/wasm_rust_gol_bg";

but this resulted in an error for me. I had to change the reference to include the .wasm extension to make it work.

import { memory } from "wasm-rust-gol/wasm_rust_gol_bg.wasm";

Create bootstrap.js

./src/bootstrap.js also comes from the original example. It's purpose is to check and load the example from ./src/gol.js.

import("./gol.js")
.catch(e => console.error("Error importing `gol.js`:", e));

The Webpack config must now also be modified to point to the ./src/bootstrap.js as the entry point.

webpack.config.js
const config = {
entry: './src/bootstrap.js',
...
}

Modify index.html

The index.html file was automatically created by Webpack when we initiated the package and it needs to be modified in line with the tutorial. The canvas object is identified through its id to be replaced by the javascript in gol.js.

index.html
<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8" />
<title>WASM-RUST-GOL-JSNew</title>
</head>

<body>
<canvas id="game-of-life-canvas"></canvas>
</body>

</html>

CSS

We could start the website now but first we will add some styling using CSS. This differs to the tutorial which put the styling directly in the HTML template (index.html). Simply move the same attributes to the CSS document using a body selector.

body {
position: absolute;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

The CSS must then be loaded into the entrypoint for the package bootstrap.js.

./src/bootstrap.js
...
import * as style_base from "stylesheets/base.css";

Running the development server

Start the development server and Webpack's updated commands should automatically open the page.

$ pnpm run start

Getting TypeScript working

Getting TypeScript (TS) working was a little more challenging than for the original and updated javascript versions of this example.

This version also uses SCSS styling over CSS but it practically makes little difference apart from some configuration options for Webpack.

Setup

Similar to the javascript update Webpack is used to setup a new package www.ts with a TypeScript solution template from the prompt. The template includes some additional files over the javascript template. This includes the TS compiler configuration tsconfig.json which needs to be configured and a tests folder.

$ tree www.ts
www.ts
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── src
│ └── index.ts
├── tests
├── tsconfig.json
└── webpack.config.js

Similar to the javascript packages we need to add the WASM package as a dependency to package.json and the install everything with pnpm.

package.json
  "devDependencies": {
...
},
"dependencies": {
"wasm-rust-gol": "file:../dist/wasm-rust-gol"
}
$ pnpm install

webpack.config.js

Web Assembly has experimental support from Webpack for now and it needs to be enabled in the Webpack config for the modules to load correctly. This is done setting the experiments key in the exported config variable. We also want to add an alias for a stylesheets directory while here.

webpack.config.js
module.exports = () => {
config.experiments = {
asyncWebAssembly: true
}
config.resolve = {
alias: {
stylesheets: path.resolve(__dirname, './stylesheets')
},
},
...
return config;
}

tsconfig.json

Creating some compatibility with WASM also needed compiler options to be specified in tsconfig.json. Through a bit of trial and error it became apparent that updates were need to the module and target versions of ES. Older versions didn't support the functionality necessary to run the application.

tsconfig.json
{
"compilerOptions": {
"module": "es2022", // required for dynamic imports ERROR:TS1323
"target": "es2018", // TS2468, TS2585, TS2712 all fixed by including more recent es version
"moduleResolution": "Bundler", // TS2793 Can't find the wasm module
},
"include": [
"src/**/*.ts"
]
}

gol.ts

There were some issues in converting the gol.js to TS and I stumbled across this tutorial which used the canvas object in a page. It inspired me to have a go at refactoring gol.js so that the game of life functionality was encapsulated within a class.

The results were fairly straight forward I think, and significantly improved the legibility of the example javascript. I also added a sleep function to slow down the game simulation.

The conversion to TS was quite simple, and once the compilation issues were resolved it functioned equivalently to the JS version.

Issues with wasm-pack

Although the program will now run, it still reports a TS error from compilation.

[tsl] ERROR in /home/ahallam/dev/wasm-rust-gol/www.ts/src/gol.ts(6,24)
TS2307: Cannot find module 'wasm-rust-gol/wasm_rust_gol_bg.wasm' or its corresponding type declarations.

Although some of the TS type definitions are added to our WASM package, the raw declarations are missing from the package.json as outline in this issue. We can resolve the error by adding them to our wasm-pack created package.json and reinstall the package with pnpm.

package.json
"files": [
"wasm_rust_gol_bg.wasm",
"wasm_rust_gol_bg.wasm.d.ts",
...
]
$ cd www.ts
$ pnpm install

Unfortunately, wasm-pack will overwrite the package.json each time you recompile the Rust crate. Hopefully this will get fixed soon!

Final steps

Similar to the Javascript Update you will need to modify the index.html template and the add the SCSS styling. Checkout the repo if you are stuck.

Running the development server

The TS version should now be working, start the development server and Webpack's updated commands should automatically open the page.

$ pnpm run start