Die Welt der Webentwicklung befindet sich in einem ständigen Wandel. Mit dem Aufstieg von WebAssembly (Wasm) eröffnen sich völlig neue Perspektiven für die Leistung und Portabilität von Webanwendungen. Besonders die Kombination von Dart, einer modernen Programmiersprache mit statischer Typisierung, und WebAssembly bietet ein großes Potenzial, native Anwendungen performant in Browser und anderen JavaScript-Umgebungen auszuführen. Doch trotz der großen Vorteile gibt es bei der Integration von Dart und WebAssembly auch einige technische Herausforderungen, insbesondere wenn es um die Interaktion zwischen Dart-Code und den nativen Bibliotheken in WebAssembly geht. Diese Lücke wird durch JavaScript-Interop überbrückt – eine Methode, Funktionen und Daten zwischen Dart und WebAssembly via JavaScript auszutauschen.
Dart hat sich als eine beliebte Programmiersprache etabliert, die insbesondere durch das Flutter-Framework große Popularität erlangt hat. Während die Einbindung nativer Funktionen bei mobilen und Desktop-Anwendungen dank der dart:ffi-Bibliothek vergleichsweise unkompliziert ist, ist die Web-Umgebung deutlich komplexer. Bei nativen Zielplattformen wie Android, iOS oder Windows kann dart:ffi direkt externe native Bibliotheken aufrufen und so performant mit externem Code kommunizieren. Für WebAssembly als neues, performantes Webziel gibt es jedoch keinen direkten FFI-Support innerhalb von Dart. Seit 2024 wurde die public API für dart:ffi-WebAssembly-Unterstützung entfernt, was Entwickler vor Probleme stellt, die Dart-Webanwendungen mit nativen WebAssembly-Modulen verbinden möchten.
Die Konsequenz daraus ist, dass WebAssembly-Bibliotheken, die aus C, C++ oder Rust kompiliert wurden, nur noch indirekt über JavaScript-Interop angesprochen werden können. Das bedeutet, dass statt direkter Verweise auf native Funktionsaufrufe mittels FFI der Zugriff auf die WebAssembly-Funktionen über JavaScript gefeiert wird. Die WebAssembly-Compiler liefern meist nicht nur das eigentliche .wasm-Modul, sondern auch eine JavaScript-Wrapperdatei, die das Laden und Interagieren mit dem Modul erleichtert. Dart nutzt hierfür seine robusten JavaScript-Interop-Fähigkeiten (beispielsweise via dart:js_interop oder der package:web), um die Kommunikation zwischen Dart und dem WebAssembly-Modul zu realisieren.
Die praktische Umsetzung dieses Ansatzes sieht vor, die WebAssembly-Funktionen mittels der @JS Annotation aus Dart-Code heraus sichtbar zu machen. Über diese Annotation lassen sich WebAssembly-Funktionen, die im globalen JavaScript-Namespace verfügbar sind, in Dart als externe Funktionen definieren. Für einfache Funktionssignaturen funktioniert dies gut: Eingabe- und Ausgabetypen werden an JavaScript-Typen angepasst oder von Dart automatisch konvertiert. Allerdings ist diese Methode für komplexere, umfangreiche WebAssembly-APIs mit hunderten von Funktionen sehr aufwendig, weil die Bindings manuell erstellt und gepflegt werden müssen. Während für nativen FFI-Code der Einsatz von automatischen Bindinggeneratoren wie ffigen gängige Praxis ist, war für JavaScript-Interop bisher keine vergleichbare Lösung vorhanden.
Die Bindungsdefinitionen mussten mühsam einzeln geschrieben werden, was bei umfangreichen Bibliotheken zu einem enormen Aufwand führte. Hier setzt das Open-Source-Projekt jsgen an – eine an ffigen angelehnte und darauf aufbauende Codegenerierung, die speziell für JavaScript-Interop mit WebAssembly erweitert wurde. jsgen analysiert C-Header und generiert sowohl dart:ffi-kompatible als auch js_interop-kompatible Bindingdateien, sodass Entwickler mit nahezu gleicher Schnittstelle und Syntax arbeiten können. Dieser Ansatz ermöglicht es, den meist aufwändigen Refactoring-Prozess zwischen nativer FFI-Nutzung und WebAssembly-Interop via JavaScript drastisch zu reduzieren. Das heißt, Projekte wie Thermion, bei denen umfangreiche native Bibliotheken eingebunden sind, können relativ einfach auf den WebAssembly-Webbetrieb gehoben werden, ohne dass umfangreiche Änderungen im gesamten Dart-Code nötig sind.
Die Bindings werden nach Zielumgebung per bedingten Importen ausgewählt: Auf nativen Plattformen nutzt der Code dart:ffi, auf Webplattformen js_interop. Ein weiterer zentraler Aspekt der Interoperabilität ist der Umgang mit gemeinsam genutztem Speicher. Im nativen FFI-Kontext ist es möglich, Pointer direkt auf Dart-Objekte oder Speicherbereiche zu übergeben. Damit ist ein direkter, effizienzorientierter Datenaustausch gewährleistet. Die .
address Eigenschaft bei Pointer-Objekten ist hierbei essenziell, da sie den Speicherort im nativen Kontext referenziert. Gleichzeitig wird durch die isLeaf-Einstellung sichergestellt, dass Garbage Collection und Speicherbewegungen während des Funktionsaufrufs ausgeschlossen sind. Bei JavaScript-Interop entfällt diese direkte Adressierbarkeit jedoch. Dart-Code und WebAssembly-Modul existieren jeweils in getrennten Speicherbereichen ohne gemeinsame Adressräume. Über JavaScript lassen sich zwar Werte und Arrays zwischen diesen teilen, jedoch keine reinen Speicheradressen, wie sie im nativen Kontext üblich sind.
Daraus folgt, dass eine analog einfache Pointerübergabe nicht möglich ist, und Entwickler alternative Methoden zur Speicherfreigabe und Datenübergabe implementieren müssen. Eine verbreitete Lösung ist es, über den emscripten-Compiler gemeinsam genutzten Speicher zwischen WebAssembly und JavaScript in Form von typed Arrays zu erzeugen. Emscripten stellt Funktionen bereit, die Speicher dynamisch auf dem WebAssembly-Heap alloziert und eine darauf basierende JavaScript-View als typedArray zurückgeben. Damit kann eine JS-kompatible Speicherbrücke geschaffen werden, um Daten aus Dart über JavaScript an WebAssembly zu übergeben. In Dart kann eine solche Speicherbrücke beispielsweise über statische Extensions für TypedData-Klassen wie Uint8List implementiert werden.
Über JavaScript-Interop-Funktionen werden Speicherblöcke im emscripten-Heap erstellt, in die Dart-Daten kopiert werden. Damit entsteht – trotz Mehrfachkopie – eine Schnittstelle, die in der WebAssembly-Ausführungsumgebung wie ein Pointer behandelt werden kann. Der Entwickler muss die Lebenszeit der eingelagerten Blöcke manuell verwalten, das heißt die evidenten Speicherfreigaben und Bereinigungen durchführen, was die Komplexität vergleichbar zum nativen Speicher-Management erhöht. Dies ist zweifellos ein Kompromiss. Die Speicherübersetzung verursacht Performancekosten durch Kopieren der Daten zwischen Dart-Heap und WebAssembly-Heap.
Zudem entfällt die speichersichere und automatisierte Lebenszeitverwaltung von dart:ffi. Dennoch stellt diese Methode aktuell die bestmögliche Option dar, um mit Dart WebAssembly-Module performant und funktionsreich in Webumgebungen einzubinden. Die Zukunftsaussichten für Dart-WebAssembly-Interop sehen vielversprechend aus. Es ist vorstellbar, dass in kommenden Versionen von Dart ein direkter und standardisierter WebAssembly-FFI-Support implementiert wird, der die Notwendigkeit der JavaScript-Zwischenschicht beseitigt. Dadurch würden native Pointer, Speicher-Handling und Funktionsaufrufe auch im Web so einfach und performant wie bei Plattform-spezifischen Dart-Applikationen möglich sein.
Bis dahin bleibt JavaScript-Interop die tragende Brücke. Die Bemühungen von Open-Source-Projekten wie jsgen und die eleganten Workarounds bei Speicherteilen und Bindungsmanagement verhelfen Dart-Entwicklern jedoch bereits heute zu leistungsfähigen und skalierbaren Lösungen. Wer Webanwendungen mit nativen Komponenten realisieren möchte, findet in Dart und der Kombination mit WebAssembly und JavaScript-Interop eine moderne und zukunftsorientierte Option. Abschließend lässt sich resümieren, dass die Kombination aus Dart und WebAssembly durch JavaScript-Interop erhebliches Potenzial birgt. Die technologische Herausforderung besteht darin, native Effizienz und typisierte Schnittstellen in die Webumgebung zu übertragen.
Dank stetiger Entwicklungen in der Community und innovativen Tools wird dieser Weg kontinuierlich erleichtert. Für Entwickler, die das Optimum aus Performance und Portabilität herausholen wollen, lohnt sich die Beschäftigung mit den Hintergründen und Möglichkeiten der Dart-WebAssembly-Interop. Die Zukunft der Webentwicklung verspricht hier spannende neue Formen der nativen Webausführung und Cross-Plattform-Kompatibilität.