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.
Was WebAssembly ist und warum Rust so gut passt
Abschnitt betitelt „Was WebAssembly ist und warum Rust so gut passt“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-bindgenschreibt den Verbindungscode zwischen Rust und JavaScript für dich, undwasm-packbaut alles mit einem einzigen Befehl. - Sicherheit und Geschwindigkeit. Du bekommst dieselbe Speichersicherheit und Performance wie auf dem Desktop.
Die Werkzeuge einrichten
Abschnitt betitelt „Die Werkzeuge einrichten“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“:
rustup target add wasm32-unknown-unknownInstalliere 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:
# Installer für Linux / macOS von der wasm-pack-Websitecurl https://wasm-bindgen.github.io/wasm-pack/installer/init.sh -sSf | sh
# oder auf jedem System, auf dem Rust installiert istcargo install wasm-pack --lockedEine Rust-Bibliothek für Wasm schreiben
Abschnitt betitelt „Eine Rust-Bibliothek für Wasm schreiben“Ein Wasm-Modul ist eine Bibliothek, kein Programm mit main. Leg eine an:
cargo new --lib hello-wasmcd hello-wasmBearbeite 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.
Die Logik zuerst nativ testen
Abschnitt betitelt „Die Logik zuerst nativ testen“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 = 5Wenn 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.)
Mit wasm-pack bauen
Abschnitt betitelt „Mit wasm-pack bauen“Führe im Ordner des Crates aus:
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.jsonDer 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.
Aus JavaScript aufrufen
Abschnitt betitelt „Aus JavaScript aufrufen“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:
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 = 5Diesen 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]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.
Die .wasm-Datei klein halten
Abschnitt betitelt „Die .wasm-Datei klein halten“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 --deveine.wasm-Datei mit 58.591 Bytes; der normale Release-Build hatte 21.530 Bytes. (Gemessen auf unserem Rechner mit abgeschaltetemwasm-opt– deine Zahlen werden abweichen.) - Lass wasm-pack
wasm-optausführen. Im Release-Modus lässt wasm-pack standardmäßig den Optimiererwasm-optvon 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 = truecodegen-units = 1Sei 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.
Wo Wasm in der Praxis eingesetzt wird
Abschnitt betitelt „Wo Wasm in der Praxis eingesetzt wird“Ein paar Beispiele, jeweils mit einem Bericht der Firma selbst (auf Englisch):
- Figma kompiliert seinen in C++ geschriebenen Design-Editor nach WebAssembly und berichtet, dass sich die Ladezeit dadurch auf ein Drittel verkürzt hat. (Kein Rust, aber es zeigt, was Wasm in einer großen Anwendung leisten kann.)
- Cloudflare Workers unterstützt Rust über das Crate
workers-rs: Dein Rust-Code wird nach Wasm kompiliert und läuft auf Cloudflares Servern auf der ganzen Welt. - Shopify Functions sind WebAssembly-Module, mit denen man Shops anpasst. Shopify empfiehlt Rust als die performanteste Sprache dafür.
- Die Browser-Erweiterung von 1Password hat das Ausfüllen von Seiten auf WebAssembly umgestellt und berichtet, dass es seitdem mindestens doppelt so schnell läuft.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“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.
Nächste Schritte: ganze Web-Apps in Rust
Abschnitt betitelt „Nächste Schritte: ganze Web-Apps in Rust“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.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- 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-unknownhinzu und installierewasm-pack. - Markiere Funktionen mit
#[wasm_bindgen], setzecrate-type = ["cdylib", "rlib"]und baue mitwasm-pack build --target web. - In JavaScript zuerst
await init(), dann deine Funktionen aufrufen; liefere die Seite überhttp://aus, nicht überfile://. - 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
Abschnitt betitelt „Kurzer Check“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
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Von Rust nach WebAssembly kompilieren – MDN: ein Schritt-für-Schritt-Tutorial, das ein wasm-pack-Projekt baut und ausführt.
- The wasm-bindgen Guide (auf Englisch): alle unterstützten Typen, wie du Browser-APIs aufrufst, und viele Beispiele.
- wasm-pack-Dokumentation (auf Englisch): alle Optionen von
build, Targets und Profile. - wasm32-unknown-unknown – The rustc book (auf Englisch): was dieses Target unterstützt und wo seine Grenzen liegen.
- WebAssembly auf MDN: was Wasm ist und wie Browser es ausführen, unabhängig von Rust.