Vorwort
Ich hatte es im Seitenleistenchat schon angekündigt: WebAssembly bietet sich eigentlich als Thema für eines meiner berüchtigten Tutorials an - schon, weil es fast alles miteinander verbindet, was ich mag: Webentwicklung, C/C++ und den dringenden Wunsch, JavaScript in das Höllenloch zurückzuwerfen, aus dem es hervorgekrochen ist.
WebAssembly ist im Wesentlichen ein von den Herstellern der
großen Browsermacher Apple, Google, Microsoft und Mozilla vereinbartes, jedoch maßgeblich von Mozilla vorangetriebenes und gestaltetes Format für Binärdateien im Browser, um dem Problem, dass seit zwei Jahrzehnten keine andere dynamische Skriptsprache als JavaScript (ob nun kompiliert oder händisch erstellt) ohne Zusatzsoftware in einem Browser eingesetzt werden kann, entgegenzuwirken, denn JavaScript
ist ziemlich *mist*e. WebAssembly (kurz "WASM") stellt hierbei eine verbesserte Version von Asm.js dar, das einen Teilbereich von JavaScript abdeckt und zwar ebenfalls recht performant ist, jedoch aufgrund der engen Bindung an den gewöhnlichen JavaScript-Parser des Browsers noch immer deutlich langsamer verarbeitet wird als Binärcode. Zumindest dieses Problem hat WebAssembly nicht, denn es hat zwar eine Quellcodedarstellung, der Browser selbst bekommt jedoch nur die kompilierte "Maschinencode"-Datei zu sehen, wobei es sich freilich nicht um
richtigen Maschinencode handelt; aber dazu komme ich noch.
Im Folgenden möchte ich kurz beschreiben, wie eine einfache Hallo-Welt-Anwendung, die im Webbrowser ausgeführt werden kann, in WebAssembly umgesetzt wird. Als Sprache greife ich auf einfaches C zurück, denn einigermaßen ausgereift sind derzeit nur die Sprachbindungen für C und C++ und ich mag C noch etwas mehr als C++. Mit
Blazor gibt es jedoch bereits ein Projekt, um Razor-Webanwendungen (also C#) in WebAssembly zu kompilieren. Es gibt des Weiteren Bestrebungen, WebAssembly künftig unter allen LLVM-fähigen Sprachen (mittlerweile also sogar Fortran) zu ermöglichen, aber das würde den Rahmen dieser Einführung gegenwärtig sprengen.
Einschränkungen
WebAssembly ist zum Entwickeln komplexer Programmlogik - einschließlich irgendwelcher Browserspiele - zwar prima geeignet, kann momentan aber nicht mit dem Dokument selbst interagieren, es ist als Ersatz für jQuery o.ä. also noch unbrauchbar. Eine Interaktion mit JavaScript ist jedoch
möglich.
Browserunterstützung
Dass es einen Konsens über das WebAssembly-Format gibt, bedeutet wie üblich nicht, dass alle Browser den Standard schon jetzt gleichermaßen beherrschen, schon, weil sie unterschiedliche Richtlinien zum Umgang mit lokalen Dateien haben. Da WebAssembly aber noch recht jung ist, wird sich das sicherlich
noch ändern (dazu weiter unten etwas mehr).
Vorbereitungen
Um anzufangen, benötigen wir zuerst einmal einen einigermaßen modernen Webbrowser, Texteditor sowie einen WebAssembly-fähigen Compiler. Derzeit scheint
Emscripten das Mittel der Wahl zu sein, der Pionier Cheerp, der früher einmal anders hieß, wird aber voraussichtlich noch 2017
ebenfalls WebAssembly unterstützen. Es empfiehlt sich, bei der Installation von Emscripten darauf zu achten, dass Python 2.7, falls noch nicht anderweitig installiert, in den Suchpfad aufgenommen wird, denn es wird von den eigenen Tools gebraucht werden.
Standardmäßig ist Emscripten
darauf ausgelegt, die meiste Arbeit weiterhin in JavaScript auszulagern. Für "Standalone"-Module, also solche, die wie eine DLL-Datei funktionieren und weniger Aufwand erfordern, wird, sofern die Releaseversion von Emscripten nicht fehlerfrei funktioniert (die Entwicklung findet durchaus schnell statt) grundsätzlich der Entwicklungszweig "incoming", also die neueste Entwicklungsversion, empfohlen. Man kann den Entwicklungszweig sozusagen im laufenden Betrieb wechseln. Hierfür ist nach erfolgter Installation den Befehlsprozessor der Wahl zu betreten und Folgendes zu tun:
Quelltext
1: 2: 3:
| emsdk install sdk-incoming-64bit emsdk activate --global sdk-incoming-64bit emsdk_env |
Um einfach nur eine veraltete Version auf den neuesten Stand zu bringen (aktuell 1.37.21), genügt ein jeweils etwas schnellerer Befehl:
Quelltext
1: 2: 3:
| emsdk install latest emsdk activate --global latest emsdk_env |
Hallo, Welt!
... in C bedarf selbst in Modulform nicht vieler Erklärung:
C++-Quelltext
1: 2: 3: 4: 5: 6: 7:
| char* sag_hallo(void) { return "Hallo, Entwickler-Ecke!"; } int sag_42(void) { return 42; } |
Um dieses Programm nun vom Webbrowser ausführen zu lassen, brauchen wir erst mal ein wasm-Modul:
Quelltext
1:
| emcc .\hallo.c -Os -s WASM=1 -s SIDE_MODULE=1 -o hallo.wasm |
Laut meinen Tests setzt diese Kombination aus Parametern mindestens das derzeit aktuelle Emscripten 1.37 voraus, noch in der Version 1.35 gibt es hier Kollisionen (SIDE_MODULE und WASM gingen nicht gleichzeitig).
Beim ersten Durchlauf wird Emscripten zuerst einmal ein paar interne Bibliotheken kompilieren, das wird eine Weile dauern. Das sollte ab dem zweiten Mal aber nicht mehr passieren. Die resultierende Datei
hallo.wasm sollte nun den gewünschten Binärcode enthalten.
WebAssembly im Browser
Ganz ohne JavaScript geht es leider noch nicht, denn der
<script>-Tag von HTML unterstützt noch kein WebAssembly. Während davon auszugehen ist, dass sich das spätestens im kommenden Jahr ändern wird (zumindest scheint die Entwicklung dahin zu gehen), muss nun ein bisschen JavaScript eingesetzt werden, um das WebAssembly-Modul zu laden. Mit Stand von heute ist Firefox übrigens der einzige Browser, der Module auch aus
file:// laden kann. Wer keinen Webserver aufsetzen will, um WebAssembly mal kurz auszuprobieren, der sollte also unbedingt Firefox nutzen, sonst wird es nicht funktionieren.
Zwar besitzt Emscripten eine eingebaute Möglichkeit, direkt vorkonfigurierte HTML- und JavaScript-Dateien auszugeben, was ungefähr so funktioniert:
Quelltext
1:
| emcc .\hallo.c -Os -s WASM=1 -s SIDE_MODULE=1 -o hallo.html |
In den meisten Fällen ist dies jedoch nicht ratsam, denn die entstehenden HTML-Dateien sind nicht nur grafisch überfrachtet (es ist aber möglich, eigene HTML-Templates zu nutzen), sondern die beigefügten JavaScript-Dateien sind auch nicht darauf ausgelegt, erweiterbar oder auch nur wartbar zu sein.
Es bietet sich daher, zumal wir gute Entwickler sind und ohnehin einen
modernen Browser voraussetzen, an, vom neuen
Fetch-API Gebrauch zu machen und den WebAssembly-"Client"-Code selbst zu implementieren.
Das sieht etwa so aus:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: 51: 52: 53: 54: 55: 56: 57: 58: 59: 60: 61: 62: 63: 64: 65: 66: 67: 68: 69: 70: 71: 72: 73: 74: 75: 76: 77: 78: 79: 80: 81: 82: 83: 84: 85: 86: 87: 88: 89: 90:
| <!doctype html> <html> <head> <meta charset="utf-8" /> <title>WebAssembly-Test</title> </head> <body> <div id="ausgabe" style="font-size:24px;color:blue;"></div> <script type="text/javascript"> function utf8ToString(arr) { let tempstring = ""; for (i = 0; arr[i]; i++) { tempstring += String.fromCharCode(arr[i]); } return tempstring; } function loadWebAssembly(filename, imports) { return fetch(filename,{ mode: 'no-cors' }) .then(response => response.arrayBuffer()) .then(buffer => WebAssembly.compile(buffer)) .then(module => { imports = imports || {}; imports.env = imports.env || {}; imports.env.memoryBase = imports.env.memoryBase || 0; imports.env.tableBase = imports.env.tableBase || 0; if (!imports.env.memory) { imports.env.memory = new WebAssembly.Memory({ initial: 256 }); } if (!imports.env.table) { imports.env.table = new WebAssembly.Table({ initial: 1, element: 'anyfunc' }); } return [new WebAssembly.Instance(module, imports), imports.env.memory]; }); } if (!('WebAssembly' in window)) { alert("Diese Seite wird bei dir nicht funktionieren, denn dein Browser ist *mist*e."); } else { loadWebAssembly('hallo.wasm') .then(instance => { console.log(instance); let funktion = instance[0].exports._sag_42; let sag_42 = funktion(); let bytes = instance[1].buffer; let sag_hallo = utf8ToString(new Uint8Array(bytes)); let ausgabestring = "Ausgabe von sag_42: " + sag_42 + "<br / >\n" + "String aus dem Speicher: " + sag_hallo; document.getElementById("ausgabe").innerHTML = ausgabestring; }); } </script> </body> </html> |
Nach dem Aufruf der Seite sollte das gewünschte Ergebnis zu sehen sein:
In den Webentwicklertools des Browsers lässt sich die Quellcodedarstellung des kompilierten Moduls ansehen, die aus S-Ausdrücken besteht. Es ist natürlich auch möglich, ein WASM-Modul direkt
in dieser Form zu programmieren.
Das sollte so weit erst mal genügen. Gibt es Fragen? Anregungen? Größere Geldgeschenke?
