Was ist WebAssembly und woher kommt es?

Seit das Web nicht nur für Dokumente, sondern auch für Apps genutzt wird, haben einige der fortschrittlichsten Apps Webbrowser an ihre Grenzen gebracht. Der Ansatz, durch die Interaktion mit Sprachen auf niedrigerer Ebene „näher an die Hardware“ zu gehen, um die Leistung zu verbessern, ist in vielen Sprachen auf höherer Ebene zu finden. In Java gibt es beispielsweise das Java Native Interface. Für JavaScript ist diese Low-Level-Sprache WebAssembly. In diesem Artikel erfahren Sie, was Assemblersprache ist und warum sie im Web nützlich sein kann. Außerdem wird erläutert, wie WebAssembly über die Zwischenlösung asm.js erstellt wurde.

Assemblersprache

Haben Sie schon einmal in Assemblersprache programmiert? In der Computerprogrammierung ist die Assemblersprache, oft einfach als Assembly bezeichnet und häufig als ASM oder asm abgekürzt, jede Low-Level-Programmiersprache mit einer sehr starken Entsprechung zwischen den Anweisungen in der Sprache und den Maschinencodeanweisungen der Architektur.

In der Intel® 64 and IA-32 Architectures (PDF) wird beispielsweise die MUL-Anweisung (für die Multiplikation) verwendet, um eine Multiplikation ohne Vorzeichen des ersten Operanden (Zieloperand) und des zweiten Operanden (Quelloperand) durchzuführen und das Ergebnis im Zieloperanden zu speichern. Vereinfacht ausgedrückt ist der Zieloperand ein impliziter Operand im Register AX und der Quelloperand befindet sich in einem Allzweckregister wie CX. Das Ergebnis wird wieder im Register AX gespeichert. Sehen Sie sich das folgende x86-Codebeispiel an:

mov ax, 5  ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx     ; Multiply the value of register AX (5)
           ; and the value of register CX (10), and
           ; store the result in register AX.

Wenn Sie beispielsweise die Aufgabe erhalten, 5 und 10 zu multiplizieren, würden Sie in JavaScript wahrscheinlich Code schreiben, der dem folgenden ähnelt:

const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;

Der Vorteil der Verwendung von Assembler besteht darin, dass solcher Code auf niedriger Ebene, der für Maschinen optimiert ist, viel effizienter ist als Code auf hoher Ebene, der für Menschen optimiert ist. Im vorherigen Fall spielt das keine Rolle, aber bei komplexeren Vorgängen kann der Unterschied erheblich sein.

Wie der Name schon sagt, ist x86-Code von der x86-Architektur abhängig. Was wäre, wenn es eine Möglichkeit gäbe, Assemblercode zu schreiben, der nicht von einer bestimmten Architektur abhängig ist, aber die Leistungsvorteile von Assembler erbt?

asm.js

Der erste Schritt zum Schreiben von Assemblercode ohne Architekturabhängigkeiten war asm.js, eine strikte Teilmenge von JavaScript, die als effiziente Zielsprache auf niedriger Ebene für Compiler verwendet werden konnte. Diese Untersprache beschrieb effektiv eine Sandbox-VM für speichersichere Sprachen wie C oder C++. Durch eine Kombination aus statischer und dynamischer Validierung konnten JavaScript-Engines eine AOT-Optimierungsstrategie (Ahead-of-Time) für gültigen asm.js-Code verwenden. Code, der in statisch typisierten Sprachen mit manueller Speicherverwaltung (z. B. C) geschrieben wurde, wurde von einem Source-to-Source-Compiler wie dem frühen Emscripten (basierend auf LLVM) übersetzt.

Die Leistung wurde verbessert, indem Sprachfunktionen auf diejenigen beschränkt wurden, die für AOT geeignet sind. Firefox 22 war der erste Browser, der asm.js unterstützt. Er wurde unter dem Namen OdinMonkey veröffentlicht. In Chrome-Version 61 wurde Unterstützung für asm.js hinzugefügt. asm.js funktioniert zwar weiterhin in Browsern, wurde aber durch WebAssembly ersetzt. asm.js kann als Alternative für Browser verwendet werden, die WebAssembly nicht unterstützen.

WebAssembly

WebAssembly ist eine Low-Level-Sprache, die Assembler ähnelt und ein kompaktes Binärformat hat. Sie wird mit nahezu nativer Leistung ausgeführt und bietet Sprachen wie C/C++ und Rust sowie vielen weiteren ein Kompilierungsziel, damit sie im Web ausgeführt werden können. Die Unterstützung von Sprachen mit Speicherverwaltung wie Java und Dart ist in Arbeit und sollte bald verfügbar sein. Bei Kotlin/Wasm ist sie bereits verfügbar. WebAssembly ist so konzipiert, dass es neben JavaScript ausgeführt werden kann.

Neben dem Browser können WebAssembly-Programme dank WASI (WebAssembly System Interface), einer modularen Systemschnittstelle für WebAssembly, auch in anderen Laufzeiten ausgeführt werden. WASI ist so konzipiert, dass es auf verschiedenen Betriebssystemen ausgeführt werden kann. Es soll sicher sein und in einer Sandbox-Umgebung ausgeführt werden können.

WebAssembly-Code (Binärcode, d. h. Bytecode) ist für die Ausführung auf einer portablen virtuellen Stackmaschine (VM) vorgesehen. Der Bytecode ist so konzipiert, dass er schneller als JavaScript geparst und ausgeführt werden kann und eine kompakte Darstellung des Codes ermöglicht.

Die konzeptionelle Ausführung von Anweisungen erfolgt über einen herkömmlichen Programmzähler, der die Anweisungen durchläuft. In der Praxis kompilieren die meisten Wasm-Engines den Wasm-Bytecode in Maschinencode und führen ihn dann aus. Anweisungen lassen sich in zwei Kategorien unterteilen:

  • Steueranweisungen, die Steuerelemente bilden und ihre Argumentwerte aus dem Stapel entfernen, können den Programmzähler ändern und Ergebniswerte auf den Stapel legen.
  • Einfache Anweisungen, die ihre Argumentwerte aus dem Stapel abrufen, einen Operator auf die Werte anwenden und dann die Ergebniswerte auf den Stapel legen, gefolgt von einer impliziten Erhöhung des Programmzählers.

Der folgende WebAssembly-Code entspricht dem x86-Code vom Anfang des Artikels:

i32.const 5  ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul      ; Pop the two most recent items on the stack,
             ; multiply them, and push the result onto the stack.

asm.js wird vollständig in Software implementiert, d. h., der Code kann in jeder JavaScript-Engine ausgeführt werden (auch wenn er nicht optimiert ist). Für WebAssembly waren neue Funktionen erforderlich, auf die sich alle Browseranbieter geeinigt haben. 2015 angekündigt und erstmals im März 2017 veröffentlicht, wurde WebAssembly am 5. Dezember 2019 zur W3C-Empfehlung. Der Standard wird vom W3C mit Beiträgen aller wichtigen Browseranbieter und anderer Interessengruppen gepflegt. Seit 2017 ist die Browserunterstützung universell.

WebAssembly hat zwei Darstellungen: textuell und binär. Oben sehen Sie die Textdarstellung.

Textdarstellung

Die Textdarstellung basiert auf S-Expressions und verwendet in der Regel die Dateiendung .wat (für das WebAssembly-Textformat). Wenn Sie es wirklich wollten, könnten Sie es auch von Hand schreiben. Wenn wir das Multiplikationsbeispiel von oben nehmen und es nützlicher machen, indem wir die Faktoren nicht mehr fest codieren, können Sie den folgenden Code wahrscheinlich nachvollziehen:

(module
  (func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
    local.get $factor1
    local.get $factor2
    i32.mul)
  (export "mul" (func $mul))
)

Binärdarstellung

Das Binärformat mit der Dateiendung .wasm ist nicht für die menschliche Nutzung, geschweige denn für die menschliche Erstellung gedacht. Mit einem Tool wie wat2wasm können Sie den obigen Code in die folgende binäre Darstellung konvertieren. Die Kommentare sind in der Regel nicht Teil der binären Darstellung, sondern werden vom Tool „wat2wasm“ zur besseren Verständlichkeit hinzugefügt.

0000000: 0061 736d                             ; WASM_BINARY_MAGIC
0000004: 0100 0000                             ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                    ; section code
0000009: 00                                    ; section size (guess)
000000a: 01                                    ; num types
; func type 0
000000b: 60                                    ; func
000000c: 02                                    ; num params
000000d: 7f                                    ; i32
000000e: 7f                                    ; i32
000000f: 01                                    ; num results
0000010: 7f                                    ; i32
0000009: 07                                    ; FIXUP section size
; section "Function" (3)
0000011: 03                                    ; section code
0000012: 00                                    ; section size (guess)
0000013: 01                                    ; num functions
0000014: 00                                    ; function 0 signature index
0000012: 02                                    ; FIXUP section size
; section "Export" (7)
0000015: 07                                    ; section code
0000016: 00                                    ; section size (guess)
0000017: 01                                    ; num exports
0000018: 03                                    ; string length
0000019: 6d75 6c                          mul  ; export name
000001c: 00                                    ; export kind
000001d: 00                                    ; export func index
0000016: 07                                    ; FIXUP section size
; section "Code" (10)
000001e: 0a                                    ; section code
000001f: 00                                    ; section size (guess)
0000020: 01                                    ; num functions
; function body 0
0000021: 00                                    ; func body size (guess)
0000022: 00                                    ; local decl count
0000023: 20                                    ; local.get
0000024: 00                                    ; local index
0000025: 20                                    ; local.get
0000026: 01                                    ; local index
0000027: 6c                                    ; i32.mul
0000028: 0b                                    ; end
0000021: 07                                    ; FIXUP func body size
000001f: 09                                    ; FIXUP section size
; section "name"
0000029: 00                                    ; section code
000002a: 00                                    ; section size (guess)
000002b: 04                                    ; string length
000002c: 6e61 6d65                       name  ; custom section name
0000030: 01                                    ; name subsection type
0000031: 00                                    ; subsection size (guess)
0000032: 01                                    ; num names
0000033: 00                                    ; elem index
0000034: 03                                    ; string length
0000035: 6d75 6c                          mul  ; elem name 0
0000031: 06                                    ; FIXUP subsection size
0000038: 02                                    ; local name type
0000039: 00                                    ; subsection size (guess)
000003a: 01                                    ; num functions
000003b: 00                                    ; function index
000003c: 02                                    ; num locals
000003d: 00                                    ; local index
000003e: 07                                    ; string length
000003f: 6661 6374 6f72 31            factor1  ; local name 0
0000046: 01                                    ; local index
0000047: 07                                    ; string length
0000048: 6661 6374 6f72 32            factor2  ; local name 1
0000039: 15                                    ; FIXUP subsection size
000002a: 24                                    ; FIXUP section size

In WebAssembly kompilieren

Wie Sie sehen, sind weder .wat noch .wasm besonders benutzerfreundlich. Hier kommt ein Compiler wie Emscripten ins Spiel. Damit können Sie aus Sprachen höherer Ebene wie C und C++ kompilieren. Es gibt auch andere Compiler für andere Sprachen wie Rust und viele mehr. Sehen Sie sich den folgenden C-Code an:

#include <stdio.h>

int main() {
  printf("Hello World\n");
  return 0;
}

Normalerweise würden Sie dieses C-Programm mit dem Compiler gcc kompilieren.

$ gcc hello.c -o hello

Wenn Emscripten installiert ist, können Sie es mit dem Befehl emcc und fast denselben Argumenten in WebAssembly kompilieren:

$ emcc hello.c -o hello.html

Dadurch werden die Datei hello.wasm und die HTML-Wrapper-Datei hello.html erstellt. Wenn Sie die Datei hello.html über einen Webserver bereitstellen, wird "Hello World" in der Entwicklertools-Konsole ausgegeben.

Es gibt auch eine Möglichkeit, ohne den HTML-Wrapper in WebAssembly zu kompilieren:

$ emcc hello.c -o hello.js

Wie zuvor wird dadurch eine hello.wasm-Datei erstellt, diesmal jedoch eine hello.js-Datei anstelle des HTML-Wrappers. Zum Testen führen Sie die resultierende JavaScript-Datei hello.js beispielsweise mit Node.js aus:

$ node hello.js
Hello World

Weitere Informationen

Diese kurze Einführung in WebAssembly ist nur die Spitze des Eisbergs. Weitere Informationen zu WebAssembly finden Sie in der WebAssembly-Dokumentation auf MDN und in der Emscripten-Dokumentation. Die Arbeit mit WebAssembly kann sich ein wenig wie das Meme „How to draw an owl“ anfühlen, insbesondere da Webentwickler, die mit HTML, CSS und JavaScript vertraut sind, nicht unbedingt mit den Sprachen wie C vertraut sind, aus denen kompiliert werden soll. Zum Glück gibt es Kanäle wie das webassembly-Tag von StackOverflow, in denen Experten oft gern weiterhelfen, wenn Sie freundlich fragen.

Danksagungen

Dieser Artikel wurde von Jakob Kummerow, Derek Schuff und Rachel Andrew geprüft.