Web

Hallo, Welt als WebAssembly

Mit WebAssembly können performante Web-Anwendungen für alle gängigen Browser realisiert werden. Mit dem WebAssembly Text Format kann auf unterster Ebene eine WASM programmiert und mit den dazugehörigen Tools kompiliert werden.

Einblick in WASM und das WAT Format
Ryoji Iwata

WebAssembly (WASM) ist eine Technologie zur Erstellung von sehr performanten Anwendungen, die in der Regel im Browser laufen. Die Erstellung von WASM-Anwendung kann mit verschiedenen Programmiersprachen erfolgen. Eine WASM kann in zwei Formaten vorliegen, binär oder als Text. Letzteres wird als WAT (WebAssembly Text Format) bezeichnet. WebAssembly ist dabei eine Sprache mit einem sehr niedrigen Niveau (low-level). Deshalb wird man vermutlich sehr selten eine WASM von Hand in WAT erstellen und vielmehr auf eine der höheren Programmiersprachen setzen. Dennoch habe ich mir WAT mal angeschaut, um ein besseres Verständnis dafür zu bekommen. Diese Erfahrung möchte ich hier anhand zweier kleiner Beispiele zeigen.

Eine in WASM geschriebene Funktion aufrufen

Als erstes Beispiel soll wieder einmal die Antwort auf alle Fragen ermittelt werden. Hierzu erstellen wir eine WebAssembly, die eine Funktion get_answer() nach außen zur Verfügung stellt, die bei Aufruf 42 zurückliefert. Folgender Code ist hierfür nötig:

;; answer.wat
(module
  ;; Definiert unsere Funktion, welche wir dann für JavaScript eportieren
  (func $the_answer(result i32)
    (i32.const 42)
  )
  (export "get_answer" (func $the_answer))
)

Die erste Zeile zeigt, wie Kommentare in WAT aussehen. Hierzu werden zwei Semikolons ;; verwendet. Der erste wichtige Codeabschnitt folgt in Zeile zwei und definiert ein neues Modul. Module werden als Container für z. B. Funktionen benötigt. Als Nächstes wird unsere Funktion für die Rückgabe von 42 definiert. Als Rückgabewert wird dabei ein 32-Bit Integer verwendet. Der letzte Teil exportiert die Funktion the_answer() als get_answer. Unter diesem Namen ist die Funktion dann von JavaScript aus aufrufbar.

Damit sind wir mit dem Code unserer ersten WebAssembly schon fertig. Zum Kompilieren kann das WebAssembly Binary Toolkit (WABT) verwendet werden. Dies ist auf GitHub.com verfügbar. Zum Erstellen wird dann folgender Befehl verwendet:

wat2wasm answer.wat

Somit haben wir unsere WebAssembly als Binärversion erstellt. Um diese auszuführen können wir ein kleines HTML, dass ein JavaScript enthält erstellen. Im JavaScript laden wir als Erstes die WebAssembly und erzeugen dann davon eine Instanz. Sobald wir die Instanz haben, können wir unsere exportierte Funktion get_answer() aufrufen. Das Ergebnis schreiben in wir in diesem Fall der Einfachheit halber direkt ins Dokument.

<!DOCTYPE html>
<html>
  <head>
    <script>
      fetch('./answer.wasm').then(x=>x.arrayBuffer()).then(bytes=> WebAssembly.instantiate(bytes, {})
      ).then(results => {
        var instance = results.instance;
        var result = instance.exports.get_answer();
        document.write(result);
      }).catch(console.error);
    </script>
  </head>
  <body>
  </body>
</html>

Möchte man dieses Beispiel nun lokal ausführen, verhindert das die Sicherheitsfunktionen des Browser (egal ob Firefox, Edge oder Chrome). Es kommt zur Fehlermeldung: Fetch API cannot load file:///answer.wasm. URL scheme must be "http" or "https" for CORS request. Je nach Browser lässt sich diese Sicherheitsfunktion auch deaktivieren. Was natürlich nur zum Test eine gute Idee ist, im Internet sollte man so nicht unterwegs sein, was selbstverständlich ist. Dies lässt sich bei Chrome durch den Parameter --allow-file-access-from-files realisieren. Ein Start unserer Testseite im Browser könnte dann z. B. wie folgt aussehen:

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --allow-file-access-from-files file:///X:/wasm/answer.html

Dummerweise hilft das Ausschalten der Sicherheitsfunktion allein noch nicht, die WebAssembly zu laden. Es kommt zu einem weiteren Fehler „answer.html:5 Fetch API cannot load file:///./answer.wasm. URL scheme "file" is not supported.„, welcher damit zusammenhängt, dass der fetch()-Aufruf nicht mit dem file://-Protokoll funktioniert. Eine Lösung besteht nun darin, den Aufruf von fetch() durch eine eigene Implementierung zu ersetzen. Diese kann im einfachsten Fall so aussehen:

async function fetchLocal(url) {
  return new Promise(function(resolve, _) {
    var request = new XMLHttpRequest();
    request.onload = function() {
      resolve(new Response(request.responseText));
    };
    request.open('GET', url);
    request.send(null);
  });
}

Flugs also noch den Aufruf in Zeile 5 durch unsere neue Funktion ersetzt (fetchLocal('./answer.wasm').then(x=>...) und ein erneuter Aufruf führt zu folgendem erfolgreichem Ergebnis:

Eine JavaScript-Funktion in WASM verwenden

Natürlich erlaubt WebAssembly auch den Zugriff auf JavaScript-Funktionen. Dies möchte ich anhand eines weiteren kleinen Beispiels zeigen. Hierzu eignet sich eine typische Hallo, Welt Anwendung. Hierzu soll eine Funktion print_hello_world in WASM erstellt werden, welche den Text Hallo, Welt! dann im Browser ausgibt. Zur Ausgabe im Browser soll dabei eine JavaScript-Funktion zur Verfügung gestellt werden.

Da wir nun keine einfachen Werttypen wie im letzten Beispiel verwenden, sondern eine Zeichenkette, wird das ganze etwas mühseliger, denn wir müssen uns selbst um das Marshalling (verpacken/entpacken) der Daten über eine Speicherseite kümmern. Hierzu muss in WASM eine lineare Speicherseite angelegt und exportiert werden, auf die in JavaScript dann zugegriffen werden kann. Es folgt der Code in WAT:

;; hallo_welt.wat
(module
  ;; Importieren der JavaScript-Funktion print_to_log
  (import "env" "write" (func $js_write (param i32)))

  ;; Bereitstellen einer Speicherseite
  (memory $0 1)
  (export "pagememory" (memory $0))

  ;; Füge den null-terminierten String an Adresse 0 hinzu 
  (data (i32.const 0) "Hallo, Welt!\00")

  ;; Definiert unsere Funktion und welche für JavaScript exportiert wird
  (func $print_hello_world
    (call $js_write (i32.const 0))
  )
  (export "print_hello_world" (func $print_hello_world))
)

Der Code ist eigentlich recht schnell erklärt. In Zeile 4 wird die JavaScript-Funktion write() als js_write() importiert. Diese wird später (Zeile 15) dann aufgerufen, um quasi den Text Hallo, Welt! zur Ausgabe zu übergeben. Quasi deshalb, da wir ja den Text über die Speicherseite transportieren müssen. Diese Speicherseite wird in Zeile 7 angelegt und in Zeile 8 als pagememory exportiert. Über diesen Namen kann dann in JavaScript darauf zugegriffen werden. In Zeile 11 wird der Text im Speicher hinterlegt. Wir verwenden dabei eine Null-Terminierung, was nicht zwingend notwendig ist, man könnte auch auf andere Weise die Textlänge beschreiben. In der vorletzten Zeile wird dann noch unsere print_hello_world() Funktion exportiert und damit für JavaScript zugänglich gemacht.

Das HTML habe ich dieses Mal einfach gestaltet und das Script als weitere Datei verpackt:

<!DOCTYPE html>
<html>
  <head>
    <script src="./hallo_welt.js"></script>
  </head>
  <body>
  </body>
</html>

Der JavaScript-Code sieht wie folgt aus, die Funktion fetchLocal() habe ich dabei weggelassen:

var memory;

this.fetchLocal('./hallo_welt.wasm').then(response=>response.arrayBuffer()).then(bytes=> WebAssembly.instantiate(bytes, {
  env: {
    write: function write_to_document(offset) {
      var a = new Uint8Array(memory.buffer);
      for (var i = offset; a[i]; i++)
        document.write(String.fromCharCode(a[i]));
    }
  }
})
).then(results => {
  instance = results.instance;
  memory = instance.exports.pagememory;
  instance.exports.print_hello_world();
}).catch(console.error);

Interessant sind hier vor allem die Zeilen 5-9, diese definieren unsere Funktion write(), innerhalb von JavaScript als write_to_document() bezeichnet. Sobald diese Funktion nun innerhalb von WASM aufgerufen wird, wird der geteilte Speicher als UInt8-Array verarbeitet, der Offset von 0 wurde dabei beim Aufruf übergeben (Falls mehrere Zeichenketten verarbeitet werden sollen). Der Speicher wird nun Zeichen für Zeichen durchlaufen und im HTML-Dokument ergänzt, bis das Endezeichen (a[i] == 0) erreicht wird. Der Speicher (hier memory genannt) wird vor dem Aufruf aus den Exports (Zeile 14) gespeichert.

Das Bauen erfolgt dann wieder via wat2wasm.exe hallo_welt.wat. Das Ergebnis kann dann wieder im Browser angezeigt werden:

Ich hoffe, ich konnte einen kleinen Einblick in die Interna von WebAssembly geben. Der Quellcode findet der beiden Beispiele finden sich auch auf GitHub (TheAnswer und HalloWelt).