WebAssembly: Das schnellere JavaScript?
Anwendungen, die im Browser laufen, statt auf der Festplatte installiert zu sein, werden immer umfangreicher. Neben typischer Bürosoftware wie Microsoft 365 oder Google Docs, die mit immer neuen Funktionen aufwarten, werden auch Browserspiele immer aufwendiger und verlangen mehr Ressourcen. Oftmals werden solche Webanwendungen in JavaScript angeboten. Inzwischen setzen aber mehr und mehr Entwickler auf WebAssembly – ein neuer Ansatz mit erstaunlichen Resultaten.
Was ist WebAssembly?
WebAssembly (Wasm) ist eine neue Art für Webentwickler, Anwendungen im Internet zur Verfügung zu stellen. Bisher musste man dafür auf JavaScript zurückgreifen. Doch JavaScript läuft relativ langsam und kommt in manchen Szenarien an seine Grenzen. Deshalb hat das World Wide Web Consortium (W3C) eine neue Methode – WebAssembly – vorangetrieben. Damit Wasm funktionieren kann, muss allerdings auch der entsprechende Browser mit der Sprache umgehen können. Deshalb waren Mozilla (Firefox), Microsoft (Edge), Apple (Safari) und Google (Chrome) an der Entwicklung beteiligt. In allen aktuellen Browserversionen der Hersteller können Anwendungen in WebAssembly ausgeführt werden.
Um die Leistungsfähigkeit von WebAssembly selbst zu erleben, lohnt sich eine Runde Funky Karts. Das Spiel – eigentlich eine mobile App – wurde in WebAssembly umgewandelt, um es auch im Browser ausführen zu können. Der Entwickler hat für das Projekt einen interessanten Blog geführt und dort die einzelnen Schritte bei der Umwandlung beschrieben.
Prinzipiell wird WebAssembly als Bytecode dargestellt. Dies kann man als Zwischenstufe gesehen werden zwischen Maschinencode, der nur vom Computer verstanden werden kann, und einer typischen Programmiersprache, die für Menschen zwar lesbar ist, aber deshalb auch erst kompiliert werden muss. Deshalb ist WebAssembly vergleichsweise schnell: Der Computer braucht kaum Aufwand, um den Code umzuwandeln. Doch in Bytecode zu schreiben, ist eher ungewöhnlich: Der Vorteil von Wasm ist, dass man selbst nicht in dieser Programmiersprache arbeiten muss. In der Praxis verfasst man die Webanwendung beispielsweise in C oder C++.
Der Quelltext wird mit der Anwendung Emscripten umgewandelt. Dieses Werkzeug war schon bevor es WebAssembly gab im Einsatz, um C/C++-Code in JavaScript (bzw. ams.js) umzuwandeln. Inzwischen lässt sich damit Code aber auch in Wasm um schreiben. Damit ist der Code vorkompiliert, muss also nicht erst im Moment des Ausführens kompiliert oder interpretiert werden. Wenn der Nutzer die Anwendung schließlich im Browser öffnet, wird eine kleine virtuelle Maschine gestartet. In dieser läuft dann die Anwendung.
Vorteile von WebAssembly
Derzeit hat WebAssembly eigentlich nur einen wirklichen Nachteil: Es verbreitet sich nur langsam. Webentwickler sind an die Arbeit mit JavaScript gewöhnt. Und es ist auch gar nicht geplant, JavaScript zu verdrängen. Die Projektleitung legt in der Kommunikation großen Wert darauf, Wasm als zusätzliche Option zu JavaScript anzupreisen. Doch durch die Unterstützung der großen Browserhersteller und durch das W3C nimmt die Verbreitung an Fahrt auf. Das liegt auch daran, dass Website-Besucher keine eigenen Schritte einleiten müssen: Die Webanwendungen in WebAssembly laden genauso einfach wie Code in JavaScript – nur schneller.
Viele Entwickler, die ohnehin bereits in C, C++ oder z. B. Rust schreiben, können nun auch direkt für das Web programmieren. Die Programmiersprachen bringen mitunter auch andere Möglichkeiten zur Gestaltung der Anwendung mit sich: Wer bei JavaScript nicht die benötigten Libraries oder Frameworks für sein Programm findet, hat nun mehr Auswahl, um zum Ziel zu gelangen. Man findet als Entwickler also einige Gründe, sich WebAssembly näher anzuschauen:
- Offener Webstandard des W3C
- Hohe Performance und niedrige Dateigröße
- Dadurch auch perfekt für mobiles Surfen geeignet
- Auch Virtual Reality ist damit theoretisch im Browser möglich
- Keine neue Programmiersprache notwendig
- Auch C, C++ oder Rust können nun zur Programmierung von Webanwendungen verwendet werden
- Wird von allen großen Browserherstellern unterstützt
- Keinerlei Einschränkung für den Nutzer
WebAssembly in der Praxis
Es ist nicht vorgesehen, dass man tatsächlich in WebAssembly programmiert. Der große Vorteil der Technik liegt gerade darin, dass man eine beliebte Hochsprache wie etwa C verwendet und dieser Code dann in das leistungsfähige Wasm-Format übertragen wird. Es kann aber durchaus sinnvoll sein, sich auch mit dem bereits kompilierten Code auseinanderzusetzen und so hinter die Funktionsweise von WebAssembly zu blicken.
Den Quelltext gibt es in zwei verschiedenen Varianten: WebAssembly Text Format (WAT) und WebAssembly Binary Format (Wasm). Letzteres ist der eigentliche Code, den die Maschine laufen lässt. Da dieser aber ausschließlich aus Binärcode besteht, ist er mehr oder weniger unbrauchbar für eine Analyse durch Menschen. Deshalb gibt es die Zwischenform WAT. Ein solcher Code verwendet lesbare Ausdrücke und kann deshalb auch vom Programmierer selbst untersucht werden, bietet aber nicht den Arbeitskomfort, den man von etablierten Programmiersprachen kennt.
Für ein Beispiel benutzen wir einen ganz einfach gestalteten Quelltext in C:
#define WASM_EXPORT __attribute__((visibility("default")))
WASM_EXPORT
int main() {
return 1;
}
Der gleiche Code im WAT-Format ist um einiges länger:
(module
(type $t0 (func))
(type $t1 (func (result i32)))
(func $__wasm_call_ctors (type $t0))
(func $main (export "main") (type $t1) (result i32)
i32.const 1)
(table $T0 1 1 anyfunc)
(memory $memory (export "memory") 2)
(global $g0 (mut i32) (i32.const 66560))
(global $__heap_base (export "__heap_base") i32 (i32.const 66560))
(global $__data_end (export "__data_end") i32 (i32.const 1024)))
Die Lesbarkeit ist bereits stark eingeschränkt, doch man kann ein paar Elemente ausmachen: Alles in WebAssembly wird in Module eingeteilt. Die Module sind wiederum in Funktionen eingeteilt, welche durch Parameter spezifiziert werden. Insgesamt lassen sich 5 verschiedene Elemente erkennen:
- module: oberste Einheit in WebAssembly
- function: Gruppierung innerhalb eines Moduls
- memory: Array mit Bytes
- global: Wert, der über mehrere Module hinweg verwendet werden kann
- table: Speicher von Verweisen
Wenn der Code allerdings in die Binärform übersetzt wird, kann man von all dem nichts mehr erkennen:
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: 02 ; num types
; type 0
000000b: 60 ; func
000000c: 00 ; num params
000000d: 00 ; num results
; type 1
000000e: 60 ; func
000000f: 00 ; num params
0000010: 01 ; num results
0000011: 7f ; i32
0000009: 08 ; FIXUP section size
; section "Function" (3)
0000012: 03 ; section code
0000013: 00 ; section size (guess)
0000014: 02 ; num functions
0000015: 00 ; function 0 signature index
0000016: 01 ; function 1 signature index
0000013: 03 ; FIXUP section size
; section "Table" (4)
0000017: 04 ; section code
0000018: 00 ; section size (guess)
0000019: 01 ; num tables
; table 0
000001a: 70 ; funcref
000001b: 01 ; limits: flags
000001c: 01 ; limits: initial
000001d: 01 ; limits: max
0000018: 05 ; FIXUP section size
; section "Memory" (5)
000001e: 05 ; section code
000001f: 00 ; section size (guess)
0000020: 01 ; num memories
; memory 0
0000021: 00 ; limits: flags
0000022: 02 ; limits: initial
000001f: 03 ; FIXUP section size
; section "Global" (6)
0000023: 06 ; section code
0000024: 00 ; section size (guess)
0000025: 03 ; num globals
0000026: 7f ; i32
0000027: 01 ; global mutability
0000028: 41 ; i32.const
0000029: 8088 04 ; i32 literal
000002c: 0b ; end
000002d: 7f ; i32
000002e: 00 ; global mutability
000002f: 41 ; i32.const
0000030: 8088 04 ; i32 literal
0000033: 0b ; end
0000034: 7f ; i32
0000035: 00 ; global mutability
0000036: 41 ; i32.const
0000037: 8008 ; i32 literal
0000039: 0b ; end
0000024: 15 ; FIXUP section size
; section "Export" (7)
000003a: 07 ; section code
000003b: 00 ; section size (guess)
000003c: 04 ; num exports
000003d: 04 ; string length
000003e: 6d61 696e main ; export name
0000042: 00 ; export kind
0000043: 01 ; export func index
0000044: 06 ; string length
0000045: 6d65 6d6f 7279 memory ; export name
000004b: 02 ; export kind
000004c: 00 ; export memory index
000004d: 0b ; string length
000004e: 5f5f 6865 6170 5f62 6173 65 __heap_base ; export name
0000059: 03 ; export kind
000005a: 01 ; export global index
000005b: 0a ; string length
000005c: 5f5f 6461 7461 5f65 6e64 __data_end ; export name
0000066: 03 ; export kind
0000067: 02 ; export global index
000003b: 2c ; FIXUP section size
; section "Code" (10)
0000068: 0a ; section code
0000069: 00 ; section size (guess)
000006a: 02 ; num functions
; function body 0
000006b: 00 ; func body size (guess)
000006c: 00 ; local decl count
000006d: 0b ; end
000006b: 02 ; FIXUP func body size
; function body 1
000006e: 00 ; func body size (guess)
000006f: 00 ; local decl count
0000070: 41 ; i32.const
0000071: 01 ; i32 literal
0000072: 0b ; end
000006e: 04 ; FIXUP func body size
0000069: 09 ; FIXUP section size
; section "name"
0000073: 00 ; section code
0000074: 00 ; section size (guess)
0000075: 04 ; string length
0000076: 6e61 6d65 name ; custom section name
000007a: 01 ; function name type
000007b: 00 ; subsection size (guess)
000007c: 02 ; num functions
000007d: 00 ; function index
000007e: 11 ; string length
000007f: 5f5f 7761 736d 5f63 616c 6c5f 6374 6f72 __wasm_call_ctor
000008f: 73 s ; func name 0
0000090: 01 ; function index
0000091: 04 ; string length
0000092: 6d61 696e main ; func name 1
000007b: 1a ; FIXUP subsection size
0000096: 02 ; local name type
0000097: 00 ; subsection size (guess)
0000098: 02 ; num functions
0000099: 00 ; function index
000009a: 00 ; num locals
000009b: 01 ; function index
000009c: 00 ; num locals
0000097: 05 ; FIXUP subsection size
0000074: 28 ; FIXUP section size
Wer selbst die ersten Schritte in WebAssembly ausprobieren möchte, kann auf das WebAssembly Studio zurückgreifen. Dort steht Ihnen online eine Entwicklungsumgebung für Wasm zur Verfügung.