Windows Developer

WebAssembly, was bist du?

WebAssembly: Mit C, C++, Rust, C# oder auch Java für den Browser entwickeln
Keine Kommentare

WebAssembly – kurz Wasm – ist eine neue Technologie, mit der man Code nahezu beliebiger Sprachen für die Webplattform kompilieren und im Browser innerhalb einer sicheren Sandbox fast mit nativer Geschwindigkeit ausführen kann. Das hört sich fast zu schön an, um wahr zu sein, oder?

Der Browser ist allgegenwärtig. Nahezu jeder trägt heute ein Smartphone oder sogar ein Tablet mit sich herum, auf dem ein Webbrowser installiert ist. Zwar herrschen auf diesen Geräten heute noch echte Apps vor, aber mit dem beginnenden Siegeszug von Progressive Web Apps (PWAs) werden reine Webanwendungen eine ernstzunehmende Alternative.

Damit wird das Web, oder konkreter der Browser, zu einer immer attraktiveren Zielplattform für unsere Anwendungen. Denn er steht nicht nur einheitlich auf mobilen Geräten zur Verfügung, sondern eben auch auf dem Desktop, und erlaubt uns daher, alle möglichen Zielplattformen zu bedienen und dabei möglichst viel Code zwischen allen Plattformen zu teilen. Doch die Webwelt bringt auch Nachteile mit sich – allen voran JavaScript. Das ist jetzt bitte nicht falsch zu verstehen: JavaScript ist eine sehr vielseitige Sprache, und keine schlechte per se, doch sie hat in der Tat einige Nachteile. Zum einen eben, dass es die einzige Sprache ist, die so universell zur Verfügung steht.

Wenn man nun ein bereits existierendes System auf neue Plattformen bringen will, ist es eher unwahrscheinlich, dass das Altsystem in JavaScript implementiert wurde. Man muss also viel Code duplizieren. Ein weiteres Problem: JavaScript wird interpretiert, und die JavaScript Engines in den Browsern müssen sie bei jedem Anwendungsstart zuerst parsen, dann JITten und können den Code erst danach ausführen. Das ist leider sehr ineffizient. Jeder Entwickler, der schon einmal mit Anwendungen gearbeitet hat, die auf Browsertechnologien – beziehungsweise konkret Electron – basieren, wird sich sicher schon mehr als einmal kopfschüttelnd darüber gewundert haben, warum ausgerechnet dieses kleine Tool gigabyteweise Arbeitsspeicher verbrät. Natürlich ist das nicht ausschließlich ein Problem von JavaScript oder dessen Ausführungsumgebung, doch beides trägt durchaus dazu bei, dass solche Probleme auftreten können. Ein weiteres Problem ist, dass JavaScript nur in einem Thread ausgeführt wird. Das macht es unmöglich, innerhalb einer Anwendung längere Berechnungen durchzuführen, ohne dabei die Anwendung zu blockieren.

Wenn es doch nur eine Möglichkeit gäbe, existierenden Code einfach in den Browser zu portieren und diesen dann idealerweise auch noch deutlich effizienter ausführen zu können. Dieser Wunsch ist nicht neu,er ist sogar schon relativ alt. Und er wurde erhört: Genau das ist heute möglich – und zwar mit WebAssembly.

Wünsche werden wahr mit WebAssembly

Bevor wir uns kurz mit der Geschichte von WebAssembly beschäftigen, steht erst einmal die Frage im Raum, ob es überhaupt eine relevante Technologie ist oder werden kann. Zuerst einmal ist WebAssembly vom W3C spezifiziert worden und damit nicht proprietär. Aber nur, weil das W3C etwas vorschlägt und spezifiziert, muss es noch lange nicht von den Browserherstellern umgesetzt werden und damit für uns verfügbar sein. Wie sieht es also konkret für WebAssembly aus?

Diese Frage hat interessanterweise Apple für uns beantwortet. Apple ist nun nicht gerade dafür bekannt, Innovationen im Web voranzutreiben. Die Browser-Engine Safari, die stellvertretend für sämtliche mobile Weberlebnisse auf Apples Smartphones und Tablets steht, steht Technologien, die den Browser zu einer echten Alternative für den hauseigenen App Store machen könnten, traditionell eher verhalten gegenüber. Seit September 2017 unterstützt allerdings auch Safari, als letzter Vertreter der modernen Browser, Wasm, neben Edge, Firefox und natürlich Chrome. Damit kann WebAssembly nun wirklich überall verwendet werden, ohne einen substanziellen Teil der Webnutzer auszuschließen.

Um zu verstehen, was WebAssembly uns heute schon bieten kann und wo es hingehen könnte, lohnt sich ein kurzer Blick in die Vergangenheit. Die Frage, die uns zuerst interessiert, ist: Wo kommt WebAssembly denn jetzt auf einmal her? Die Arbeit an der Wasm-1.0-Spezifikation begann im Jahr 2015 und ihr Ziel war es, initial die Features von asm.js abbilden zu können und WebAssembly dann von dort aus weiter auszubauen.

asm.js: Der Vorgänger von WebAssembly

Was ist denn nun wieder asm.js? asm.js existiert seit Anfang 2013 und ist ein Subset von JavaScript. Es ermöglicht signifikante Geschwindigkeitsverbesserungen gegenüber dem vollwertigen JavaScript, indem es sich auf bestimmte Sprachkonstrukte beschränkt, die beim Parsen und bei der Ausführung möglichst wenig Overhead erzeugen. Diese erlauben es, statisch typisierte Sprachen mit manueller Speicherverwaltung wie z. B. C oder C++ automatisch in asm.js-Code zu übersetzen (Code-to-Code-Übersetzung oder auch Transpilierung) und den daraus resultierenden asm.js-Code dabei mittels Ahead-of-Time-(AOT-) und weiterer Optimierungen auf Transpiler-Ebene so zu übersetzen, dass eine JavaScript-Laufzeitumgebung diesen möglichst effizient ausführen kann.

Mit Fachbegriffen aus dem Compilerbau heißt das konkret, der Code sollte mit möglichst wenig zusätzlichem Aufwand für das Aufbauen des Syntaxbaums (syntaktische Analyse) und der darauf folgenden semantischen Analyse analysiert werden können. Der daraus resultierende Syntaxbaum, der dann für die eigentliche Generierung des ausführbaren Maschinencodes verwendet wird, sollte idealerweise so aussehen, dass ihn der JavaScript-JIT-Compiler möglichst wenig umbaut beziehungsweise weiter optimiert. Unterstützt ein Browser asm.js direkt, kann er daher für diesen Code auch die weitere Suche nach Optimierungsmöglichkeiten deaktivieren (da sie ja bereits vom Transpiler vorgenommen wurden) und somit noch etwas schneller sein. Kurzum, asm.js ist eine Technik, die durch die automatische Generierung von möglichst weit im Voraus optimiertem Code die Laufzeitperformance erhöht.

Der erste Browser mit Unterstützung für asm.js war Mozillas Firefox, und da dieser nur ein Subset von JavaScript war, war es für die anderen Browserhersteller ein Leichtes, asm.js auch recht zügig bereitzustellen. Die erste praktische Anwendung hierfür war beeindruckenderweise eine vollständige Portierung der Unreal Engine 3 und zugehöriger Demos nach asm.js, in Summe über eine Million Zeilen C++-Code. Die Unreal Engine ist eine vollwertige 2-D- und 3-D-Spiele-Engine von Epic Games, die neben Spielen für den Desktop auch für Konsolen eingesetzt werden kann. Die asm.js-Portierung der gesamten Engine, inkl. Umstellung des Grafik-Renderers von OpenGL auf WebGL, wurde mithilfe des Emscripten Transpilers in beeindruckenden vier Tagen fertiggestellt. Was die Performance angeht, so läuft C-/C++-Code, der zu asm.js transpiliert wurde, im Browser nur ca. zweimal langsamer als nativ (vergleichbar mit äquivalentem Code in C# oder Java), was ca. vier- bis zehnmal schneller ist als entsprechender JavaScript-Code.

Allerdings hat asm.js drei große Nachteile. Zum einen der Flaschenhals des Parsers, denn das Parsen des (vereinfachten) JavaScript-Codes kann nicht wegoptimiert werden. Zweitens ist asm.js-Code immer noch Code, und der muss übertragen werden. Natürlich ist Text recht dankbar bei der Komprimierung wie mit GZip, aber die Dekomprimierung des Codes vor dem Parsing ist recht teuer, was die Performance angeht. Drittens hat JavaScript ein paar unschöne Seiten, die sich nicht so einfach umgehen lassen, und solange man auf einem reinen Subset von JavaScript arbeitet, kann man dort keine Optimierungen auf Sprachebene einbauen, die sich nicht auch auf JavaScript selbst niederschlagen würden und gegebenenfalls unangebracht oder gar unsicher wären.

WebAssembly: Die Evolution von asm.js und mehr

Hier kommt WebAssembly ins Spiel: WebAssembly ist ein binärer Zwischencode für eine Stack-basierte virtuelle Maschine wie die JavaScript-Laufzeitumgebung. Der Zwischencode ist optimiert und bildet die Features von asm.js vollständig ab. Hier fallen also schon mal das Parsen und die notwendige Komprimierung für die Übertragung weg. Stattdessen kann die Laufzeitumgebung im Browser den Wasm-Code direkt decodieren und hat schon den vollständigen Syntaxbaum vorliegen, der daraufhin unmittelbar ausgeführt werden kann. Da Wasm nicht mehr als Subset von JavaScript definiert ist, können in folgenden Versionen Optimierungen eingeführt werden, die sich nicht automatisch auch in der JavaScript-Syntax wiederfinden werden.

Wie auch bei asm.js sind die ersten praktischen Anwendungen von WebAssembly dort zu finden, wo die Ausführungsgeschwindigkeit ein kritischer Faktor ist: Spiele. Die Unreal Engine 4 war die erste Spiele-Engine, die schon vor einem Jahr WebAssembly als ein mögliches Plattformziel unterstützt hat. Und natürlich profitieren nicht nur Spiele von Wasm; auch Software zur Videobearbeitung oder komplexe Berechnungskerne existierender Systeme können hiermit performant in den Browser portiert werden. Genauso ist es möglich, einfache Hilfsbibliotheken, die nur in C/C++ zur Verfügung stehen, nach WebAssembly zu portieren und von seiner Webanwendung aus anzusprechen.

Das primäre Ziel von WebAssembly war es, schnell, effizient und portierbar zu sein. Das wurde offensichtlich erreicht, wenn eine komplette Spiele-Engine flott läuft. Ein weiteres Ziel war es, lesbar und debugbar zu sein. Deshalb gibt es Wasm einmal in der binären, optimierten Form, und darüber hinaus auch noch als eine textuelle Repräsentation (wat: WebAssembly Text Format). Diese kann mit dem IL-Code bei .NET verglichen werden, wenn man ihn mit einem Disassembly-Tool anschaut. Weiterhin soll WebAssembly die existierenden Sandboxing-Tools der JavaScript-Ausführungsumgebungen mitnutzen können. Auch dieses Ziel wurde erreicht.

Ein einfaches Beispiel im Code

Doch wie sieht das Ganze nun in der Praxis aus? Schauen wir uns zuerst einmal ein einfaches Beispiel in C an. Wir wollen eine Berechnungsfunktion, die uns in C vorliegt, in den Browser bringen. Diese hochkomplexe Funktion sehen wir in Listing 1. Das visibility-Attribut, das hier verwendet wird, ist etwas, das man in C-Code so üblicherweise nicht sieht. Es wird vom GCC-Compiler ausgewertet und dann bei der Erzeugung des WebAssembly-Codes berücksichtigt.

#define WASM_EXPORT __attribute__((visibility("default")))

WASM_EXPORT
int add(int a, intb) {
  return a + b;
}

Dies führt uns auch gleich zu dem WebAssembly-Code, der aus unserem Stück C-Code erzeugt wird. Diesen sehen wir in seiner textbasierten Form (.wat: WebAssembly Text Format) in Listing 2. Das WebAssembly Text Format erlaubt es uns, den erzeugten Code zu lesen und zu debuggen, auch wenn es nicht schön oder angenehm ist. Es ist angedacht, dass es in Zukunft eine Möglichkeit geben soll, diese wat-Darstellung des Wasm-Codes zum Ursprungscode zu mappen, um hierüber echtes Debugging in den Originalquellen zu ermöglichen. Das entspräche dann in etwa den aktuellen JavaScript Source Maps.

(module
  (type $t0 (func))
  (type $t1 (func (param i32 i32) (result i32)))
  (func $__wasm_call_ctors (type $t0))
  (func $_start (export "_start") (type $t0))
  (func $add (export "add") (type $t1) (param $p0 i32) (param $p1 i32) (result i32)
    get_local $p1
    get_local $p0
    i32.add)
  (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))
)

In Listing 2 sehen wir unsere eigentliche Berechnungsfunktion in den Zeilen 6–9. Der Code davor ist Infrastruktur und beschreibt ein WebAssembly-Modul, das wiederum zwei Typen definiert. Eines ist eine void-Prozedur zum Initialisieren, die als _start exportiert wird, sowie unsere Berechnungsfunktion, die als add exportiert wird. Der Code hinter unserer Funktion ist wiederum Infrastruktur, und diese definiert Eigenschaften des Wasm-Moduls, wie dessen Table-Index und Speichereinstellungen.

Doch wie kommt unser Wasm-Modul nun in den Browser, und wie können wir es ausführen? Wie im Browser üblich, ist unser Einstieg eine ganz klassische HTML-Seite, wie wir sie in Listing 3 sehen. Diese Seite definiert lediglich einen span, in dem später unser Ergebnis angezeigt werden wird, sowie ein Script-Tag, das eine reine JavaScript-Datei nachlädt.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
</head>
<body>
  <span id="container"></span>
  <script src="./main.js"></script>
</body>
</html>

Dieses JavaScript finden wir in Listing 4. Es verwendet das Fetch-API des Browsers, um unsere Wasm-Datei zu laden. Das Fetch-API basiert auf Promises, und sobald die Datei vollständig geladen wurde, wird der erste Callback ausgeführt, den wir im ersten Aufruf von .then() angeben. Dieser gibt uns den eigentlichen Inhalt der Wasm-Datei als ein Byte Array zurück. Mit diesem wird dann im zweiten Callback des WebAssembly ein API aufgerufen, das uns dieses Wasm-Modul instanziieren soll. Auch dieser Aufruf ist ein Promise, das uns sowohl das kompilierte Wasm-Modul als auch bereits eine erste Instanz dieses Moduls zurückliefert. Auf diese greifen wir im dritten Callback zu und legen sie in eine lokale Variable. Im letzten Schritt ermitteln wir mit dem DOM-API des Browsers unser span und weisen ihm einen Text zu. Dieser Text ist das Ergebnis unserer Berechnungsfunktion, die wir hier über die Instanz unseres Moduls auf der Variable instance aus den exportierten Funktionen über .exports.add() aufrufen können.

fetch('../out/main.wasm')
  .then(response => response.arrayBuffer())
  .then(bytes => WebAssembly.instantiate(bytes))
  .then(results => {
    let instance = results.instance;
    document.getElementById("container").innerText = instance.exports.add(1, 2);
  });

Laden wir diese Seite nun in den Browser, wird die HTML-Datei geladen, die unser JavaScript lädt, das unser Wasm-Modul lädt und ausführt, und am Ende sehen wir das Ergebnis unserer Berechnung auf der Webseite. Auf der Seite von WebAssembly Studio ist es möglich, das Ganze in Aktion zu sehen und auch selbst auszuprobieren, ohne lokal etwas installieren zu müssen. WebAssembly Studio unterstützt derzeit C, Rust und AssemblyScript (TypeScript, das mittels Binarys zu WebAssembly kompiliert wird).

Wo geht die Reise hin?

Nicht nur C,C++, Go oder Rust, die alle plattformspezifischen, nativ ausführbaren Maschinencode produzieren, können zu WebAssembly kompiliert werden. Auch Sprachen wie Java, VB.NET, C# oder F#, die eine eigene Runtime benötigen, können Wasm bedienen. Für Java gibt es hier zum Beispiel die TeaVM, die den Java-Bytecode des Programms und der verwendeten Bibliotheken in einen vollständigen, statischen Kompiliervorgang (AOT) in WebAssembly übersetzen kann. Für .NET ist eine vollständige AOT-Übersetzung aktuell noch nicht verfügbar, das Mono-Team ist aber gerade mit der Umsetzung beschäftigt. Was hier aber heute schon funktioniert, ist, eine vollständig nach Wasm übersetzte Mono-Runtime in den Browser zu laden, die dann die Programm-Assemblies im IL-Code nachlädt und sie dann just in time kompiliert und ausführt. Das ist zwar etwas langsamer als eine vollständige AOT-Kompilierung nach WebAssembly, aber es ist dennoch sehr schnell und schon heute ein gangbarer Weg, um .NET-Code im Browser auszuführen.

Wie bereits gesagt, ist WebAssembly Stand heute ein einfaches Upgrade zu asm.js, um dessen Funktionsumfang bereitzustellen. Das Ziel war es jedoch, eine Plattform zu schaffen, die erweiterbar ist, ohne dabei Seiteneffekte auf JavaScript einzuführen. Auf welche Features in WebAssembly können wir uns also in Zukunft freuen? Zuerst seien Threads genannt. Da asm.js nur ein Subset von JavaScript ist, und JavaScript single-threaded daherkommt, ist dies auch eine aktuelle Beschränkung von WebAssembly. Das zu beheben ist eins der ersten größeren Ziele für eine kommende Version von WebAssembly. Mit mehreren Threads wird es dann auch möglich sein, existierenden Code, der von Parallelisierung profitiert, in den Browser zu portieren. Ein weiteres Feature, das auf der Roadmap steht, ist Garbage Collection.

Sehr interessant wird auch Type Reflection sein. Da WebAssembly typisiert ist, ist es möglich, diese Typinformationen über ein JavaScript-API herauszugeben. Damit kann JavaScript dann z. B. instanziierbare Typen oder Funktionen automatisch aus dem Wasm-Modul ermitteln und dynamisch aufrufen. Direkter Zugriff auf Browser-APIs wie das DOM ist auch ein wünschenswertes Feature, denn aktuell muss man JavaScript-Funktionen, die man aus WebAssembly heraus aufrufen möchte, explizit einzeln über ein Modul exportieren und Wasm bekannt machen. Eine Übersicht über weitere geplante Features findet man auf dem GitHub Repository, in dem WebAssembly als Open-Source-Spezifikation weiterentwickelt wird.

Fazit

WebAssembly ist gekommen, um zu bleiben. Es ist eine Ergänzung zu JavaScript und kein Ersatz dafür. In Zukunft werden Wasm und JavaScript Hand in Hand für moderne, mächtige und vor allem performante Anwendungen im Browser sorgen. Und auch wenn WebAssembly Stand jetzt noch eine erste Version ist, so zeigt die praktische Anwendung der Portierung einer kompletten 2-D- und 3-D-Spiele-Engine schon heute, dass es eine grundsolide Plattform ist. Wer vorerst noch auf direkten Zugriff auf Browser-APIs und Multithreading in seinem zu portierenden Code verzichten mag, hat damit schon heute ein mächtiges Werkzeug an der Hand, mit dem er existierenden Code in nahezu beliebigen Sprachen in den Browser portieren kann. Und wer Webanwendungen hat, die er in Zukunft erweitern will, hat jetzt auch die Option, diesen neuen Code nicht unbedingt in JavaScript schreiben zu müssen. Alles in allem ergänzt WebAssembly die Plattform „Browser“ sehr gut, und mit der generellen Verfügbarkeit in allen modernen Browsern steht der Verwendung von Wasm nichts im Wege.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -