Programming WebAssembly with Rust Part 1: From WAT to WASM Integration

Matthew MacFarquhar
7 min readOct 12, 2024

--

Introduction

I have been reading through this book which goes through the intricacies and benefits of building to and using Wasm for creating modules that can be run in polyglot environments. I will be augmenting and summarizing the book chapters throughout these articles. The code and complete examples for these articles can be found in the below github repo.

Background

WebAssembly (Wasm) is a low-level assembly-like language that runs in modern web browsers, designed to complement JavaScript by providing a more efficient way to execute code in the browser. Unlike JavaScript, which is interpreted at runtime, WebAssembly code is compiled ahead of time and runs at near-native speed. WebAssembly is platform-independent, meaning it can run on any environment that supports the Wasm runtime, making it a powerful tool for polyglot development.

At its core, WebAssembly works by interacting with linear memory — a contiguous block of memory that the program can directly manipulate. The host environment can also interact with this memory block, which gives an illusion of Wasm and host interoperability when in fact they are only interacting through a single buffer of data.

The WebAssembly execution model is stack-based, meaning that values are pushed onto and popped from a stack to perform operations. WebAssembly modules are compiled into binary instructions that the browser executes, and these instructions are designed to be optimized by modern hardware, ensuring quick execution.

Benefits of WebAssembly

  • Performance: Since WebAssembly is compiled into highly efficient bytecode that runs at near-native speed, it can handle computationally intensive tasks that would be too slow for JavaScript alone. This makes WebAssembly ideal for applications like video editing, 3D graphics, scientific simulations, and gaming.
  • Language Support: WebAssembly is not restricted to one programming language. It can be compiled from languages like C, C++, and Rust, enabling developers to bring existing codebases into the web environment without rewriting them in JavaScript.
  • Portability: WebAssembly is designed to be platform-agnostic, allowing the same code to run on different operating systems and devices. This makes it easy to deploy high-performance applications across various platforms.
  • Security: WebAssembly runs in a sandboxed execution environment within the browser, which adds a layer of security. It interacts with the host system in a controlled way, minimizing the risk of vulnerabilities being exploited.
  • Interoperability: WebAssembly doesn’t replace JavaScript but complements it. It can seamlessly integrate with JavaScript applications, allowing developers to use WebAssembly for performance-critical parts of their app while continuing to rely on JavaScript for other aspects.

Basic Example

Let’s get some practice with a basic example. WebAssembly code can be written in a human-readable format called WebAssembly Text Format (WAT). Let’s go through a basic example of a WebAssembly module that performs addition.

Here’s a piece of simple WAT code that defines an addition function:

(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
(i32.add
(local.get $lhs)
(local.get $rhs)
)
)
(export "add" (func $add))
)

This module has a single add function which take in two i32 parameters and returns the addition of them. In practice, we don’t usually write WAT code by hand, we use a higher level language (like Rust or C) which is then turned into WAT. However, doing this by hand lets us get more familiar with the full process of what the compilation is doing.

The WebAssembly Text Format (WAT) is a great way to represent WebAssembly in a human-readable form, but it needs to be compiled into a binary format (Wasm) to be used by a browser or a JavaScript environment.

To compile the WAT code into a WASM module, you can use the wat2wasm tool.

wat2wasm add.wat -o add.wasm

WASM Tic-Tac-Toe

Now, let’s work on a more complete example for Tic-Tac-Toe, which we will then load up inside of JS to execute in the browser.

The WAT (WebAssembly Text Format) Code

Here’s the WAT code for our Tic-Tac-Toe game. This code defines a game board, manages player turns, and provides functions to set and retrieve pieces on the board.

(module
(import "events" "pieceplaced" (func $notify_pieceplaced (param $x i32) (param $y i32)))

(global $currentTurn (mut i32) (i32.const 1))
(global $X i32 (i32.const 1))
(global $O i32 (i32.const 2))
(memory $mem 1)

;; get linear index in memeory given x and y
(func $indexForPosition (param $x i32) (param $y i32) (result i32)
(i32.add
(i32.mul
(i32.const 3)
(local.get $y)
)
(local.get $x)
)
)

;; Offset = (x + y * 3) * 4 offset in bytes
(func $offsetForPosition (param $x i32) (param $y i32) (result i32)
(i32.mul
(call $indexForPosition (local.get $x) (local.get $y))
(i32.const 4)
)
)

;; Determine if piece is X
(func $isX (param $piece i32) (result i32)
(i32.eq
(i32.and (local.get $piece) (global.get $X))
(global.get $X)
)
)

;; Determine if piece is Y
(func $isY (param $piece i32) (result i32)
(i32.eq
(i32.and (local.get $piece) (global.get $O))
(global.get $O)
)
)

;; Sets a piece on the game board
(func $setPiece (param $x i32) (param $y i32) (param $piece i32)
(i32.store
(call $offsetForPosition
(local.get $x)
(local.get $y)
)
(local.get $piece)
)
)

;; Get a piece from the board. Out of range causes a trap
(func $getPiece (param $x i32) (param $y i32) (result i32)
(if (result i32)
(block (result i32)
(i32.and
(call $inRange
(i32.const 0)
(i32.const 2)
(local.get $x)
)
(call $inRange
(i32.const 0)
(i32.const 2)
(local.get $y)
)
)
)
(then
(i32.load
(call $offsetForPosition
(local.get $x)
(local.get $y)
)
)
)
(else
(unreachable)
)
)
)

;; detect if within board range
(func $inRange (param $low i32) (param $high i32) (param $value i32) (result i32)
(i32.and
(i32.ge_s (local.get $value) (local.get $low))
(i32.le_s (local.get $value) (local.get $high))
)
)

;; Gets current turn owner
(func $getTurnOwner (result i32)
(global.get $currentTurn)
)

;; Sets turn owner
(func $setTurnOwner (param $piece i32)
(global.set $currentTurn (local.get $piece))
)

;; Toggle turn owner
(func $toggleTurnOwner
(if (i32.eq (call $getTurnOwner) (i32.const 1))
(then (call $setTurnOwner (i32.const 2)))
(else (call $setTurnOwner (i32.const 1)))
)
)

;; Determine if it's a player's turn
(func $isPlayersTurn (param $player i32) (result i32)
(i32.gt_s
(i32.and (local.get $player) (call $getTurnOwner))
(i32.const 0)
)
)

;; Determine if move is valid
(func $isValidMove (param $x i32) (param $y i32) (result i32)
(local $target i32)
(local.set $target (call $getPiece (local.get $x) (local.get $y)))

(if (result i32)
(block (result i32)
(i32.eq (local.get $target) (i32.const 0))
)
(then (i32.const 1))
(else (i32.const 0))
)
)

;; Exported move function to be called by the game host
(func $move (param $x i32) (param $y i32) (result i32)
(if (result i32)
(block (result i32)
(call $isValidMove (local.get $x) (local.get $y))
)
(then
(call $do_move (local.get $x) (local.get $y))
)
(else
(i32.const 0)
)
)
)

;; Internal move function, performs actual move post-validation of target.
(func $do_move (param $x i32) (param $y i32) (result i32)
(local $curpiece i32)
(local.set $curpiece (call $getTurnOwner))
(call $setPiece (local.get $x) (local.get $y) (local.get $curpiece))
(call $toggleTurnOwner)
(call $notify_pieceplaced (local.get $x) (local.get $y))
(i32.const 1)
)

(export "getPiece" (func $getPiece))
(export "getTurnOwner" (func $getTurnOwner))
(export "move" (func $move))
(export "isValidMove" (func $isValidMove))
(export "memory" (memory $mem))
)

The first thing we do in our WAT file is to import an outside function called pieceplaced which lives in an events module. Building this import mapping allows our Wasm code to take in that host defined function and execute it within our Wasm.

Next, we define some global variables we will use across our modules and the memory block we will be operating on (which will represent the tic-tac-toe board).

We define a series of functions to help us index into the memory to access parts of the tic-tac-toe board, set and get pieces on the board, validate and perform moves, and facilitate turn management.

We export some functions at the bottom of the WAT file which expose these functions to our host (JS in our case) for it to invoke.

Loading and Using the Wasm Module

fetch("./tictactoe.wasm").then(response => response.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes, {
events: {
pieceplaced: (x, y) => {
console.log(`Piece placed at (${x}, ${y})`)
}
}
}))
.then(results => {
const instance = results.instance;

console.log(`At start, turn owner is ${instance.exports.getTurnOwner()}`)

instance.exports.move(0, 1); // X
console.log(`Turn owner is ${instance.exports.getTurnOwner()}`)
instance.exports.move(0,1); // O
console.log(`Afer invalid move, Turn owner is ${instance.exports.getTurnOwner()}`)
instance.exports.move(1,1); // O

console.log(`At end, turn owner is ${instance.exports.getTurnOwner()}`)
}).catch(console.error)

To use a Wasm module inside of our JS environment we must preform the following steps:

  • fetch: Loads the compiled WebAssembly module as an array buffer.
  • WebAssembly.instantiate: Instantiates the module and provides the imported functions (in this case, the pieceplaced event).
  • instance.exports: Through this accessor, we can invoke Wasm defined functionality from our JS environment

HTML Setup

Running this in a browser is as simple as creating an index.html which uses our JS.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
background-color: rgb(255, 255, 255);
}
</style>
</head>
<body>
<span id="container"></span>
<script src="./index.js"></script>
</body>
</html>

and then starting a server which will allows us to run the html page and let the js load the WASM module.

python3 -m http.server 8080

Conclusion

In this project, we manually wrote WebAssembly Text Format (WAT) code, which was then converted into WebAssembly (Wasm) to handle the core game logic for a Tic-Tac-Toe game. Along the way, we accomplished several key objectives:

  • Writing WAT by hand: This process helped solidify our understanding of how low-level memory management and functions work in WebAssembly. We manually defined global variables, memory blocks, and key game functions like placing pieces and switching turns.
  • Loading Wasm from JavaScript: We saw how to seamlessly load and instantiate Wasm modules in a JavaScript environment using the WebAssembly.instantiate method.
  • Importing host functions into Wasm: By importing JavaScript functions into the Wasm module, we were able to trigger DOM updates, such as logging moves or notifying the UI when a piece was placed. This demonstrated how WebAssembly can call out to host functions, blending its power with the flexibility of JavaScript.
  • Exporting Wasm functionality for the host to call: We also learned how to export Wasm functions, allowing JavaScript to call core game functions such as checking for valid moves, placing pieces, and switching turns. This modular approach created a smooth connection between Wasm’s fast execution and JavaScript’s ability to manage interactions and UI.

Overall, this project provided valuable insights into combining WebAssembly and JavaScript, creating efficient and interactive web applications.

--

--

Matthew MacFarquhar
Matthew MacFarquhar

Written by Matthew MacFarquhar

I am a software engineer working for Amazon living in SF/NYC.

No responses yet