Was ist WebAssembly und woher kommt es?

Seit das Web zu einer Plattform nicht nur für Dokumente, sondern auch für Apps wurde, haben einige der fortschrittlichsten Anwendungen Webbrowser an ihre Grenzen gebracht. Der Ansatz, „näher an das Metall“ zu gehen, indem er zur Verbesserung der Leistung durch die Schnittstelle mit untergeordneten Sprachen die Leistung verbessert, ist in vielen höheren Sprachen anzutreffen. Java hat zum Beispiel 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 sein kann. Anschließend erfahren Sie, wie WebAssembly über die Zwischenlösung von asm.js erstellt wurde.

Assemblersprache

Haben Sie schon einmal in Assemblersprache programmiert? In der Computerprogrammierung ist Assembly-Sprache, oft einfach als Assembly bezeichnet und allgemein als ASM oder ASM abgekürzt, jede Low-Level-Programmiersprache, bei der eine enge Übereinstimmung zwischen den Anweisungen in der Sprache und den Maschinencodeanweisungen der Architektur besteht.

In der Intel® 64- und IA-32-Architektur (PDF) wird beispielsweise mit der Anweisung MUL (für Multiplikation) eine Multiplikation zwischen dem ersten Operanden (Zieloperand) und dem zweiten Operanden (Quelloperand) ohne Vorzeichen ausgeführt und das Ergebnis wird im Zieloperand gespeichert. Sehr vereinfacht dargestellt: Der Zieloperand ist 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 das Ziel beispielsweise die Multiplikation von 5 und 10 sein soll, würden Sie wahrscheinlich Code wie den folgenden in JavaScript schreiben:

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

Der Vorteil der Montage besteht darin, dass dieser Low-Level- und maschinenoptimierten Code viel effizienter ist als High-Level- und von Menschen optimierter Code. Im vorherigen Fall ist das keine Rolle, aber Sie können sich vorstellen, dass bei komplexeren Operationen der Unterschied signifikant 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, Assembly-Code zu schreiben, der nicht von einer bestimmten Architektur abhängig wäre, aber die Leistungsvorteile der Assemblies übernehmen würde?

asm.js

Der erste Schritt zum Schreiben von Assembly-Code ohne Architekturabhängigkeiten war asm.js, eine strikte Teilmenge von JavaScript, die als einfache, effiziente Zielsprache für Compiler verwendet werden konnte. In dieser Untersprache wurde effektiv eine in einer Sandbox ausgeführte virtuelle Maschine für Sprachen wie C oder C++ beschrieben, bei denen der Arbeitsspeicher nicht sicher ist. Eine Kombination aus statischer und dynamischer Validierung ermöglichte den JavaScript-Engines, eine vorzeitige Kompilierungsstrategie für gültigen asm.js-Code anzuwenden. Code, der in statisch typisierten Sprachen mit manueller Speicherverwaltung (z. B. C) geschrieben wurde, wurde von einem Source-to-Source-Compiler wie dem early Emscripten (basierend auf LLVM) übersetzt.

Die Leistung wurde verbessert, indem Sprachfunktionen auf diejenigen eingestellt wurden, die für AOT freigegeben sind. Firefox 22 war der erste Browser, der asm.js unterstützt und unter dem Namen OdinMonkey veröffentlicht wurde. Chrome hat in Version 61 asm.js-Unterstützung hinzugefügt. asm.js funktioniert zwar noch in Browsern, wurde jedoch durch WebAssembly ersetzt. Der Grund für die Verwendung von asm.js wäre an dieser Stelle eine Alternative für Browser, die WebAssembly nicht unterstützen.

WebAssembly

WebAssembly ist eine Low-Level-Assembly-ähnliche Sprache mit einem kompakten Binärformat, das nahezu native Leistung bietet und Sprachen wie C/C++ und Rust sowie viele weitere mit einem Kompilierungsziel bietet, damit sie im Web ausgeführt werden. Die Unterstützung für speicherverwaltete Sprachen wie Java und Dart ist in Arbeit und sollte bald verfügbar sein oder wie bei Kotlin/Wasm bereits eingeführt worden sein. WebAssembly wurde für die Ausführung neben JavaScript entwickelt, sodass beide zusammen funktionieren.

Neben dem Browser laufen WebAssembly-Programme auch in anderen Laufzeiten ab – dank WASI, der WebAssembly-Systemschnittstelle, einer modularen Systemschnittstelle für WebAssembly. WASI ist so konzipiert, dass es auf verschiedene Betriebssysteme übertragen werden kann, um Sicherheit zu gewährleisten und in einer Sandbox-Umgebung ausgeführt zu werden.

WebAssembly-Code (Binärcode, also Bytecode) ist für die Ausführung auf einer portablen Virtual Stack Machine (VM) vorgesehen. Der Bytecode ist schneller parsen und ausführen als JavaScript und hat eine kompakte Codedarstellung.

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 diesen dann aus. Anleitungen lassen sich in zwei Kategorien unterteilen:

  • Steueranweisungen, mit denen Steuerelemente erstellt und deren Argumentwerte aus dem Stapel entfernt werden, können den Programmzähler ändern und Ergebniswerte in den Stapel verschieben.
  • Einfache Anweisungen, mit denen die Argumentwerte aus dem Stapel entnommen, ein Operator auf die Werte angewendet und dann der bzw. die Ergebniswerte in den Stack übertragen werden, gefolgt von einer impliziten Fortsetzung des Programmzählers.

Wenn wir noch einmal auf das vorherige Beispiel zurückkommen, würde der folgende WebAssembly-Code dem x86-Code vom Anfang des Artikels entsprechen:

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 komplett in Software implementiert, das heißt, sein Code kann in jeder JavaScript-Engine ausgeführt werden (auch wenn sie nicht optimiert ist), aber WebAssembly erforderte neue Funktionen, auf die sich alle Browseranbieter geeinigt haben. Angekündigt im Jahr 2015 und erstmals im März 2017 veröffentlicht, wurde WebAssembly am 5. Dezember 2019 zu einer W3C-Empfehlung. Das W3C pflegt diesen Standard mit Beiträgen von allen großen Browser-Anbietern und anderen interessierten Parteien. Seit 2017 werden Browser überall unterstützt.

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

Textdarstellung

Die Textdarstellung basiert auf S-Ausdrücken und verwendet üblicherweise die Dateiendung .wat (für das WebAssembly-text-Format). Wenn Sie es wirklich wollten, können Sie es auch von Hand schreiben. Wenn wir das Multiplikationsbeispiel von oben nehmen und es nützlicher machen, indem Sie die Faktoren nicht mehr hartcodieren, könnten Sie wahrscheinlich den folgenden Code sinnvoller gestalten:

(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 den menschlichen Gebrauch gedacht, geschweige denn von Menschen. Mit einem Tool wie wat2wasm können Sie den Code oben in die folgende Binärdarstellung umwandeln. Die Kommentare sind normalerweise nicht Teil der binären Darstellung, sondern wurden zur besseren Verständlichkeit 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

Weder .wat noch .wasm sind besonders menschenfreundlich. Hier kommt ein Compiler wie Emscripten ins Spiel. Damit kannst du aus höheren Sprachen wie C und C++ kompilieren. Es gibt auch andere Compiler für andere Sprachen wie Rust und viele mehr. Betrachten Sie den folgenden C-Code:

#include <stdio.h>

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

Normalerweise wird dieses C-Programm mit dem Compiler gcc kompiliert.

$ gcc hello.c -o hello

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

$ emcc hello.c -o hello.html

Dadurch werden eine hello.wasm-Datei und die HTML-Wrapper-Datei hello.html erstellt. Wenn du die Datei hello.html von einem Webserver aus bereitstellst, 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 auch hier eine hello.wasm-Datei erstellt, aber dieses Mal eine hello.js-Datei anstelle des HTML-Wrappers. Führen Sie zum Testen die resultierende JavaScript-Datei hello.js aus, z. B. mit Node.js:

$ node hello.js
Hello World

Mehr dazu

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 Mem zum Zeichnen einer Eule wirken, vor allem deshalb, weil Webentwickler, die mit HTML, CSS und JavaScript vertraut sind, nicht unbedingt auch mit den zu kompilierenden Sprachen wie C vertraut sind. Glücklicherweise gibt es Kanäle wie das webassembly-Tag von StackOverflow, bei denen Ihnen Experten oft helfen, wenn Sie nett danach fragen.

Danksagungen

Dieser Artikel wurde von Jakob Kummerow, Derek Schuff und Rachel Andrew verfasst.