Neue Möglichkeiten im Browser dank WebAssembly

22.09.2021Ricky Elfner
Cloud WebAssembly Rust Tutorial Hands-on

Bei dem Thema WebAssembly handelt es sich um Code, welcher speziell für den Browser kompiliert wurde. Dieser liegt in einem sehr kompakten Binary-Format vor. Dabei ist WebAssembly eine sehr low-level assembler-artige Sprache. Wasm ist mittlerweile auch von der W3C auch als ein Webstandard festgelegt worden. Von vornherein sollte man auch direkt erwähnen, dass es sich bei Wasm um eine Ergänzung zu JavaScript handelt und nicht als Ersatz dienen soll. Da es als Ergänzung zählt, ist es möglich, innerhalb von JavaScript-Applikationen WebAssembly Module zu laden und deren Funktionen zu nutzen. Ebenfalls ist dies auch andersrum möglich. Somit nutzt man hier die Flexibilität von JavaScript und die Performance, die Wasm einem bietet. Aus diesem Grund ermöglicht diese Kombination Applikationen browserfähig zu bekommen, bei denen es zuvor nicht möglich war.

Um Wasm zu nutzen, gibt es verschiedene Möglichkeiten, wie zum Beispiel wasm-Code select schreiben oder vorhandenen Code in wasm-Code zu kompilieren. Dazu jedoch später mehr.

Früher hat die Rechenleistung von JavaScript alleine gereicht. Heutzutage reicht dies durch Applikationen im Bereich von 3D-Games, Virtual oder Augmented Reality, Image/Video Editing nicht mehr. Dieses Problem soll mit der Verwendung von Wasm behoben werden. Zu den Vorteilen, die beide Welten bieten gehören vor alle:

  • bei JavaScript:

    • sehr flexible, um Webanwendungen zu schreiben

    • benötigt kein vorheriges Compiling

    • grosses Ecosystem mit sehr vielen Frameworks, Libraries und anderen Tools

  • bei WebAssembly:

    • sehr kompaktes binary-Format

    • ist eine sehr low-level assembler-artige Sprache

Dabei wird von der Browser VM nun zwei Typen von Code geladen — JavaScript und WebAssembly. Die WebAssembly JavaScript API macht es erst möglich, Funktionen aus dem wasm-Code aufzurufen. Dies ist möglich, in dem die API den wasm-Code quasi umschliesst. Auch das Aufrufen von JavaScript-Funktionen innerhalb von wasm-Code ist kein Problem.

Ziele

WebAssembly definiert dabei vor allem diese vier Ziele:

  • Be fast, efficient, and portable

    • wasm-Code kann nahezu in native Geschwindigkeit ausgeführt werden unabhängig von der Platform
  • Be readable and debuggable

    • obwohl es eine low-level Assembler-artige Sprache ist, gibt es ein human-readable Text Format. Dadurch besteht die Möglichkeit Code selbst zu schreiben, ihn zu lesen und auch zu debuggen
  • Keep secure

    • wasm wird in einer sicheren Sandbox Environment ausgeführt. Dabei übernimmt es dieselben same-origin und Permission-Policies wie der Browser
  • Don’t break the web

    • ist dafür ausgelegt, dass wasm abwärtskompatibel ist und somit ohne Probleme mit anderen Web Technologien zusammen arbeiten kann.

Key Konzepte

Wenn man WebAssembly in seiner Applikation nutzen möchte, gibt es zunächst einmal die folgenden Key-Konzepte, die man wissen sollte, um den Aufbau zu verstehen.

Module

Ein Module stellt ein WebAssembly Binary dar, welches durch den Browser zu ausführbaren Maschinencode compiled wurde. Dabei ist ein Module, genauso wie ein Blob, stateless und kann zwischen Windows und Workers geteilt werden.

Memory

Bei dem Speicher handelt es sich um ein resizable ArrayBuffer.

Table

Eine Tabelle ist ein resizable Typed Array, welches Referenzen (bsp. Funktionen) enthält. Dabei können diese aus Sicherheitsgründen nur als Bytes gespeichert werden.

Instanz

Sobald es eine Kombination aus einem Module, States des Speichers, Tabellen und importierten Variablen gibt, spricht man von einer Instanz.

Das Einbinden

Leider gibt es noch nicht die Möglichkeit, reinen wasm-Code einfach über ein import-Statement oder über das Html-Tag <script type='module'> zu machen. Die gängigsten zwei Möglichkeiten sind einmal über Fetch API oder über einen XMLHttpRequest.

Fetch

Der schnellste und auch effektivste Weg ist über die Funktion WebAssembly.instantiateStreaming(). Dabei nimmt diese als ersten Parameter die wasm-Datei entgegen. In unserem Beispiel wird diese Datei über den Fetch-Befehl geladen. Optional kann man einen weiteren Parameter angeben, welche Werte für die neue Instanz bereitstellt.

1
2
3
4
WebAssembly.instantiateStreaming(fetch('bnvoaExample.wasm'), importObject)
.then(results => {
  // Do something with the results!
});

Als Response bekommt man ein Promise zurück, welches als ResultObject aufgelöst wird und zwei Felder enthält. Das erste ist das module, welches das kompilierte WebAssembly Module darstellt. Dieses kann wiederum noch einmal instanziiert werden oder über die postMessage() geteilt werden. Das zweite Feld ist instance, welches alle exportieren WebAssembly Funktionen beinhaltet.

XMLHttpRequest

Dies ist eine etwas ältere Variante, wird jedoch immer noch supportet. Dabei muss zuerst ein XMLHttpRequest() erstellt werden. In der open()-Funktion muss nun die Request-Methode bestimmt werden, sowie der Name des wasm-Codes. Anschliessend ist es wichtig, den responseType auf arrrayBuffer festzulegen. Nun kann der Request gesendet werden. In einem weiteren Schritt kann dann gleich wie oben auf das ResultObjekt zugegriffen werden.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
request = new XMLHttpRequest();
request.open('GET', 'bnvoaExample.wasm');
request.responseType = 'arraybuffer';
request.send();

request.onload = function() {
  var bytes = request.response;
  WebAssembly.instantiate(bytes, importObject).then(results => {
    // Do something with the results!
  });
};

Konvertieren

Wie oben erwähnt gibt es zusätzlich zu dem Binary-Format (.wasm) noch ein human-readable-Format (.wat). Falls Sie nur eines der beiden Formate haben können Sie dieses mit dem Tool wabt in das andere Format konvertieren.

Dafür installieren Sie zunächst einmal wabt:

1
brew install wabt

Nun stehen ihnen zwei tools zur Verfügung um Ihren Code zu konvertieren: wasm2wat und wat2wasm.

1
2
3
wat2wasm test.wat -o test.wasm

wasm2wat test.wasm -o test.wat

Das Verwenden

Nun zeigen wir Ihnen, wie Sie wasm-Code selbst erstellen können. Da wir uns letzteWoche mit dem Thema Rust beschäftigt haben, möchten wir Ihnen nun auch zeigen, wie Sie eine Rust Applikation in wasm-Code konvertieren können, damit Sie diesen in ihrem Browser verwenden können.

Falls Sie Rust noch nicht installiert haben, können Sie dies hier tun. Als Nächstes müssen Sie über den Packagemanager cargo das wasm-pack installieren. Mithilfe dieses Package ist es möglich, Ihren Code zu wasm-Code zu kompilieren. Ebenso stellt es das korrekte Packaging bereit, um es innerhalb des Browsers zu verwenden.

1
cargo install wasm-pack

Falls Sie noch keine Applikationen in Rust haben, die Sie kompilieren wollen, müssen Sie sich zunächst einmal ein Rust-Projekt erstellen.

1
2
 cargo new --lib hello-bnova
     Created library `hello-bnova` package

Anschliessend sollten Sie folgende Ordner Struktur haben.

1
2
3
4
5
.
└── hello-bnova
    ├── Cargo.toml
    └── src
        └── lib.rs

Um die Funktionalität von WebAssembly aufzuzeigen, werden wir zwei unterschiedliche Funktionen hinzufügen. Einmal das Aufrufen von JS-Code innerhalb von Rust-Code und auch andersrum. Dazu müssen Sie in unserem Fall das lib.rs File wie folgt anpassen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern {
    pub fn alert(s: &str);
}

#[wasm_bindgen]
pub fn greet(name: &str) {
    alert(&format!("Hello, {}!", name));
}

Auf Zeile 1 wird das wasm_bindgen::prelude Modul importiert. Dieses wird dafür zuständig sein, um zwischen JS und Rust zu kommunizieren.

Bei dem Code von Zeile 3 bis Zeile 6 handelt es sich um den Aufruf der JavaScript-Funktion alert(). Der Code von Zeile 8 bis Zeile 10 zeigt wiederum die Möglichkeit aus JavaScript diese Rust Funktion aufzurufen.

Um nun den Code auch zu WemAssembly zu kompilieren müssen Sie ihr Cargo.toml noch nach folgendem Muster anpassen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[package]
name = "hello-bnova"
version = "0.1.0"
authors = ["Ricky Elfner ricky.elfner@b-nova.com"]
description = "A sample project with wasm-pack"
license = "MIT/Apache-2.0"
edition = "2018"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

Nun sind Sie bereit, dafür das Package zu bilden. Dieser Vorgang kann ein bisschen dauern.

1
wasm-pack build --target web

Bei diesem Vorgang wird Ihr Rust-Code zu WebAssembly kompiliert. Dabei wird ein JavaScript File um das WebAssembly File als Module erstellt, damit der Browser dieses verwenden kann. Des Weiteren wird ein pkg-Ordner erstellt, in den das JavaScript File und das WebAssembly-File verschoben werden. Ebenso wird aus dem Cargo.toml einpassendes package.json erstellt. Sollten Sie bereits ein README.md haben wird auch dieses in diesen Ordner kopiert.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
.
├── Cargo.lock
├── Cargo.toml
├── pkg
│   ├── hello_bnova.d.ts
│   ├── hello_bnova.js
│   ├── hello_bnova_bg.wasm
│   ├── hello_bnova_bg.wasm.d.ts
│   └── package.json
├── src
│   └── lib.rs
└── target

Der Ordner target ist jedoch zu gross um diesen hier abzubilden.

Um nun Ihr Projekt auch im Browser verwenden zu können, müssen Sie ein index.html File erstellen, welches Ihres Script einbindet. Dafür legen Sie in Ihrem root-Verzeichnis folgendes HTML-File an:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>hello-wasm example</title>
  </head>
  <body>
    <script type="module">
      import init, {greet} from "./hello_bnova.js";
      init()
        .then(() => {
          greet("WebAssembly")
        });
      </script>
  </body>
</html>

Nun können Sie einen lokalen Webserver, beispielsweise mit python3 -m http.serverstarten und Ihr index.html über Ihren Browser aufrufen. Dadurch sollten Sie nun ein Hinweisfenster sehen mit der Nachricht “Hello, WebAssembly!”.

Nutzen Sie NPM

Eine weitere Möglichkeit besteht darin, eine Rust-Applikation mittels NPM zu verwenden. Die Voraussetzung hierfür sind einfach, dass Sie Node.js und Npm installiert haben. Hier für wechseln Sie zunächst ihn Ihr root-Verzeichnis ihrer Applikation und rufen folgenden Befehl auf:

1
wasm-pack build --target bundler

Sie erhalten folgende Ausgabe:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
pwd
/Users/relfner/Development/techhub/wasm/hello-bnova
Ricky-MBP:hello-bnova relfner$ wasm-pack build --target bundler
[INFO]: 🎯  Checking for the Wasm target...
[INFO]: 🌀  Compiling to Wasm...
    Finished release [optimized] target(s) in 0.04s
[WARN]: ⚠️   origin crate has no README
[INFO]: License key is set in Cargo.toml but no LICENSE file(s) were found; Please add the LICENSE file(s) to your project directory
[INFO]: ⬇️  Installing wasm-bindgen...
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional field missing from Cargo.toml: 'repository'. This is not necessary, but recommended
[INFO]: ✨   Done in 0.55s
[INFO]: 📦   Your wasm pkg is ready to publish at /Users/relfner/Development/techhub/wasm/hello-bnova/pkg.

Um nun dieses Package auch für andere JavaScript Package verfügbar zu machen, müssen Sie einmal in den pkg-Ordner wechseln und ein npm link ausführen.

1
2
cd pkg
npm link

Nun haben Sie ein npm Package, welches in Rust geschrieben wurde aber zu WebAssembly kompiliert wurde.

Nun müssen Sie wieder in das root-Verzeichnis wechseln, einen neuen Ordner anlegen und in diesen wieder wechseln. Durch das Ausführen von npm link wird ein neuer Ordner mit dem Namen node_modules erstellt.

1
2
3
4
cd ..
mkdir site
cd site
npm link hello-bnova

Zusätzlich müssen Sie noch ein package.json und ein webpack.config.js erstellen.

In dem ersten File werden verschiedene Dependencies bestimmt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "scripts": {
    "serve": "webpack-dev-server"
  },
  "dependencies": {
    "hello-bnova": "^0.1.0"
  },
  "devDependencies": {
    "webpack": "^4.25.1",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.10"
  }
}

In dem Webpack Javascript File müssen Sie folgendes hinzufügen

1
2
3
4
5
6
7
8
9
const path = require('path');
module.exports = {
  entry: "./index.js",
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "index.js",
  },
  mode: "development"
};

Nun benötigen Sie noch ein JS-File, welches die Funktion aus ihrem WebAssembly File aufruft.

1
2
3
import("./node_modules/hello-bnova/hello_bnova.js").then((js) => {
  js.greet("WebAssembly with NPM");
});

Da es nun auch notwendig ist, eine HTML-Seite zuhaben, die Ihre zuvor erstelltes JavaScript einbindet, benötigen Sie ebenfalls ein index.html File.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>hello-wasm example</title>
  </head>
  <body>
    <script src="./index.js"></script>
  </body>
</html>

Nun haben Sie fast alle Schritte erledigt, um Ihre Rust-Applikation in Ihrem Browser aufzurufen. Nur noch alle Dependencies installieren und den Server starten.

1
2
npm install
npm run serve

Jetzt können Sie Ihre Seite im Browser aufrufen und bekommen Ihre Nachricht in einer Alter-Box angezeigt.

Nun haben wir Ihnen einmal die Grundlagen gezeigt wie Sie WebAssembly nutzen können. Was jedoch mit der Kombination aus JavaScript und WebAssembly möglich ist und wo es verwendet werden kann wird in nähere Zukunft ein sehr spannendes Thema werden, welches wir bei b-nova auf jeden Fall verfolgen werden.

Ricky Elfner

Ricky Elfner – Denker, Überlebenskünstler, Gadget-Sammler. Dabei ist er immer auf der Suche nach neuen Innovationen, sowie Tech News, um immer über aktuelle Themen schreiben zu können.