- Basics
- Runtimes
- Application platforms outside the browser
This is a Docker image for exercises in a Wasm workshop. It puts together the following tools:
Tool | Notes |
---|---|
Build Essentials | C Compiler, C Library, etc. |
wget, curl, vim | Just some useful tools |
WebAssembly Binary Toolkit (WABT) | Contains useful tools like wat2wasm |
Wasmtime | A runtime for WebAssembly & WASI |
Wasmer | A runtime for WebAssembly & WASIX |
emscripten | Compiler toolchain to Wasm |
Rust | Rust tools for Rust-related Wasm examples |
Fermyon Spin* | Platform for serverless Wasm apps |
WASI SDK | WASI-enabled WebAssembly C/C++ toolchain |
Wasm Tools | Rust tooling for low-level manipulation of WebAssembly modules |
WIT Bindgen | Guest language bindings generator for WIT and the Component Model |
.NET | .NET SDK for building Blazor apps |
Just | Useful command runner |
http-server | Simple static HTTP server |
Note that for Rust, the wasm32-wasip1 target, the wasm32-unknown-unknown target, wasm-pack, cargo-wasix
, WASM Composition Tooling (WAC), and cargo component
are also installed.
Note that for .NET, the wasm-tools and the wasm-experimental workload are also installed.
Note that for Wasm Tools, the languge toolings for Rust and JavaScript are installed.
Don't forget that there are online alternatives to running WABT locally!
*) Spin is currently disabled because of problems with running the installier in GitHub Actions.
NOTE: See the GitHub Repository for lots of sample code to be used in the workshop.
-
Start the container using Docker or a compatible container runtime:
docker run -it --rm -p 8080:8080 rstropek/wasm-workshop
-
If you don't have it, install Visual Studio Code with the following extensions:
-
Attach VSCode to the running container using the Docker extension.
The Docker image accepts the following arguments:
Argument | Default Value | |
---|---|---|
base_image |
ubuntu:noble |
The base image |
wasi_sdk |
24 |
WASI SDK version |
dotnet_repo |
24.04 |
Used .NET repository |
dotnet_version |
8.0 |
Installed .NET version |
node_major |
20 |
Installed Node version |
wasm_tools |
1.216.0 |
Installed Wasm Tools version |
Read more about .NET repository version here.
Note: Not all of these exercises have been checked for the newest versions of the tools. Please let me know if you find any issues.
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}
Compile and run:
$CCWASM hello.c -o hello.wasm
wasmtime hello.wasm
-
cargo new hello-wasm
-
Look at src/main.rs
-
Compile and run:
cargo build --target wasm32-wasi wasmtime target/wasm32-wasi/debug/hello-wasm.wasm
Goals: Make sure that you have the necessary tools (given if you use the Docker image), get familiar with compiling code to Wasm and running it outside of the browser with Wasmtime.
- Choose C or Rust
- Implement a program that prints all Fibonacci numbers up to 1000
- Compile it to Wasm
- Run it with Wasmtime
Try this code in the online WAT editor.
(module
;; The memory section declares a linear memory instance and initializes it with a given contents.
;; Memory is array-like and can be accessed with loads and stores.
;; Here, we allocate 1 page of memory, which is 64KiB.
(memory 1)
;; The export section makes WebAssembly functions and memory available for calling from JavaScript.
;; We're exporting the memory we defined above so we can manipulate it or read from it in JS.
(export "memory" (memory 0))
;; The func section declares a list of functions in the module.
(func $fibonacci
;; Declaring the result type of the function.
;; Our fibonacci function returns an i32 (32-bit integer) with the number
;; of elements written to memory (address 0).
(result i32)
;; Defining local variables which will be used in the function.
;; The local.get and local.set instructions allow for manipulating them.
(local $current i32) ;; will hold a fibonacci number
(local $next i32) ;; will hold the subsequent fibonacci number
(local $ptr i32) ;; will be used as a pointer to memory where to store the numbers
(local $limit i32) ;; will define our upper bound for fibonacci calculation
(local $temp i32) ;; temporary variable
;; Initializing our local variables.
(local.set $current (i32.const 0))
(local.set $next (i32.const 1))
(local.set $ptr (i32.const 0))
(local.set $limit (i32.const 100))
;; Storing the first two Fibonacci numbers (0 and 1) in memory.
(i32.store (local.get $ptr) (local.get $current))
;; Update $ptr to point to the next memory cell.
(local.set $ptr (i32.add (local.get $ptr) (i32.const 4)))
(i32.store (local.get $ptr) (local.get $next))
(local.set $ptr (i32.add (local.get $ptr) (i32.const 4)))
;; Loop that calculates Fibonacci numbers and stores them in memory.
(loop $loop1
;; Store the sum of $current and $next in a temporary variable.
(local.set $temp (i32.add (local.get $current) (local.get $next)))
;; Check if the new Fibonacci number is less than or equal to our limit.
(if (i32.le_s (local.get $temp) (local.get $limit))
(then
;; If yes, update $current to the value of $next.
(local.set $current (local.get $next))
;; Update $next to the new Fibonacci number.
(local.set $next (local.get $temp))
;; Store the new Fibonacci number in memory.
(i32.store (local.get $ptr) (local.get $temp))
;; Update $ptr to point to the next memory cell.
(local.set $ptr (i32.add (local.get $ptr) (i32.const 4)))
;; Continue loop from its beginning.
(br $loop1)
)
)
)
;; At the end, we return how many Fibonacci numbers have been calculated
;; and stored in memory by dividing the memory pointer by 4
;; (since WebAssembly's i32 takes up 4 bytes of memory).
(i32.div_u (local.get $ptr) (i32.const 4))
)
;; Exporting the fibonacci function so it can be called from JavaScript.
(export "fibonacci" (func $fibonacci))
)
const wasmInstance = new WebAssembly.Instance(wasmModule, {});
const { fibonacci } = wasmInstance.exports;
let len = fibonacci();
console.log(`We got ${len} numbers and here they are:`);
const fibonacciNumbers = new Uint32Array(wasmInstance.exports.memory.buffer);
// Extract Fibonacci numbers from memory and print them
for (let i = 0; i < len; i++) {
console.log(fibonacciNumbers[i]);
}
Exercises:
- Store the WAT code locally and compile it with wat2wasm:
wat2wasm fib.wat
- Decompile the Wasm code with wasm2wat:
wasm2wat fib.wasm
- Decompile the Wasm code with wasm2c:
wasm2c fib.wasm
- Try running wat-desugar on the WAT code: :
wat-desugar fib.wat
- Try running wasm-stats:
wasm-stats fib.wasm
Palindrome checker writte with WAT, running in the browser. Sample code can be found here.
Introduction to WAT based on first day of Advent of Code 2022. See justfile for commands. The sample contains hosts in
- Rust,
- JavaScript and
- .NET
Compare Mini Rust sample with Full Rust sample.
Emscripten is a complete Open Source compiler toolchain to WebAssembly. Using Emscripten you can compile C and C++ code, or any other language that uses LLVM, into WebAssembly, and run it on the Web, Node.js, or other Wasm runtimes. Read more here.
The following examples uses cwrap
. cwrap
is quite straightforward and designed to be simple. It's suitable for scenarios where you only need to call a few C functions from JavaScript. It doesn't handle C++ classes or objects. It's quite limited in terms of type support. When your needs are pretty simple, such as calling a few C functions without involving C++ objects or classes, cwrap
is a good choice because of its simplicity and less overhead.
mkdir emscripten
cd emscripten
mkdir test
cd test
Create the file hello_function.cpp in the test folder:
#include <math.h>
extern "C" {
int int_sqrt(int x) {
return sqrt(x);
}
bool is_palindrome(char *text, int len) {
char *end = text + len - 1;
while (text < end) {
if (*text != *end) { return false; }
text++;
end--;
}
return true;
}
}
cd ..
emcc test/hello_function.cpp -o function.js -sEXPORTED_FUNCTIONS=_int_sqrt,_is_palindrome -sEXPORTED_RUNTIME_METHODS=ccall,cwrap
Create the file index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="function.js"></script>
<script>
Module.onRuntimeInitialized = () => {
int_sqrt = Module.cwrap("int_sqrt", "number", ["number"]);
console.log(int_sqrt(12));
is_palindrome = Module.cwrap("is_palindrome", "number", [
"string",
"number",
]);
text = "otto";
console.log(is_palindrome(text, text.length));
};
</script>
</body>
</html>
Run a local web server (http-server
) and open the page in your browser. Check the console for results.
Embind is used to bind C++ functions and classes to JavaScript, so that the compiled code can be used in a natural way by "normal" JavaScript. embind is more feature-rich and complex than cwrap
. It provides a robust framework for interacting between C++ and JavaScript. Unlike cwrap
, embind allows you to expose entire C++ classes and objects to JavaScript, not just functions. It provides a wide range of type mappings and can handle complex data types, such as classes and enums. When you need to expose more than just functions and deal with C++ objects, classes, and other complex types, embind would be the preferred choice despite its additional overhead.
mkdir embind
cd embind
mkdir test
cd test
Create the file hello_function.cpp in the test folder:
#include <emscripten/bind.h>
#include <math.h>
#include <string>
using namespace emscripten;
extern "C" {
int int_sqrt(int x) {
return sqrt(x);
}
bool is_palindrome(const std::string& text)
{
int start = 0;
int end = text.length() - 1;
while (start < end) {
if (text[start] != text[end]) {
return false;
}
start++;
end--;
}
return true;
}
}
EMSCRIPTEN_BINDINGS(my_module) {
function("int_sqrt", &int_sqrt);
function("is_palindrome", &is_palindrome);
}
cd ..
emcc -l embind test/hello_function.cpp -o function.js
Create the file index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="function.js"></script>
<script>
Module.onRuntimeInitialized = () => {
console.log(Module.int_sqrt(12));
text = "radar";
console.log(Module.is_palindrome(text));
};
</script>
</body>
</html>
Run a local web server (http-server
) and open the page in your browser. Check the console for results.
Unfortunately, WasmFiddle is no longer available. The sample code is kept here just for reference.
WasmFiddle was a great online tool for experimenting with Wasm. It allowed you to write Wasm code in C. It also allowed you to write JavaScript code that can call Wasm functions. You had log functions and you can even draw on a HTML Canvas. The tool was very useful for learning Wasm and experimenting with it.
Here is an example:
// Define a structure to represent a point in 2D space
struct Point {
long x; // Horizontal coordinate
long y; // Vertical coordinate
};
// Define a structure to encapsulate two Point structures
struct TwoPoints {
struct Point point1; // First point in 2D space
struct Point point2; // Second point in 2D space
};
// Create a global variable to hold two points
struct TwoPoints points;
// Directional variables to dictate the movement of point1
int dir_x1 = 10; // Horizontal direction (positive means moving right)
int dir_y1 = 15; // Vertical direction (positive means moving up)
// Directional variables to dictate the movement of point2
int dir_x2 = 25; // Horizontal direction (positive means moving right)
int dir_y2 = -5; // Vertical direction (negative means moving down)
// Function to move two points within specified width and height
// and bounce them back when they hit the boundaries
void move(int width, int height) {
// Move point1
// Add respective direction values to point1 coordinates
points.point1.x += dir_x1;
points.point1.y += dir_y1;
// Move point2
// Add respective direction values to point2 coordinates
points.point2.x += dir_x2;
points.point2.y += dir_y2;
// Check bounds and bounce for point1
// If point1 x-coordinate is out of bounds, reverse its x-direction
if (points.point1.x <= 0 || points.point1.x >= width) {
dir_x1 = -dir_x1;
}
// If point1 y-coordinate is out of bounds, reverse its y-direction
if (points.point1.y <= 0 || points.point1.y >= height) {
dir_y1 = -dir_y1;
}
// Check bounds and bounce for point2
// If point2 x-coordinate is out of bounds, reverse its x-direction
if (points.point2.x <= 0 || points.point2.x >= width) {
dir_x2 = -dir_x2;
}
// If point2 y-coordinate is out of bounds, reverse its y-direction
if (points.point2.y <= 0 || points.point2.y >= height) {
dir_y2 = -dir_y2;
}
}
// Function to get a pointer to the points data
// This function could be used to access point data without exposing the actual structure
long* getPoints() {
// Cast the address of `points` to a pointer to long
// This allows accessing x and y coordinates as an array
// Note: This breaks the abstraction of the point structures
// and would require knowledge of the structure layout to use safely.
return (long*)&points;
}
// Initialize a WebAssembly (Wasm) module and instance
// `wasmCode` and `wasmImports` are assumed to be defined elsewhere in your code
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, wasmImports);
// Acquire a reference to the memory used by the Wasm instance, and create
// a typed array (Int32Array) to manipulate the memory in a more accessible way.
// Note: Wasm memory is a contiguous buffer of bytes. Typed arrays allow us
// to interact with this buffer using JavaScript’s numeric types.
const buffer = wasmInstance.exports.memory.buffer;
const points = new Int32Array(buffer);
// Obtain the offset into Wasm memory where point data is stored.
// Divide by 4 because Int32Array views memory as 32-bit chunks,
// and we want to index into them, not the individual bytes.
let offset = wasmInstance.exports.getPoints() / 4;
// Initialize the points in the Wasm memory.
// These points will be used in the rendering logic below.
points[offset] = 10;
points[offset + 1] = 20;
points[offset + 2] = 30;
points[offset + 3] = 40;
// Initialize the canvas and get its 2D rendering context
// `lib.showCanvas()` is assumed to perform canvas-related initialization
// and `canvas` is assumed to be available in the scope.
lib.showCanvas();
let ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Define a counter that will limit the number of animation frames to 1000.
// It's useful to prevent an infinite animation loop.
let counter = 1000;
function getRandomColor() {
// Generate random values for red, green, and blue channels.
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
// Construct and return an RGB color string.
return `rgb(${r},${g},${b})`;
}
// Define the animation function that will be called repeatedly
function step() {
// Decrease the counter by one on each frame.
counter--;
// Call the `move` function exported from the Wasm instance,
// which updates the position of points according to canvas dimensions.
wasmInstance.exports.move(canvas.width, canvas.height);
// Clear the entire canvas, preparing it for the next frame of drawing.
// Try the code with the following line and without
//ctx.clearRect(0, 0, canvas.width, canvas.height);
// Begin a new path for the line to be drawn between the points.
ctx.beginPath();
// Move the drawing cursor to the first point.
ctx.moveTo(points[offset], points[offset + 1]);
// Draw a line from the current position (first point) to the second point.
ctx.lineTo(points[offset + 2], points[offset + 3]);
// Set the style and width of the line.
ctx.strokeStyle = getRandomColor();
ctx.lineWidth = 5;
// Actually draw the path using the previously defined line and style.
ctx.stroke();
// If the counter is not yet exhausted, request the next animation frame.
if (counter > 0) {
window.requestAnimationFrame(step);
}
}
// Kickstart the animation by calling `step` on the next frame.
window.requestAnimationFrame(step);
Example see here.
$CCWASM demo.c -o demo.wasm
ls -la demo.wasm
wasmtime demo.wasm
echo hello wasm from c > test.txt
wasmtime demo.wasm test.txt /tmp/test.txt
wasmtime --dir=. --dir=/tmp demo.wasm test.txt /tmp/test.txt
cat /tmp/test.txt
You can also limit the CPU usage of Wasm by specifying a fuel limit:
# Should work, enough fuel
wasmtime --dir=. demo.wasm --fuel 10000 hallo.txt hallo2.txt
# Should not work, Wasm runs out of fuel
wasmtime --dir=. demo.wasm --fuel 1000 hallo.txt hallo2.txt
Read more about the above sample script here.
Example see here.
cargo build --target wasm32-wasi
ls -la target/wasm32-wasi/debug/demo.wasm
cp target/wasm32-wasi/debug/demo.wasm .
wasmtime demo.wasm
echo hello wasm from Rust > test.txt
wasmtime demo.wasm test.txt /tmp/test.txt
wasmtime --dir=. --dir=/tmp demo.wasm test.txt /tmp/test.txt
cat /tmp/test.txt
Read more about the above sample script here.
mkdir dotnet-wasi
cd dotnet-wasi
dotnet new console
dotnet add package Wasi.Sdk --prerelease
ls -la ./bin/Debug/net8.0/
wasmtime ./bin/Debug/net8.0/dotnet-wasi.wasm
Read more here. Note that this example uses experimental features of .NET and is not ready for production!
Microsoft offers good tutorials for Blazor Wasm. This image contains the necessary tools to follow the tutorials.
Notes:
- You must run Blazor apps with
dotnet run --urls http://*:8080
to make them accessible from outside of the container. - Blazor caches the Wasm- and DLL-files after the first load of the app. If you want to demonstrate how Blazor Wasm uses .NET DLLs in the browser, do not forget to clear the Cache storage using your browser's dev tools.
(module
(func $add (param $lhs i32) (param $rhs i32) (result i32)
local.get $lhs
local.get $rhs
i32.add)
(export "add" (func $add))
)
package example:component;
world example {
export add: func(x: s32, y: s32) -> s32;
}
wasm-tools component embed add.wit add.wat -o add.wasm
wasm-tools component new add.wasm -o add.component.wasm
See wasm-component
- Create a new Spin application:
spin new http-rust hello_spin
- Build the application:
spin build
- Take a look at the generated code:
ls -la target/wasm32-wasi/release/
- Run the app:
spin up --listen [::]:8080
(this enables accessing the app from outside of the container)
A larger example (Todo list) can be found here. Sample requests can be found here (you can run time with Rest Client, Postman, or another tool for issuing HTTP requests).