Autor Beitrag
hydemarie
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 475
Erhaltene Danke: 51



BeitragVerfasst: Do 07.09.17 21:41 
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:

ausblenden 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:

ausblenden 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:

ausblenden 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:

ausblenden 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:

ausblenden 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:

ausblenden volle Höhe HTML-Dokument
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) {  
             // Wandelt UTF-8-Array in String um.  
             let tempstring = "";  
             for (i = 0; arr[i]; i++) {  
                 tempstring += String.fromCharCode(arr[i]);  
             }  
             return tempstring;  
         }  
  
         function loadWebAssembly(filename, imports) {  
             // Hilfsfunktion, um ein WebAssembly aus einer Datei zu laden.  
             // Wird hoffentlich demnächst überflüssig, sobald Mozilla direkten  
             // Import aus WASM erlaubt.  
             return fetch(filename,{  
                 // CORS abschalten, damit es auch lokal funktioniert:  
                 mode: 'no-cors'  
             })  
                 .then(response => response.arrayBuffer())  
                 .then(buffer => WebAssembly.compile(buffer))  
                 .then(module => {  
                     // Legt ein Modul für dieses WebAssembly an.  
                     // Da die Speicherverwaltung von C/C++ für WebAssembly nicht  
                     // greift, müssen wir den Speicher händisch initialisieren.  
                     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' // dies ist aktuell die einzig mögliche Angabe
                         });  
                     }  
  
                     // Promise zurückgeben:  
                     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 => {  
                     // instance[0]: WebAssembly.Instance  
                     // instance[1]: WebAssembly.Memory  
                     //  
                     // Zum Debuggen:  
                     console.log(instance);  
  
                     // Zunächst das Einfachere - einen Integerwert ausgeben:  
                     let funktion = instance[0].exports._sag_42;  
                     // Dieser Unterstrich -------------^ ist eine Kurzform von
                     // "Module.ccall()".  
  
                     let sag_42 = funktion(); // In "sag_42" sollte jetzt 42 stehen.
  
                     // Jetzt zum Schwierigeren: einen String ausgeben. WebAssembly
                     // kann nämlich leider nicht mit Strings umgehen. Aber durch
                     // Direktzugriff auf den Speicher (WebAssembly.Memory) können
                     // wir den String trotzdem auslesen, er ist als Byte-Array ge-
                     // speichert:
                     let bytes = instance[1].buffer;
                     let sag_hallo = utf8ToString(new Uint8Array(bytes));  
  
                     let ausgabestring = "Ausgabe von sag_42: " + sag_42 +
                                         "<br / >\n" + // Hinweis: Leerzeichen entfernen, die Kack-Boardsoftware zeigt das nicht richtig an...
                                         "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:

MXX1Hup

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? :)
Einloggen, um Attachments anzusehen!

Für diesen Beitrag haben gedankt: cryptnex, FinnO
cryptnex
ontopic starontopic starontopic starontopic starofftopic starofftopic starofftopic starofftopic star
Beiträge: 23
Erhaltene Danke: 5



BeitragVerfasst: Fr 08.09.17 23:18 
Vielen Dank für Dein Tutorial!

Ich habe ein paar Fragen zu WebAssembly: Ist es möglich (mit Strg+U) den Quellcode zu sehen oder erhält man nur ein lange Liste an Binärcode, den man erst mithilfe eines Decoders versteht? Wie gestaltet sich das Debugging? Wird es dazu auch noch einmal ein Tutorial geben? Bedeutet WASM das Ende des offenen Internets? :)
hydemarie Threadstarter
ontopic starontopic starontopic starontopic starontopic starontopic starontopic starofftopic star
Beiträge: 475
Erhaltene Danke: 51



BeitragVerfasst: Fr 08.09.17 23:25 
Du bekommst die S-Expressions - "den Quellcode" wirst du niemals sehen. So ist das mit Kompiliertem. ;)
Und ja, das ist tatsächlich ein Problem - aber die Binärdarstellung ist wenigstens standardisiert und dokumentiert.