Sign inSign up

rstropek/wasm-workshop

By rstropek

•Updated about 2 years ago

Docker Image for WASM Workshop

Image
0

1.1K

rstropek/wasm-workshop repository overview

GitHub Action Badge Docker Badge

⁠Docker Image for WASM Workshop

⁠Image Content

This is a Docker image for exercises in a WASM workshop. It puts together the following tools:

ToolNotes
Build Essentials⁠C Compiler, C Library, etc.
wget, curl, vimJust 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.

⁠How to Use

⁠Arguments

The Docker image accepts the following arguments⁠:

ArgumentDefault Value
base_imageubuntu:jammyThe base image
wasi_sdk20WASI SDK version
dotnet_repo22.04Used .NET repository
dotnet_version7.0Installed .NET version
node_major20Installed Node version
wasm_tools1.0.51Installed Wasm Tools version

Read more about .NET repository version here⁠.

⁠Exercises

⁠Hello World Wasm
⁠With C
#include <stdio.h>

int main() {
    printf("Hello World\n");
    return 0;
}

Compile and run:

$CCWASM hello.c -o hello.wasm
wasmtime hello.wasm
⁠With Rust
  • 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
    
⁠Exercise

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
⁠Introduction to Wasm with WAT
⁠Hello World (Online)

wat2wasm online

⁠Fibonacci

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:

  • 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

Palindrome checker writte with WAT, running in the browser. Sample code can be found here⁠.

⁠Diving Deeper into WAT

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

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⁠.

⁠Using cwrap

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.

⁠Using embind

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.

⁠WasmFiddle

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);
⁠WASI
⁠With C

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⁠.

⁠With Rust

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⁠.

⁠.NET with Wasm/WASI
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!

⁠Blazor

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.
⁠Wasm Component Model
⁠WAT
(module
  (func $add (param $lhs i32) (param $rhs i32) (result i32)
      local.get $lhs
      local.get $rhs
      i32.add)
  (export "add" (func $add))
)
⁠WIT
package example:component;

world example {
    export add: func(x: s32, y: s32) -> s32;
}
⁠Build Wasm Component
wasm-tools component embed add.wit add.wat -o add.wasm
wasm-tools component new add.wasm -o add.component.wasm
⁠Rust Host

See wasm-component⁠

⁠Fermyon Spin
⁠Hello World
  • 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)
⁠Larger Example

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).

Tag summary

Content type

Image

Digest

sha256:aaf9a4b3c…

Size

2.4 GB

Last updated

about 2 years ago

docker pull rstropek/wasm-workshop