23 Punkte von GN⁺ 2025-06-08 | 1 Kommentare | Auf WhatsApp teilen
  • Erläutert detailliert anhand eines praktischen Rubik’s-Cube-Solver-Beispiels den Prozess, C/C++-Code mit Emscripten nach WebAssembly zu portieren und daraus eine im Browser laufende Web-App zu erstellen
  • Behandelt konkrete Hürden und Lösungswege in Browser-/WebAssembly-Umgebungen Schritt für Schritt, von Hello World über Multithreading, Callbacks, persistenten Speicher bis hin zur Modularisierung
  • Fokussiert sich auf praxisnahes Troubleshooting zu Themen wie asynchroner JavaScript-Initialisierung, Export von Funktionen, Web Workern und Spectre-Problemen sowie persistenter Speicherung über IndexedDB via IDBFS
  • Betont wiederholt, dass die Abstraktionen von Emscripten in der Praxis oft „leaky abstractions“ sind, und unterstreicht die Notwendigkeit, die Grenzen und innere Struktur der Webplattform zu verstehen
  • Ein erfahrungsbasierter Leitfaden, der Entwicklern, die bestehende C/C++-Bibliotheken ins Web portieren möchten, durch praktische Erfahrungen beim Überführen komplexer C-Codebasen ins Web mit nur minimalen JavaScript-/HTML-Kenntnissen konkrete Hilfe und Know-how bietet

Einführung

  • Kürzlich wurde ein Projekt umgesetzt, das den Rubik’s-Cube-Optimal-Lösungsalgorithmus als Web-App implementiert
  • Dokumentiert wird der Prozess, einen in C entwickelten Rubik’s-Cube-Optimierungssolver mit Emscripten zu kompilieren und als WebAssembly im Webbrowser auszuführen
  • Der Hauptgrund für den Einsatz von WebAssembly ist, im Web eine nahezu native Performance im Vergleich zu JavaScript zu erreichen
  • Dieser Artikel ist kein klassisches Webentwicklungs-Tutorial, sondern ein „Leidensweg“ für Entwickler, die bestehenden C/C++-Code ins Web portieren möchten
  • Auch ohne viel Erfahrung in der Webentwicklung ist er nachvollziehbar, solange man die Grundstruktur von HTML und JavaScript sowie den Umgang mit Browser-Entwicklertools kennt

Umgebung einrichten

  • Sämtlicher Beispielcode ist im git-Repository und auf github verfügbar
  • Emscripten muss installiert werden (Installationsanleitung siehe offizielle Website); als Webserver können etwa darkhttpd oder Python http.server verwendet werden
  • Die Tutorial-Codebeispiele wurden unter Linux und UNIX-artigen Systemen getestet. Windows-Nutzern wird WSL (Windows Subsystem for Linux) empfohlen

Hello World

  • Wenn man Hello World in C mit dem Befehl emcc -o index.html hello.c kompiliert, entstehen drei Dateien: index.html (Webseite), index.wasm (WebAssembly-Bytecode) und index.js (JavaScript glue code)
  • Die Ausgabe kann sowohl im Browser als auch unter Node.js laufen; für beide Umgebungen gibt es unterschiedliche Einsatzweisen
  • Um nur .wasm zu erzeugen, wird die Option -sSTANDALONE_WASM verwendet
  • Zwar kann Emscripten auch nur .wasm erzeugen, in den meisten Fällen ist jedoch JavaScript glue code unverzichtbar

Intermezzo I: Was ist WebAssembly?

  • WebAssembly (WASM) ist eine Low-Level-Sprache, die in einer hochperformanten virtuellen Maschine innerhalb des Webbrowsers ausgeführt wird
  • WASM wird seit 2017 von allen wichtigen Browsern unterstützt
  • Ursprünglich wandelte Emscripten C/C++-Code in asm.js, eine Teilmenge von JavaScript, um; mit dem Aufkommen von WASM erfolgte der Wechsel
  • Es gibt auch eine Textdarstellung, und die Struktur ist stackbasiert. Bis vor Kurzem wurde nur eine 32-Bit-Architektur unterstützt, sodass nicht mehr als 4 GB Speicher nutzbar waren; WASM64 wird inzwischen schrittweise in Browsern eingeführt

Bibliothek bauen

  • Es wird ein einfaches Grundbeispiel gezeigt, bei dem die C-Funktion multiply() zu WASM gebaut und anschließend aus JavaScript aufgerufen wird
  • Beim Standard-Build versieht Emscripten Funktionsnamen mit einem Unterstrich (_), z. B. _multiply
  • Für die externe Freigabe von Funktionen muss die Option -sEXPORTED_FUNCTIONS gesetzt werden
  • Da die Initialisierung beim Laden der Bibliothek asynchron erfolgt, ist eine asynchrone Behandlung über onRuntimeInitialized oder await erforderlich
  • Der Übungscode befindet sich im Ordner 01_library des Repositories

Intermezzo II: JavaScript und DOM

  • Um in JavaScript auf Bestandteile von HTML zuzugreifen und sie zu verändern, muss das Document Object Model (DOM) genutzt werden
  • Mit Event Listenern (addEventListener) und eingebauten Operatoren/Funktionen lassen sich dynamische UIs umsetzen
  • Für die Beispiele wird eine grundlegende HTML-/JavaScript-Struktur mit Eingabe, Button und Ergebnisanzeige erläutert
  • Es werden auch praktische Vorgehensweisen und Probleme beim Trennen/Zusammenführen von Skripten erklärt, etwa die Nutzung von defer und die Lade-Reihenfolge von DOM-Elementen

Bibliothek modularisieren und laden

  • Damit sich WASM-Bibliotheken mehrfach einbinden oder sowohl unter Node.js als auch im Web wiederverwenden lassen, können sie mit den Optionen MODULARIZE und EXPORT_NAME als Modul gebaut werden
  • Die Erweiterung .mjs wird aus Gründen der Node.js-Kompatibilität empfohlen
  • So lässt sich das Modul sowohl im Web als auch unter Node mit import MyLibrary from ... verwenden

Multithreading

  • In WebAssembly lässt sich pthread-basiertes Multithreading portieren, um die Performance zu steigern
  • Innerhalb einer Funktion werden mehrere Threads erzeugt, um Rechenaufgaben parallel auszuführen (z. B. das Zählen von Primzahlen)
  • Beim Build werden die Optionen -pthread und -sPTHREAD_POOL_SIZE= benötigt
  • In realen Browsern müssen zusätzlich HTTP-Header wie Cross-Origin-Opener-Policy: same-origin und Cross-Origin-Embedder-Policy: require-corp gesetzt werden
  • Alle Beispiele finden sich im Ordner 03_threads des Repositories

Intermezzo III: Web Workers und Spectre

  • Emscripten-Multithreading wird mit Web Workers umgesetzt (Web Workers sind separate Prozesse mit nachrichtenbasierter Kommunikation)
  • Die Nutzung von SharedArrayBuffer unterliegt sicherheitsbedingten Einschränkungen
  • Nach dem Auftreten der Spectre-Schwachstelle im Jahr 2018 wurden Anforderungen an Cross-Origin Isolation sowie die entsprechenden Header verpflichtend

Vorsicht vor dem Blockieren des Main Threads

  • Wenn lange laufende Aufgaben den Main-UI-Thread des Browsers BLOCKIEREN, verschlechtert sich die Benutzererfahrung drastisch
  • Um das zu vermeiden, werden Web Worker eingeführt: Verarbeitung von UI/Eingaben und Rechenlogik werden klar getrennt
  • Die ereignisbasierte Kommunikation zwischen Main Thread und Worker wird mit postMessage und onmessage umgesetzt
  • Im Web Worker wird das Emscripten-WASM-Modul geladen und übernimmt ausschließlich asynchrone Berechnungen

Callback-Funktionen

  • Wird ein Funktionszeiger (Callback) als Parameter an eine C-Funktion übergeben, ist keine automatische Anbindung an JavaScript-Funktionsobjekte möglich
  • Dafür müssen von Emscripten bereitgestellte Funktionen wie addFunction() und UTF8ToString() genutzt werden; beim Build sind zusätzlich -sEXPORTED_RUNTIME_METHODS und -sALLOW_TABLE_GROWTH erforderlich
  • Callbacks funktionieren nur stabil, wenn sie ausschließlich im Main Thread aufgerufen werden (aus Web Workern heraus ist kein Zugriff möglich)

Persistenter Speicher

  • Um Daten dauerhaft im Browser des Nutzers zu speichern, wird IDBFS von Emscripten verwendet, ein auf IndexedDB basierendes Dateisystem
  • Beim Build sind Flags wie --lidbfs.js und --pre-js für die anfängliche Einrichtung nötig
  • Im C-Code können Dateiein-/ausgabefunktionen wie fopen, fread und fwrite unverändert genutzt werden; damit Daten jedoch tatsächlich übernommen und synchronisiert werden, sind in JavaScript explizites Mapping und Sync-Verarbeitung erforderlich
  • Aufgrund der Sandbox- und Sicherheitsrichtlinien des Browsers ist direkter Zugriff auf das lokale Dateisystem nur in Node.js möglich; im Browser müssen für sichere persistente Speicherung Backends wie IDBFS verwendet werden

Fazit

  • Über den gesamten Ablauf dieses Tutorials hinweg lässt sich im Detail lernen, wie komplexer nativer C/C++-Code mit nur minimalem JavaScript und HTML sicher und ohne Performanceverlust im Browser ausgeführt werden kann
  • In einer realen Umgebung erlebt man die Hürden und Lösungen aller zentralen Themenbereiche – Multithreading, Callbacks, asynchrone Verarbeitung und Speicheranbindung – und lernt zugleich aktuelle Trends wie passende Konfigurationen und Browser-Einschränkungen kennen
  • Die bereitgestellten Git-Repository-Beispiele können als Grundlage für die eigene Anwendung genutzt und erweitert werden

1 Kommentare

 
GN⁺ 2025-06-08
Hacker-News-Kommentare
  • Man wünschte, es wäre hervorgehoben worden, dass nur die Dateiendung von .js zu .mjs geändert wurde; das trifft genau das frustrierende Gefühl, dass man in der Realität unabhängig von der verwendeten Endung auf Probleme stößt. Aus der Perspektive von jemandem, der von dojo über CommonJS, AMD, ESM, webpack, esbuild und rollup schon viele Modulsysteme genutzt hat, ist das wirklich 100% nachvollziehbar.
    • Der Wechsel von CommonJS zu ESM war ein enormer Umbruch, fast wie der Übergang von Python 2 zu Python 3, aber im Verhältnis zu den Erwartungen scheint der Nutzen gering und der Aufwand nur größer geworden zu sein. Viele Bibliotheken unterstützen inzwischen nur noch ESM, sodass man heute oft im npm-Tab versions einfach die in den letzten 30 Tagen meistgeladene Version auswählt, weil das mit hoher Wahrscheinlichkeit die letzte CommonJS-Version ist. ESM ist zwar eindeutig ein fortschrittlicheres Modulsystem, aber dass tc39 es fast absichtlich inkompatibel zu CommonJS gemacht hat, etwa mit Top-Level await, ist ehrlich gesagt kaum nachvollziehbar.
    • Die Geschichte der Module in JS fühlt sich wirklich fast wie ein Trauma an. Jetzt wurden sogar noch Import Maps im Browser eingeführt, und man fragt sich schon, welche interessanten(?) Probleme als Nächstes auftauchen werden.
    • Kürzlich habe ich erfahren, dass das Function-Objekt zur Laufzeit beliebigen JS-Code kompilieren kann, und in meiner Umgebung, in der ich nicht einmal import verwenden kann, ist das zu einer Art Lebensader geworden und sehr nützlich. Im JS-Ökosystem braucht man das vielleicht nicht besonders oft, aber mir hilft es enorm.
    • Deshalb sollten alle einfach bun.sh verwenden.
    • Könnte man nicht auch .esm.js verwenden?
  • Wenn man noch mehr Stellen benennen will, die in diesem Artikel langfristig Probleme verursachen könnten, würde ich empfehlen, statt var lieber let oder const zu verwenden. var funktioniert zwar weiterhin, aber die meisten heutigen JS-Entwickler verbieten die Nutzung von var per Linter. var unterstützt nur Function Scope, was für Entwickler aus den meisten anderen Sprachen früher oder später verwirrend ist. Als Beispiel für Portierungsprobleme nativer Apps wurde genannt, dass Copy-and-Paste zur Compile-Zeit mit hartkodierten Ctrl-C- und Ctrl-V-Tastenkombinationen umgesetzt wurde, was unter Linux und Windows funktioniert, auf dem Mac aber nicht. Im Web sollte man stattdessen copy- und paste-Events erkennen. Selbst in Frameworks wie Unity hat man gesehen, dass wegen fest verdrahteter Tasten auf dem Mac Kopieren und Einfügen nicht funktioniert. Für die meisten Spiele ist das egal, aber sobald man Web-Funktionen mit Copy-and-Paste-Bedarf ausliefert, wird es zum Problem.
  • Klage darüber, wie sehr man Multithreading im Web bzw. in NodeJS hasst. Schade sei, dass statt Synchronisationsprimitiven wie mutex oder rwlock, mit denen sich Werte selbst zwischen Kontexten wie z. B. v8 isolates übertragen ließen, letztlich nur der praktisch kaum nützliche SharedArrayBuffer eingeführt wurde. Die Thread-Synchronisation läuft am Ende doch auf Thunking und Datenkopien über eine RPC-Schicht hinaus. Unsere Produktions-App im Unternehmen ist eine riesige Anwendung, die 70 bis 100 GB RAM nutzt, was schon so war, bevor ich sie gebaut habe. Daher suchen wir nach einer eigenartigen Lösung auf Basis nativen Codes, bei der Speicherseiten und benutzerdefinierte Datenstrukturen direkt verwaltet werden, um Serialisierung und Deserialisierung zu minimieren. V8 verwendet für String-Encoding UTF-16, wodurch die Arbeit mit JS-Werten in der nativen Schicht teuer wird.
    • Man fragt sich, ob diese App mit 100 GB RAM wirklich überhaupt eine Web-App sein musste. Das klingt eher nach einem internen Tool, das in einer Sprache wie C# hätte geschrieben werden sollen.
  • Dieses Ökosystem ist so nahe am Chaos, dass selbst die Bezeichnung „Masochist“ schon fast normal klingt.
    • Man könnte auch einfach sagen, dass das Chaos bereits eingebaut ist.
  • Der Artikel selbst ist gut geschrieben, und noch dazu überrascht die Wahl eines schwierigen und komplexen Einstiegswegs. Man merkt wieder, dass die Projekteinrichtung der härteste Teil ist. Lob dafür, dass man sofort auf Sicherheits-/Header-Probleme gestoßen ist, wobei die erwartete Ursache oft CORS ist. Bei uns im Unternehmen wird ebenfalls mit emscripten/C++ gebaut, und mit WebGPU/Shadern sowie WebAudio steht offenbar noch ein deutlich härterer Weg bevor.
  • Früher dachte ich vage, Code im Browser zu kompilieren müsse „langsam“ sein, aber der OP erklärt gut, dass das nicht so ist. Auch das Emscripten-Projekt betont, dass „dank der Kombination aus LLVM, Emscripten, Binaryen und WebAssembly die Ausgabe klein ist und nahezu mit nativer Geschwindigkeit läuft“ (emscripten.org).
    • Für mich ist heute so ein Tag wie mit dem „Yellow-Bus-Syndrom“: Bis letzte Woche kannte ich Emscripten nicht, dann stieß ich beim Anschließen von SDL an mein Projekt in CMake auf einen Kommentar mit den Targets APPLE, MSVC und EMSCRIPTEN, und heute begegne ich auf HN schon wieder Emscripten. Jetzt ist wohl der Zeitpunkt gekommen, mir wirklich Zeit zu nehmen und tiefer einzusteigen.
    • Die Formulierung „nahezu native Geschwindigkeit“ wirkt ziemlich subjektiv. Ich konnte in der Dokumentation keine Zahlen dazu finden, wie schnell es tatsächlich ist.
  • Der Artikel war hilfreich, und ich selbst plane ebenfalls, einen in C geschriebenen Compiler nach WebAssembly zu kompilieren und als Web-Playground bereitzustellen. Zur Info: In aktuellen Browsern kann man SQLite über JavaScript verwenden; ich frage mich, ob das auch in wasm möglich ist. Falls emscripten die sqlite-API-Aufrufe aus C mit einer SQLite-Datenbank im Browser verbinden könnte, wäre das ideal und definitiv eine nähere Untersuchung wert.
  • Ich frage mich, warum für SSL Port 48 verwendet wurde. Gab es dafür einen besonderen Grund?
    • Die Antwort lautet, dass der Port zufällig vom Namen H48 abgeleitet wurde. Diese Web-App benötigt zusätzliche HTTP-Header, deshalb wurde einfach ein anderer Port verwendet, um das ohne Auswirkungen auf die gesamte Website umzusetzen. Es gibt auch eine Weiterleitung auf https://h48.tronto.net, und später wolle man entweder das Setup mit httpd und relayd unter OpenBSD weiter verbessern oder gleich auf eine separate Domain umziehen.