WASM: Webassembly mit C/C++

Nun finden also meine ersten Ausflüge ins WASM-Land (Web-ASseMbly) statt. Und neben beeindruckender Möglichkeiten sehe ich leider auch eine große Anzahl von Problemen auf mich zukommen.

Denn es wäre doch zu schön, wenn ich einfach meine GATE-Apps für die “Browser-Plattform” kompilieren könnte, und schon läuft alles online.


Vorgeschichte

Heißt das nicht Java-Applet ?

Java ist schon in den 90ern daran gescheitert, als es mit seinen Applets und Midlets jeden Browser infiziert hatte, die dann alle unter Java Sicherheitslücken litten.
Und am Ende wurde die Technologie abgeschaltet.

Seit über 10 Jahren spukt nun der Gedanke im Äther herum, man könne doch native Anwendungen auch “im Web”, also im Browser laufen lassen.
Man müsste Code in eine Metasprache kompilieren, die der Browser dann in nativen Maschinencode umwandelt und abspielt.
… das wäre dann also nochmal dotNet 1.0.

Und daraus entstand asm.js, aus dem sich der heutige WebAssembly Standard entwickelte.

Das heißt also:

  • Ich schreibe C Code und exportiere C-Funktionen
  • Ich übersetze den C Code in WASM Dateien
  • Ich lade Javascript Code im Browser, der meine WASM-Dateien lädt
  • Ich rufe die in C geschriebenen Funktionen aus JavaScript heraus auf.

(Und natürlich geht das auch mit C++, nur Klassen sind da etwas schwieriger zu verwalten.)

Emscripten + Ninja + CMake

WebAssemblies werden recht einfach mit dem Emscripten EMSDK erstellt. Das ist ein angepasster Compiler mit weiteren Headern und Sourcen, den man herunterladen kann.
Das Setup ist in Tutorials gut beschrieben.

Danach kann man die Environment-Variable EMSDK auf das Installationsverzeichnis setzen und einige weitere Pfade ebenso bedenken:

1set EMSDK=C:\path\to\emsdk
2set EMSDK_NODE=%EMSDK%\node\14.18.2_64bit\bin\node.exe
3set EMSDK_PYTHON=%EMSDK%\python\3.9.2-nuget_64bit\python.exe
4set PATH=%PATH%;%EMSDK%
5SET PATH=%PATH%;%EMSDK%\upstream\emscripten
6SET PATH=%PATH%;%EMSDK%\node\14.18.2_64bit\bin

Jetzt könnte man schon das erste “Hello-World” mit emcc hello.c erzeugen lassen.

Aber interessant wird es erst mit CMake, und dafür brauchen wir unter Windows zuerst das Build-Tool Ninja. Dessen EXE kommt am besten auch in den Pfad:

1SET PATH=%PATH%;C:\path\to\ninja

Und nachdem das EMSDK bereits ein CMake Umgebungstool mitgeliefert hat, braucht man nur noch eine einfache CMakeLists.txt zu seiner C-Datei erstellen und
emcmake cmake path\to\src aufrufen.
Denn emcmake ruft CMake mit den nötigen Parametern auf um Ninja-Buildscripts zu generieren.
Und wenn man am Ende noch ninja im Buildverzeichnis aufruft, startet die Übersetzung und man erhält eine .wasm und eine .js Datei.

Brücke zwischen Browser und WebAssembly

Die Idee ist, dass eine HTML Seite die beim Kompilieren generierte Javascript-Datei lädt, in der zahlreiche Initialisierungsschritte abgearbeitet werden und die WASM-Datei geladen wird.

Hierfür müssen einige Dinge “ausgetauscht” werden:

  • Der C-Code exportiert Funktionen, in der Regel ist main() die erste Wahl.
  • Der Javascript-Code definiert selbst ein paar Funktionen, die als Import-Objekte der WebAssembly zur Verfügung stehen (z.B.: Callbacks für das Lesen und Schreiben von stdin / stdout).
  • Wir erhalten einen Heap-Speicherblock, der von der WebAssembly und Javascript gemeinsam genutzt wird.
  • Und Javascript importiert C-Funktionen (allen voran main) um diese ausführen zu können.

Jetzt kann man WebAssemblies mit unterschiedlichen “Features” kompilieren lassen und dementsprechend anders sieht dann der Javascript Code aus.
Meine erste Annahme war nämlich, man könnte mit einer “generischen” Javascript-Implementierung alle WebAssembly Features abgreifen, doch dem ist leider nicht so.

Problempunkt: Threading

Wenn ich in Richtung GATE Projekt denke ergibt sich ein gröberes Problem: Ich setze gerne Threads ein, doch diese wiedersprechen dem Javascript async-Gedanken.

Denn in Javascript läuft grundsätzlich nur ein Thread, und wenn etwas system-spezifisches länger dauert, wird es als async Funktion im Hintergrund abgearbeitet. Ein sleep() oder andere Formen von blockierenden Funktionen darf es in dieser Welt nicht geben, weil sonst der Browser einfrieren würde.

Am der Stelle wird es kompliziert und man hat mehrere Möglichkeiten mit unterschiedlichen Seiteneffekten.

Asyncify

Kompiliert man den Code mit der ASYNCIFY Option, wird Code erzeugt, der es erlaubt, laufende C-Funktionen zu unterbrechen … also in etwa etwas wie Koroutinen.
So lassen sich dann sleeps() ohne Störungen ausführen und man könnte auch mehrere Aufgaben “anstoßen”, die schrittweise abgearbeitet werden und “kooperativ” immer wieder den Mainthread weiterarbeiten lassen.

Ein Ersatz für Threads ist das allerdings nichts und die Verwaltung von vielen parallelen Funktionsaufrufen erfordert dann sehr viele Kreuzaufrufe zwischen WASM und dem Javascript-Code im Browser.

Pthreads

Unglaublich, aber wahr: Man kann Webassemblies auch mit POSIX Thread-API Support kompilieren. Hierfür werden die neueren Worker-APIs des Browsers der C-Anwendung verfügbar gemacht, und somit entstehen echte parallele Ausführungsschienen. Diese kommunizieren über SharedArrayBuffer-Instanzen und senden sich so Daten und Zustände zu.

Das Problem daran ist: SharedArrayBuffer ist auf Browsern im Standard gesperrt und kann nur durch spezielle HTTP-Header für “Cross-Origin” Scripting aktiviert werden. Außerdem braucht man dann mehrere Javascript-Dateien, weil Worker Objekte mit unterschiedlichen Dateien funktionieren. Man kann also nicht einfach innerhalb einer Script-Datei einen Worker für eine Funktion anlegen.

Worker API

Man kann aber auch direkt mit Web-Worker Objekten arbeiten und eine riesige Menge an Javascript-Patchcode damit umgehen. Auch hier wird mit SharedArrayBuffer und mehreren Javascript-Dateien gearbeitet und auch dafür muss “Cross-Origin” Scripting am Webserver aktiviert sein.

Zumindest hat man hier aber nur leichtgewichtige Konstrukte vor sich und kann die emscripten-Header nutzen, um neue Worker-Threads zu erzeugen.

Fazit

Puh … wie fügt sich das jetzt in mein Weltbild ein ?

Die bisherige Anbindung von Plattformen an das GATE-Framework verlief so, dass im Framework der Plattform-Support-Layer die Umgebungsdetails wegabstrahiert hat alles auf einen Nenner brachte.

Doch WASM funktioniert im Browser nur in feiner Abstimmung mit externen Javascript-Dateien, die wiederum mit Webseiten abgestimmt sind.

Und dann kommt noch die Threading-Misere hinzu.

Ich bin mir noch nicht sicher, ob und wie ich das mit meinen Vorstellungen vereinen kann, doch eines kann ich bereits sagen:

Das Emscripten SDK ist eine hervorragende Möglichkeit, den Spielraum von C/C++ gewaltig zu erweitern.

Denn bisher waren C/C++ Projekte als reine native System-Binaries abgestempelt. Doch als WASM lassen sie sich über alle Mainstream-Browser auf jedes Gerät übertragen um dort ihr gutes Werk zu vollrichten.

📧 📋 🐘 | 🔔
 

Meine Dokus über:
 
Weitere externe Links zu:
Alle extern verlinkten Webseiten stehen nicht in Zusammenhang mit opengate.at.
Für deren Inhalt wird keine Haftung übernommen.



Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!