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 |
| 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-wasi target and wasm-pack 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!
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:jammy | The base image |
wasi_sdk | 20 | WASI SDK version |
dotnet_repo | 22.04 | Used .NET repository |
dotnet_version | 7.0 | Installed .NET version |
node_major | 20 | Installed Node version |
wasm_tools | 1.0.51 | Installed Wasm Tools version |
Read more about .NET repository version here.
#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.

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 $i i32) ;; will hold a fibonacci number
(local $j i32) ;; will hold the subsequent fibonacci number
(local $k 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
;; Initializing our local variables.
(local.set $i (i32.const 0))
(local.set $j (i32.const 1))
(local.set $k (i32.const 0))
(local.set $limit (i32.const 100))
;; Storing the first two Fibonacci numbers (0 and 1) in memory.
(i32.store (local.get $k) (local.get $i))
;; Update $k to point to the next memory cell.
(local.set $k (i32.add (local.get $k) (i32.const 4)))
(i32.store (local.get $k) (local.get $j))
(local.set $k (i32.add (local.get $k) (i32.const 4)))
;; Loop that calculates Fibonacci numbers and stores them in memory.
(loop $loop1
;; Calculate the next Fibonacci number and store it in $i.
(local.set $i (i32.add (local.get $i) (local.get $j)))
;; Check if the new Fibonacci number is less than or equal to our limit.
(if (i32.le_s (local.get $i) (local.get $limit))
(then
;; If yes, store it in memory.
(i32.store (local.get $k) (local.get $i))
;; Update $k to point to the next memory cell.
(local.set $k (i32.add (local.get $k) (i32.const 4)))
;; Update $j to hold the last computed Fibonacci number.
(local.set $j (local.get $i))
;; 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 $k) (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:
wat2wasm fib.watwasm2wat fib.wasmwasm2c fib.wasmwat-desugar fib.watwasm-stats fib.wasmPalindrome 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
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.
cwrapThe 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.
embindEmbind 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.
WasmFiddle is a great online tool for experimenting with Wasm. It allows you to write Wasm code in C. It also allows you to write JavaScript code that can call Wasm functions. You have log functions and you can even draw on a HTML Canvas. The tool is 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/net7.0/
wasmtime ./bin/Debug/net7.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:
dotnet run --urls http://*:8080 to make them accessible from outside of the container.(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
spin new http-rust hello_spinspin buildls -la target/wasm32-wasi/release/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).
Content type
Image
Digest
sha256:aaf9a4b3c…
Size
2.4 GB
Last updated
about 2 years ago
docker pull rstropek/wasm-workshop