WASM: Webassembly mit C/C++
« | 17 Dec 2022 | »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:
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.