C und Rust im Browser mittels WebAssembly
Im letzten Artikel zum Thema WebAssembly haben wir uns Python im Browser angeschaut.
Doch wie bekommt man Low-Level Sprachen wie C in den Browser? Das ist einfacher, als man vielleicht glauben mag: Man kann aus C (oder Rust, Go, C++,…) mit Standard-Toolchains in Richtung Low Level Virtual Machine kompilieren. Dies - und das ist der eigentliche "Trick" - muss dann nur noch ins WebAssembly-Format übersetzt werden. Dafür stellt emscripten eine passende Toolchain (von Quellcode über LLVM nach WebAssembly) zur Verfügung.
Diesen Vorgang beschreiben wir im Folgenden anhand eines einfachen "Hallo, Welt!" Beispiels - hier basierend auf C:
#include<stdio.h>
int main() {
printf("Hello, Welt!\n");
return 0;
}
emscripten
Zunächst installieren und aktivieren wir emscripten
, um auf die C-Toolchain zugreifen zu können.
$ git clone https://github.com/emscripten-core/emsdk.git
$ cd emsdk
$ ./emsdk install latest
$ ./emsdk activate latest
$ . ./emsdk_env.sh
Kompilation nach WebAssembly
Die Kompilation erfolgt mittels emcc
:
$ emcc hello-world.c -s WASM=1 -o hello-world.html
Damit wird neben dem WebAssembly-Binary hello-world.asm
auch Beispiel-Code HTML-Code hello-world.html
und - viel wichtiger - JavaScript-Code hello-world.c
zum Lesen und Instantiieren des WebAssembly-Binaries erzeugt.
Diesen kann man dann direkt ausprobieren, z.B. mittels Python:
$ python -m SimpleHTTPServer 8000 &
$ x-www-browser http://localhost:8000/hello-world.html
Natürlich kann man die Beispiel-HTML auch weg lassen:
$ emcc hello-world.c -s WASM=1 -o hello-world.js
Nur das Binary:
$ emcc hello-world.c -s WASM=1 -o hello-world.wasm
Ein (kurzer) Blick unter die JavaScript-Haube
Um zu verstehen, wie das WebAssembly-Binary in den Browser gelangen kann, lohnt der Blick in den Beispiel-Code:
Die HTML-Datei birgt kaum Überraschungen. Hier wird lediglich eine Textarea für den vom WebAssembly generierten Standardausgaben-Text spezifiziert und JavaScript nachgeladen:
Im generierten hello-world.js
wird geprüft, ob der Browser WebAssembly-fähig ist. Falls dies
der Fall ist, erfolgt der Zugriff mittels eines WebAssembly
Objektes:
var wasmBinary;
if (Module['wasmBinary']) wasmBinary = Module['wasmBinary'];legacyModuleProp('wasmBinary', 'wasmBinary');
var noExitRuntime = Module['noExitRuntime'] || true;legacyModuleProp('noExitRuntime', 'noExitRuntime');
if (typeof WebAssembly != 'object') {
abort('no native wasm support detected');
}
Das WASM-Binary muss nun gelesen werden, idealerweise asynchron:
var wasmBinaryFile;
wasmBinaryFile = 'hello-world.wasm';
if (!isDataURI(wasmBinaryFile)) {
wasmBinaryFile = locateFile(wasmBinaryFile);
}
...
function instantiateAsync() {
if (!wasmBinary &&
typeof WebAssembly.instantiateStreaming == 'function' &&
!isDataURI(wasmBinaryFile) &&
!isFileURI(wasmBinaryFile) &&
!ENVIRONMENT_IS_NODE &&
typeof fetch == 'function') {
return fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function(response) {
/** @suppress {checkTypes} */
var result = WebAssembly.instantiateStreaming(response, info);
return result.then(
receiveInstantiationResult,
function(reason) {
err('wasm streaming compile failed: ' + reason);
err('falling back to ArrayBuffer instantiation');
return instantiateArrayBuffer(receiveInstantiationResult);
});
});
}
...
}
Neben dem JavaScript-Boilerplatecode ist dabei
WebAssembly.instantiateStreaming(response, info)
zentral. Diese Methode
kümmert sich um das asynchrone Auslesen und Instantiieren des WebAssemblies.
Genauer formuliert - der Aufruf erfolgt wie beschrieben asynchron - liefert die Methode ein Promise zurück,
das neben einem WebAssembly-Modul
(damit können neue WebAssembly-Instanzen generiert werden), die
WebAssembly.Instance
beinhaltet, die die eigentliche Funktion ausführt - in unserem Fall das Hallo, Welt!
.
WebAssembly mit Rust
Wie beschrieben kann man alternativ zu Python und C auch andere Sprachen nutzen, z.B. die Multiparadigmensprache Rust.
Die Installation von Rust inklusive dessen Paketierungswerkzeug Cargo sowie die Installation des WebAssembly-Paketes gestalten sich einfach:
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
$ . "$HOME/.cargo/env"
$ cargo install wasm-pack
Anschließend generieren wir den Rumpf unserer Beispielanwendung in einem Projektverzeichnis:
$ mkdir hello-rust
$ cd hello-rust
$ cargo init
Die Projektspezifikation erfolgt in der generierten Cargo.toml
:
[package]
name = "hallo-welt"
version = "0.0.1"
authors = ["Peter Lustig <peterle@wolke7.himmel>"]
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2"
Das Beispielprogramm inklusive Javascript-Bindung legen wir in src/lib.rs
an:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn gen_answer() -> i32 {
return 42;
}
Nun geht es an das Bauen. Es werden in das Verzeichnis pkg
neben dem WebAssembly-Binary auch Bindings, u.a.
für JavaScript generiert:
$ wasm-pack build --target web
Jetzt noch etwas Web-Kodierung (index.js
und index.html
) und fertig ist mit dem
Aufruf von halloWelt.gen_answer()
und der DOM-Body-Modifikation (document.body.textContent
) die
WebAssembly-Integration!
import init from "./pkg/hallo_welt.js";
const runWasm = async () => {
const halloWelt = await init("./pkg/hallo_welt_bg.wasm");
const val = halloWelt.gen_answer();
document.body.textContent = `Hallo Welt: ${val}`;
};
runWasm();
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>WebAssembly und Rust</title>
<script type="module" src="./index.js"></script>
</head>
<body></body>
</html>
Vergleich C und Rust
Rust punktet mit seinem modernen, auf Performanz und auf sichere Nebenläufigkeit ausgerichteten Sprachkonzepten sowie mit seinem integrierten Paketierungssystem samt stetig breiter werdender Library-Basis; in unserem Kontext mit einer guten WebAssembly-Integration.
Allerdings darf man bei der Bewertung nicht außer Acht lassen, das gemäß TIOBE-Index die Sprache C noch immer auf Platz 2, Rust dagegen im hinteren Feld (Platz 26) liegt. Hier wird die extrem breite Werkzeug- und Library-Basis sowie das verfügbare Know-How für C ausschlaggebend sein.
Unsere kleinen Beispiele geben (hoffentlich) eine Idee, wie Low-Level Code in den Browser gelangen kann. Aber es gibt noch viel mehr zu entdecken! Dazu in kommenden Artikeln mehr…
Die im Artikel verwendeten Quelldateien finden Sie hier.
Zu den Themen Python und Webtechnologien bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:
Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.