Was ist WebAssembly und woher kommt es?

Das Web wurde nicht nur zu einer Plattform für Dokumente, sondern auch für Apps. Seitdem haben einige der fortschrittlichsten Anwendungen die Webbrowser an ihre Grenzen gebracht. Der Ansatz, durch die Verknüpfung mit untergeordneten Sprachen die Leistung zu verbessern, kommt in vielen übergeordneten Sprachen zum Einsatz. Java verfügt beispielsweise über die native Java-Schnittstelle. 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 Assembly-Sprache, die häufig einfach als Assembly bezeichnet und allgemein als ASM oder asm abgekürzt wird, beliebige Low-Level-Programmiersprachen mit einer sehr starken Übereinstimmung zwischen den Anweisungen in der Sprache und den Anweisungen zum Maschinencode 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 dargestellt ist der Zieloperanden ein impliziter Operand in Register AX und der Quelloperanden in einem allgemeinen Register wie CX. Das Ergebnis wird noch einmal im 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.

Wenn Sie zum Vergleich 5 und 10 multiplizieren möchten, würden Sie wahrscheinlich Code wie den folgenden in JavaScript schreiben:

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

Der Vorteil des Assembler-Prozesses besteht darin, dass derartiger Low-Level- und maschinenoptimierter Code viel effizienter ist als High-Level- und von Menschen optimierter Code. 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, Assemblercode zu schreiben, der nicht von einer bestimmten Architektur abhängig wäre, aber die Leistungsvorteile der Assembly erbt?

asm.js

Der erste Schritt zum Schreiben von Assemblercode ohne Architekturabhängigkeiten war asm.js, eine strikte Teilmenge von JavaScript, die als Low-Level-, effiziente Zielsprache für Compiler verwendet werden konnte. Diese Untersprache beschreibt effektiv eine virtuelle Maschine in einer Sandbox für speicherunsichere Sprachen wie C oder C++. Durch eine Kombination aus statischer und dynamischer Validierung konnte JavaScript-Engines eine im Voraus optimierte Kompilierungsstrategie für gültigen asm.js-Code einsetzen. Code, der in statisch typisierten Sprachen mit manueller Speicherverwaltung (z. B. C) geschrieben ist, wurde von einem Source-to-Source-Compiler wie dem early Emscripten (basierend auf LLVM) übersetzt.

Die Leistung wurde verbessert, indem Sprachfunktionen auf diejenigen beschränkt wurden, die für AOT zur Verfügung stehen. Firefox 22 war der erste Browser, der asm.js unter dem Namen OdinMonkey unterstützt. Chrome unterstützt seit Version 61 asm.js. asm.js funktioniert zwar weiterhin in Browsern, wurde aber durch WebAssembly ersetzt. Sie sollten asm.js an dieser Stelle als Alternative für Browser verwenden, die WebAssembly nicht unterstützen.

WebAssembly

WebAssembly ist eine Assembly-ähnliche Low-Level-Sprache mit einem kompakten Binärformat, das mit nahezu nativer Leistung ausgeführt wird und Sprachen wie C/C++ und Rust mit einem Kompilierungsziel zur Ausführung im Web bereitstellt. 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 für die Ausführung neben JavaScript entwickelt, sodass beides zusammenwirken kann.

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 so konzipiert, dass sie auf alle Betriebssysteme übertragen werden kann, um 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 tragbaren virtuellen Stapelmaschine (VM) vorgesehen. Der Bytecode ist so konzipiert, dass er schneller geparst und ausgeführt werden kann als JavaScript und 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, die ein Steuerelement konstruieren und ihre Argumentwerte aus dem Stack entfernen, den Programmzähler ändern und Ergebniswerte in den Stapel verschieben können.
  • Einfache Anweisungen, bei denen Argumentwerte aus dem Stack entfernt, ein Operator auf die Werte angewendet und dann die Ergebniswerte in den Stapel verschoben werden, gefolgt von einer impliziten Erweiterung des Programmzählers.

Um auf das vorherige Beispiel zurückzukommen, 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 ist zwar ausschließlich in Software implementiert, d. h. der Code kann in jeder JavaScript-Engine ausgeführt werden, selbst wenn sie nicht optimiert ist. WebAssembly erforderte jedoch neue Funktionen, auf die sich alle Browseranbieter geeinigt haben. WebAssembly wurde 2015 angekündigt und im März 2017 veröffentlicht. Am 5. Dezember 2019 wurde WebAssembly zu einer W3C-Empfehlung. Das W3C hält den Standard mit Beiträgen aller großen Browser-Anbieter und anderer interessierter Parteien aufrecht. Seit 2017 werden Browser allgemein unterstützt.

WebAssembly bietet zwei Darstellungen: textuell und binär. Oben sehen Sie die Darstellung in Textform.

Textdarstellung

Die Textdarstellung basiert auf S-Ausdrücken und verwendet normalerweise die Dateiendung .wat (für das WebAsembly-text-Format). Wenn Sie es wirklich wollten, können Sie es von Hand schreiben. Wenn Sie das obige Multiplikationsbeispiel verwenden und es nützlicher machen, indem Sie die Faktoren nicht mehr hartcodieren, könnte der folgende Code wahrscheinlich Sinn ergeben:

(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 menschliche Inhalte gedacht, geschweige denn durch Menschen. Mit einem Tool wie wat2wasm können Sie den obigen Code in die folgende Binärdarstellung konvertieren. (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

Kompilierung in WebAssembly

Wie du siehst, sind weder .wat noch .wasm besonders nutzerfreundlich. Hier kommt ein Compiler wie Emscripten ins Spiel. Damit können Sie Inhalte 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 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 in 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 den HTML-Wrapper in WebAssembly zu kompilieren:

$ emcc hello.c -o hello.js

Wie zuvor wird eine hello.wasm-Datei erstellt, aber diesmal eine hello.js-Datei anstelle des HTML-Wrappers. Zum Testen führen Sie die resultierende JavaScript-Datei hello.js z. B. 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 anfühlen wie How todraw an Owl Meme, insbesondere da Webentwickler, die mit HTML, CSS und JavaScript vertraut sind, nicht unbedingt mit Programmiersprachen wie C vertraut sind, die kompiliert werden müssen. Zum Glück gibt es Kanäle wie das webassembly-Tag von StackOverflow, bei denen Experten Ihnen oft weiterhelfen.

Danksagungen

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