Was ist WebAssembly und woher kommt es?

Seit das Web nicht nur eine Plattform für Dokumente, sondern auch für Apps ist, haben einige der fortschrittlichsten Apps Webbrowser an ihre Grenzen gebracht. Der Ansatz, durch die Verknüpfung mit untergeordneten Sprachen die Leistung weiter zu verbessern, gibt es in vielen übergeordneten Sprachen. Java hat beispielsweise die Java Native Interface. Bei JavaScript ist diese untergeordnete Sprache WebAssembly. In diesem Artikel erfahren Sie, was Assembly-Sprache ist und warum sie im Web nützlich ist. Außerdem erfahren Sie, wie WebAssembly mithilfe der vorläufigen Lösung von asm.js erstellt wurde.

Assemblersprache

Haben Sie jemals in Assemblersprache programmiert? In der Computerprogrammierung ist Assemblersprache, oft einfach als Assembler bezeichnet und üblicherweise als ASM oder asm abgekürzt, eine beliebige Low-Level-Programmiersprache mit einer sehr engen Übereinstimmung zwischen den Anweisungen in der Sprache und den Maschinencodeanweisungen der Architektur.

Wenn Sie sich beispielsweise die Intel® 64- und IA-32-Architekturen (PDF) ansehen, führt der MUL-Befehl (für die multiplikation) eine vorzeichenlose Multiplikation des ersten Operanden (Zieloperanden) und des zweiten Operanden (Quelloperanden) durch und speichert das Ergebnis im Zieloperanden. Sehr vereinfacht ausgedrückt: Der Zieloperand ist ein impliziter Operand, der sich in Register AX befindet, und der Quelloperand befindet sich in einem allgemeinen Register wie CX. Das Ergebnis wird wieder in Register AX gespeichert. Betrachten Sie das folgende x86-Codebeispiel:

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.

Zum Vergleich: Wenn Sie die Aufgabe hätten, 5 × 10 zu multiplizieren, würden Sie wahrscheinlich in JavaScript Code ähnlich dem folgenden schreiben:

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

Der Vorteil der Assembly-Methode besteht darin, dass solcher Low-Level-Code, der für Maschinen optimiert ist, viel effizienter ist als High-Level-Code, der von Menschen optimiert wurde. Im vorherigen Fall spielt das keine Rolle, aber Sie können sich vorstellen, dass der Unterschied bei komplexeren Operationen erheblich sein kann.

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

asm.js

Der erste Schritt zum Schreiben von Assemblercode ohne Architekturabhängigkeiten war asm.js, eine strenge Teilmenge von JavaScript, die als Low-Level-, effiziente Zielsprache für Compiler verwendet werden konnte. Diese Untersprache beschrieb effektiv eine virtuelle Maschine mit Sandbox für speicherunsichere 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 geschrieben wurde (z. B. C), wurde von einem Quell-zu-Quell-Compiler wie dem frühen Emscripten (basierend auf LLVM) übersetzt.

Die Leistung wurde verbessert, indem die Sprachfunktionen auf solche beschränkt wurden, die für die AOT-Kompilierung geeignet sind. Firefox 22 war der erste Browser, der asm.js unterstützte, das unter dem Namen OdinMonkey veröffentlicht wurde. Chrome unterstützt seit Version 61 asm.js. asm.js funktioniert zwar weiterhin in Browsern, wurde aber durch WebAssembly ersetzt. Der Grund für die Verwendung von asm.js besteht darin, eine Alternative für Browser bereitzustellen, die WebAssembly nicht unterstützen.

WebAssembly

WebAssembly ist eine Low-Level-Assembly-ähnliche Sprache mit einem kompakten Binärformat, die mit nahezu nativer Leistung ausgeführt wird. Sie bietet Sprachen wie C/C++ und Rust sowie viele weitere mit einem Kompilierungsziel, damit sie im Web ausgeführt werden können. Die Unterstützung für arbeitsspeicherverwaltete Sprachen wie Java und Dart ist in Arbeit und sollte bald verfügbar sein oder ist bereits verfügbar, wie im Fall von Kotlin/Wasm. WebAssembly wurde entwickelt, um neben JavaScript ausgeführt zu werden, sodass beide zusammenarbeiten können.

Neben dem Browser werden WebAssembly-Programme dank WASI, der WebAssembly-Systemschnittstelle, einer modularen Systemschnittstelle für WebAssembly, auch in anderen Laufzeiten ausgeführt. WASI ist für die Interoperabilität zwischen Betriebssystemen konzipiert, mit dem Ziel, sicher zu sein und in einer Sandbox-Umgebung ausgeführt werden zu können.

WebAssembly-Code (Binärcode, d. h. Bytecode) ist für die Ausführung auf einer portablen virtuellen Stack-Maschine (VM) vorgesehen. Der Bytecode soll schneller geparst und ausgeführt werden als JavaScript und eine kompakte Codedarstellung haben.

Die konzeptionelle Ausführung von Anweisungen erfolgt über einen traditionellen 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. Anleitungen lassen sich in zwei Kategorien unterteilen:

  • Steueranweisungen, die Kontrollkonstrukte bilden und ihre Argumentwerte vom Stack abrufen, können den Programmzähler ändern und Ergebniswerte auf den Stack schieben.
  • Einfache Anweisungen, die die Argumentwerte aus dem Stack poppen, einen Operator auf die Werte anwenden und dann die Ergebniswerte auf den Stack schieben, gefolgt von einer impliziten Weiterführung des Programmzählers.

Zurück zum vorherigen Beispiel: 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.

Während asm.js vollständig in Software implementiert ist, d. h. sein Code in jeder JavaScript-Engine ausgeführt werden kann (auch wenn sie nicht optimiert ist), erforderte WebAssembly neue Funktionen, auf die sich alle Browseranbieter geeinigt haben. WebAssembly wurde 2015 angekündigt und erstmals im März 2017 veröffentlicht. Am 5. Dezember 2019 wurde es als W3C-Empfehlung anerkannt. Das W3C verwaltet den Standard mit Beiträgen aller großen Browseranbieter und anderer interessierter Parteien. Seit 2017 wird der Browser von allen unterstützt.

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

Textuelle Darstellung

Die Textdarstellung basiert auf S-Ausdrücken und verwendet normalerweise die Dateiendung .wat (für das WebAsembly-text-Format). Wenn Sie möchten, können Sie sie auch handschriftlich verfassen. Wenn Sie das Multiplikationsbeispiel oben noch nützlicher machen, indem Sie die Faktoren nicht mehr hartcodieren, können Sie wahrscheinlich den folgenden Code verstehen:

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

Binäre Darstellung

Das Binärformat mit der Dateiendung .wasm ist nicht für den menschlichen Gebrauch und erst recht nicht für die Erstellung durch Menschen gedacht. Mit einem Tool wie wat2wasm können Sie den obigen Code in die folgende Binärdarstellung umwandeln. (Die Kommentare sind normalerweise nicht Teil der Binärdarstellung, sondern werden zum besseren Verständnis vom Tool wat2wasm 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 nutzerfreundlich. Hier kommt ein Compiler wie Emscripten ins Spiel. Damit können Sie aus höheren Programmiersprachen wie C und C++ kompilieren. Es gibt auch andere Compiler für andere Sprachen wie Rust und viele mehr. Betrachten wir den folgenden C-Code:

#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, kompilieren Sie es mit dem Befehl emcc und fast denselben Argumenten zu WebAssembly:

$ emcc hello.c -o hello.html

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

Es gibt auch eine Möglichkeit, ohne HTML-Wrapper zu 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. Ehrlich gesagt kann die Arbeit mit WebAssembly ein bisschen wie das Meme „How to draw an owl“ (Wie zeichnet man eine Eule?) anfühlen, vor allem, weil Webentwickler, die mit HTML, CSS und JavaScript vertraut sind, nicht unbedingt mit kompilierten Sprachen wie C vertraut sind. Glücklicherweise gibt es Kanäle wie das webassembly-Tag von StackOverflow, wo Experten oft gerne helfen, wenn Sie nett fragen.

Danksagungen

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