Zum Inhalt springen

Rust und WebAssembly

Mit WebAssembly (oft kurz Wasm) kannst du Code aus Sprachen wie Rust im Webbrowser ausführen, direkt neben JavaScript. Stell es dir wie einen zweiten Motor im Browser vor: JavaScript steuert weiterhin die Seite. Aber wenn schwere Arbeit ansteht – Bildfilter, Physik, große Dateien parsen –, kannst du sie an schnellen, kompilierten Rust-Code abgeben.

Auf dieser Seite schreibst du eine winzige Rust-Bibliothek, kompilierst sie nach Wasm und rufst sie von einer Webseite aus auf. Nichts davon läuft im Rust Playground (dafür braucht es einen Browser und zusätzliche Werkzeuge). Die meisten Beispiele sind deshalb einfache Codeblöcke für deinen eigenen Rechner. Die reinen Rust-Teile kannst du aber sofort ausprobieren.

WebAssembly ist ein kompaktes Binärformat für Programme. Jeder große Browser kann es ausführen – nahezu in nativer Geschwindigkeit und in derselben Sicherheits-Sandbox wie JavaScript. Und es ist nicht nur etwas für Browser: Auch Server und Edge-Plattformen führen Wasm aus.

Rust ist eine der beliebtesten Sprachen für Wasm, und zwar aus mehreren Gründen:

  • Kein Garbage Collector und keine große Laufzeitumgebung. Rust muss keine zusätzliche Maschinerie mit deinem Code ausliefern, deshalb können .wasm-Dateien klein sein.
  • Hervorragende Werkzeuge. wasm-bindgen schreibt den Verbindungscode zwischen Rust und JavaScript für dich, und wasm-pack baut alles mit einem einzigen Befehl.
  • Sicherheit und Geschwindigkeit. Du bekommst dieselbe Speichersicherheit und Performance wie auf dem Desktop.

Sag Rust zuerst, dass es für das Wasm-Target kompilieren darf. Ein Target ist „die Art von Maschine, auf der der Code laufen wird“. wasm32-unknown-unknown bedeutet „32-Bit-Wasm, kein bestimmtes Betriebssystem“:

Terminal window
rustup target add wasm32-unknown-unknown

Installiere als Nächstes wasm-pack. Es kompiliert dein Crate, ruft wasm-bindgen auf und legt alles, was du brauchst, in einen Ordner. Nimm entweder den offiziellen Installer oder baue es mit Cargo:

Terminal window
# Installer für Linux / macOS von der wasm-pack-Website
curl https://wasm-bindgen.github.io/wasm-pack/installer/init.sh -sSf | sh
# oder auf jedem System, auf dem Rust installiert ist
cargo install wasm-pack --locked

Ein Wasm-Modul ist eine Bibliothek, kein Programm mit main. Leg eine an:

Terminal window
cargo new --lib hello-wasm
cd hello-wasm

Bearbeite dann die Cargo.toml. Die Zeile crate-type sagt Cargo, dass es eine Wasm-taugliche Bibliothek (cdylib) erzeugen soll. wasm-bindgen ist das Crate, das Rust und JavaScript verbindet:

[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"

Schreib jetzt zwei Funktionen in src/lib.rs. Das Attribut #[wasm_bindgen] markiert jede davon als „aus JavaScript aufrufbar“:

use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hallo, {name}! Dieser Text wurde in Rust gebaut.")
}

Mehr Rust brauchst du nicht. Es sind ganz normale Funktionen: wasm-bindgen kümmert sich darum, einen JavaScript-String in einen &str umzuwandeln und den zurückgegebenen String wieder in einen JavaScript-String.

Diese Angewohnheit spart dir viel Zeit: Halte die eigentliche Arbeit in normalen Rust-Funktionen und teste sie auf deinem gewohnten Rechner mit cargo test oder einem schnellen main. Debuggen im Browser ist mühsamer als im Terminal. Dieselbe Logik läuft ohne das Attribut überall:

fn add(a: i32, b: i32) -> i32 {
a + b
}
fn greet(name: &str) -> String {
format!("Hallo, {name}! Dieser Text wurde in Rust gebaut.")
}
fn main() {
println!("{}", greet("Ferris"));
println!("2 + 3 = {}", add(2, 3));
}

Ausgabe:

Hallo, Ferris! Dieser Text wurde in Rust gebaut.
2 + 3 = 5
▶ Selbst ausprobieren

Wenn die Logik stimmt, ist die #[wasm_bindgen]-Hülle nur noch eine dünne Tür zum Browser. (Tests mit #[test] laufen ebenfalls nativ mit cargo test – siehe Testen.)

Führe im Ordner des Crates aus:

Terminal window
wasm-pack build --target web

--target web bedeutet: „Erzeuge Dateien, die ein Browser direkt mit einem <script type="module">-Tag laden kann“ – ganz ohne Bundler wie webpack. Bei uns enthielt der neue Ordner pkg/ danach:

pkg/
├── hello_wasm_bg.wasm der kompilierte Rust-Code
├── hello_wasm.js JavaScript-Verbindungscode, der ihn lädt und jede Funktion verpackt
├── hello_wasm.d.ts TypeScript-Typen für deine Funktionen
├── hello_wasm_bg.wasm.d.ts
└── package.json

Der Bindestrich in hello-wasm ist in den Dateinamen zu einem Unterstrich geworden. Standardmäßig erzeugt wasm-pack build einen Release-Build (optimiert). Mit --dev bekommst du beim Experimentieren einen schnelleren, nicht optimierten Build.

Leg index.html neben die Cargo.toml (sodass pkg/ daneben liegt):

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Hallo aus Rust</title>
</head>
<body>
<p id="message">Wird geladen…</p>
<script type="module">
import init, { add, greet } from "./pkg/hello_wasm.js";
await init(); // die .wasm-Datei herunterladen und starten
const text = greet("Ferris");
const sum = add(2, 3);
document.getElementById("message").textContent = `${text} 2 + 3 = ${sum}`;
</script>
</body>
</html>

Der Default-Export init() lädt die .wasm-Datei herunter und startet sie. Danach sind add und greet ganz normale JavaScript-Funktionen.

Browser laden keine Module von einer file://-Adresse, deshalb brauchst du einen kleinen lokalen Webserver. Wenn du Python installiert hast, geht es so am schnellsten:

Terminal window
python3 -m http.server 8080

Öffne http://localhost:8080 in deinem Browser. Bei uns zeigte die Seite:

Hallo, Ferris! Dieser Text wurde in Rust gebaut. 2 + 3 = 5

Diesen Satz hat Rust gebaut – und zwar direkt im Browser. 🦀

Was die Grenze günstig überquert (und was nicht)

Abschnitt betitelt „Was die Grenze günstig überquert (und was nicht)“

Rust und JavaScript teilen sich den Speicher nicht auf dieselbe Weise. Das Wasm-Modul hat seinen eigenen Speicherblock, und Werte müssen zwischen beiden Seiten hin- und herreisen. Manche Reisen sind kostenlos, andere kosten eine Kopie:

Typ in Rust In JavaScript Kosten
i32, u32, f64, bool number, boolean Günstig: wird direkt übergeben
&str, String string Eine Kopie plus Umwandlung von UTF-16 nach UTF-8 (oder zurück)
&[u8], Vec<u8> Uint8Array Eine Kopie in den Wasm-Speicher (oder heraus)
&mut [u8] Uint8Array Hineinkopiert und nach dem Aufruf zurückkopiert

Die goldene Regel lautet also: Mach wenige Aufrufe, die jeweils viel Arbeit erledigen. Rust einmal aufzurufen, um ein ganzes Bild zu bearbeiten, ist schnell. Rust eine Million Mal aufzurufen, einmal pro Pixel, verbringt die meiste Zeit mit dem Überqueren der Grenze.

So sieht das aus. Diese Funktion hellt einen ganzen Puffer mit Pixelwerten in einem einzigen Aufruf auf:

use wasm_bindgen::prelude::*;
/// Jedes Pixel heller machen. JS übergibt ein Uint8Array.
#[wasm_bindgen]
pub fn brighten(pixels: &mut [u8], amount: u8) {
for p in pixels.iter_mut() {
*p = p.saturating_add(amount);
}
}

In JavaScript würdest du nach await init() einfach brighten(pixels, 20) mit einem Uint8Array aufrufen, und das Array wird direkt verändert. Die Schleife selbst ist reines Rust, also kannst du sie nativ testen:

fn brighten(pixels: &mut [u8], amount: u8) {
for p in pixels.iter_mut() {
// saturating_add bleibt bei 255 stehen, statt überzulaufen
*p = p.saturating_add(amount);
}
}
fn main() {
let mut pixels = [10, 100, 250];
brighten(&mut pixels, 20);
println!("{pixels:?}");
}

Ausgabe:

[30, 120, 255]
▶ Selbst ausprobieren

Aus 250 + 20 wurde 255 – kein Absturz und kein Überlauf auf einen kleinen Wert: saturating_add bleibt beim Maximum stehen. Genau das willst du bei Farben.

Jedes Byte deiner .wasm-Datei muss heruntergeladen werden, bevor die Seite sie benutzen kann. Ein paar Tipps:

  • Liefere immer den Release-Build aus. Für unser winziges Beispiel erzeugte wasm-pack build --dev eine .wasm-Datei mit 58.591 Bytes; der normale Release-Build hatte 21.530 Bytes. (Gemessen auf unserem Rechner mit abgeschaltetem wasm-opt – deine Zahlen werden abweichen.)
  • Lass wasm-pack wasm-opt ausführen. Im Release-Modus lässt wasm-pack standardmäßig den Optimierer wasm-opt von Binaryen über die Ausgabe laufen, was sie meist noch weiter verkleinert.
  • Schalte auf deinem Server Komprimierung ein. Unsere 21.530-Byte-Datei hatte nach gzip noch 8.464 Bytes.
  • Probiere Einstellungen für kleine Dateien aus und miss danach:
[profile.release]
opt-level = "z" # auf Größe optimieren ("s" ist eine mildere Variante)
lto = true
codegen-units = 1

Sei ehrlich zu dir und miss nach. Bei unserem Beispiel mit zwei Funktionen haben diese Einstellungen nicht geholfen – die Datei hatte danach 22.817 Bytes, war also sogar etwas größer. Bei größeren Crates lohnen sie sich eher. Auch Dinge wie format! und Panic-Meldungen ziehen Code nach sich. Ein Crate, das darauf verzichtet, kann deshalb viel kleiner sein.

Ein paar Beispiele, jeweils mit einem Bericht der Firma selbst (auf Englisch):

crate-type = ["cdylib"] vergessen. Ohne diese Zeile bricht wasm-pack sofort ab. Zum Glück sagt dir die Fehlermeldung genau, was du ergänzen musst:

Error: crate-type must be cdylib to compile to WebAssembly. Add the following to your Cargo.toml file:
[lib]
crate-type = ["cdylib", "rlib"]

Die Meldung heißt: Der crate-type muss cdylib sein, damit nach WebAssembly kompiliert werden kann – füge die folgenden Zeilen zu deiner Cargo.toml hinzu.

index.html per Doppelklick öffnen. Die Seite bleibt bei „Wird geladen…“ hängen, und die Browser-Konsole (in Chrome) meldet:

Access to script at 'file:///…/pkg/hello_wasm.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome, chrome-extension, chrome-untrusted, data, http, https, isolated-app.

Der Browser blockiert also das Skript, weil Anfragen über verschiedene Ursprünge (CORS) nur über Protokolle wie http und https erlaubt sind – nicht über file://. Die Lösung: Stell den Ordner über einen lokalen Webserver bereit, etwa mit python3 -m http.server, und öffne stattdessen die Adresse http://localhost.

Eine Funktion vor await init() aufrufen. Das Wasm-Modul ist dann noch nicht geladen, also hat der Verbindungscode nichts, was er aufrufen könnte. Als wir die Zeile mit init() entfernt haben, zeigte Chrome Cannot read properties of undefined (reading '__wbindgen_free'). Ruf also immer zuerst await init() auf.

Ein paar Rust-Funktionen aus JavaScript aufzurufen, ist ein toller Anfang. Wenn du die ganze Web-App in Rust bauen willst, schau dir diese Frameworks an. Beide kompilieren nach Wasm und lassen dich Komponenten in Rust schreiben, ein bisschen wie in React:

  • Leptos (auf Englisch): ein schnelles, modernes Framework, das Seiten auch auf dem Server rendern kann.
  • Yew (auf Englisch): ein ausgereiftes, komponentenbasiertes Framework.
  • WebAssembly ist ein schnelles, sicheres Binärformat, das Browser (und Server) ausführen können. Rust lässt sich gut dorthin kompilieren, weil es keinen Garbage Collector hat.
  • Füge das Target mit rustup target add wasm32-unknown-unknown hinzu und installiere wasm-pack.
  • Markiere Funktionen mit #[wasm_bindgen], setze crate-type = ["cdylib", "rlib"] und baue mit wasm-pack build --target web.
  • In JavaScript zuerst await init(), dann deine Funktionen aufrufen; liefere die Seite über http:// aus, nicht über file://.
  • Zahlen überqueren die Grenze günstig, Strings und Slices werden kopiert – also lieber wenige, große Aufrufe.
  • Liefere Release-Builds mit Komprimierung aus und miss nach, bevor du Größen-Tricks vertraust.

Kurzer Check

1. Mit welchem Befehl fügst du das Rust-Target für WebAssembly im Browser hinzu?

2. Was bewirkt das Attribut #[wasm_bindgen]?

3. Deine Seite bleibt bei „Wird geladen…“ hängen, und die Konsole zeigt einen CORS-Fehler für eine file:///-Adresse. Was hilft?

4. Was lässt sich am günstigsten von JavaScript an Rust übergeben?

Punkte: 0 / 4