Neuigkeiten von trion.
Immer gut informiert.

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.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Zur Desktop Version des Artikels