17. Sep 2018
Lesedauer 10 Min.
C# – auch in Ihrem Browser
WebAssembly und Mono
Mit der WebAssembly-Ausgabe von Mono macht C# im Webbrowser erste Gehversuche: lauffähig auf Mobil- und Desktop-Plattformen, ganz ohne Browser-Plug-in.

Das Web ist in den vergangenen Jahren zu einer ernst zu nehmenden Ausführungsumgebung für Client-Anwendungen avanciert. Zentraler Vorteil ist neben der Plattformunabhängigkeit die besonders einfache Bereitstellung dieses Anwendungstyps. So verwundert es kaum, dass .NET Core bis jetzt noch keine ernst zu nehmende Frontend-Technologie in seinem Lieferumfang enthält – von der kürzlich eingeführten Unterstützung für die Windows Presentation Foundation (WPF) und Windows Forms abgesehen, die aber wieder nur unter Microsoft Windows betrieben werden können (siehe [1]).In der dotnetpro 1/2018 haben wir Ihnen unter [2] bereits drei Möglichkeiten gezeigt, wie Sie C# mit Webtechnologien verknüpfen können, um webbasierte Frontends für moderne Businessanwendungen zu schreiben. Seitdem hat sich die Welt weitergedreht und Microsoft hat Blazor vorgestellt, ein experimentelles Framework des ASP.NET-Core-Teams zur Entwicklung von Single Page Web Applications (SPA), die komplett im Browser laufen. Dabei kommen die .NET-Sprache C# und die von ASP.NET MVC oder ASP.NET Web Pages bekannte Templating-Engine Razor zum Einsatz. Unter der Haube tickt Mono – oder genauer: eine Portierung der Mono-Laufzeitumgebung für WebAssembly.
WebAssembly: Ein Bytecode für das Web
Das technologische Fundament für mono-wasm stellt WebAssembly (kurz: Wasm) dar, ein Low-Level-Bytecode für das Web. Dieser zeichnet sich durch eine besonders hohe Ausführungsgeschwindigkeit aus. Was die übrigen Entwicklungsziele [3] angeht, ist WebAssembly- schnell, effizient und portabel,
- für den Menschen lesbar und einfach zu debuggen,
- sicher, weil es derselben Sandbox unterliegt, die auch für die übrigen Webanwendungen gilt,
- und zudem Teil des offenen, abwärtskompatiblen Web.
Mono: Das Rückgrat der .NET-Cross-Platform-Entwicklung auf dem Client
Die quelloffene .NET-Laufzeitumgebung Mono stellt Microsofts aktuelle Strategie für plattformübergreifend ausführbare Anwendungen dar. So basiert etwa Xamarin, das von Microsoft angebotene Produkt zur Multi-Plattform-Entwicklung über verschiedene mobile Plattformen hinweg, auf ebenjener Laufzeitumgebung. Auch die Spiele-Engine Unity nutzt Mono, um einmal entwickelte Spiele auf unterschiedlichen Plattformen (Mobil, Desktop, Spielekonsolen) bereitstellen zu können. Mit dieser Breite von Plattformen war Miguel de Icaza, Begründer des Mono-Projekts, jedoch noch nicht zufrieden. Denn auf der wohl weitverbreitetsten Plattform, dem Webbrowser, lief Mono noch nicht. So kündete de Icaza im August 2017 an, Mono auch nach WebAssembly und somit in aktuelle Webbrowser zu bringen [6]. Im Blogpost wurden zugleich zwei Prototypen vorgestellt.- JiT (Just in Time): Bei diesem Vorgehen wird die auf C (und damit eine der „Referenzsprachen“ für WebAssembly) basierende Mono-Runtime nach WebAssembly kompiliert. Auf dieser Laufzeitumgebung wird anschließend Intermediate-Language-Code (IL) ausgeführt – was jedoch zulasten der Performance geht.
- AoT (Ahead of Time): Hierbei werden sowohl die Mono-Laufzeitumgebung und die Mono-Klassenbibliotheken als auch der Quellcode des Entwicklers statisch nach WebAssembly kompiliert – was jedoch deutlich länger dauert.
Listing 1: Ausführen der AoT-Build-Pipeline
$ mcs -nostdlib -noconfig -r:../../dist/lib/mscorlib<span class="hljs-selector-class">.dll</span> hello<span class="hljs-selector-class">.cs</span> -out:hello<span class="hljs-selector-class">.exe</span> <br/>$ mono-wasm -<span class="hljs-selector-tag">i</span> hello<span class="hljs-selector-class">.exe</span> -o output <br/>$ cp index<span class="hljs-selector-class">.html</span> output <br/>$ ls output <br/>hello<span class="hljs-selector-class">.exe</span> index<span class="hljs-selector-class">.html</span> index<span class="hljs-selector-class">.js</span> index<span class="hljs-selector-class">.wasm</span> mscorlib<span class="hljs-selector-class">.dll</span>
- Der C#-Code des Anwenders (sowie weitere Abhängigkeiten, etwa die mscorlib) wird mithilfe des Mono-C#-Compilers (mcs) in die Intermediate Language (IL) überführt.
- Das Kommandozeilen-Tool mono-wasm nimmt IL-Assemblies entgegen und überführt diese in Bitcode, der sich zur Weiterverarbeitung durch die Compilerplattform LLVM eignet. Diese besitzt ein Backend zur Ausgabe für Wasm. In das Ausgabeverzeichnis wird am Ende die darüber generierte Binärdatei index.wasm sowie der nötige Glue Code zur Kommunikation mit dem nach Wasm überführten C#-Code in JavaScript (index.js) kopiert, sowie auch der IL-Code.
- Abschließend wird noch die Datei index.html als Einsprungpunkt der Website in das Ausgabeverzeichnis kopiert. Das Gesamtprojekt schlägt derzeit noch mit rund 10 MByte Dateigröße zu Buche.
Listing 2: Implementierung von hello.cs
<span class="hljs-keyword">using</span> System; <br/><br/><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Hello</span> { <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Main</span>(<span class="hljs-params"><span class="hljs-keyword">string</span>[] args</span>) </span>{ <br/> <span class="hljs-keyword">var</span> hello = <span class="hljs-keyword">new</span>[] { <span class="hljs-string">"hello"</span>, <span class="hljs-string">"dotnetpro"</span>, <br/> <span class="hljs-string">"this"</span>, <span class="hljs-string">"is"</span>, <span class="hljs-string">"wasm"</span> }; <br/> Console.WriteLine(String.Join(<span class="hljs-string">" "</span>, hello)); <br/> Yell(<span class="hljs-string">"Hack"</span>); <br/> } <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Yell</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> text</span>) </span>{ <br/> Console.WriteLine(<span class="hljs-string">$"<span class="hljs-subst">{</span></span><br/><span class="hljs-string"><span class="hljs-subst"> text.ToUpperInvariant()}</span>!!!"</span>); <br/> } <br/>}
Die Main-Methode wird bei Ausführung im Browser initial aufgerufen. Danach besteht in beide Richtungen die Möglichkeit, zu kommunizieren: So ruft Mono das benutzerdefinierte Ereignis WebAssemblyContentLoaded auf, nachdem die Wasm-Anwendung vollständig hochgefahren wurde. Auf dieses kann sich die mit HTML, JavaScript und CSS geschrieben Webanwendung auf Wunsch registrieren.Listing 3 zeigt, wie sich aus JavaScript heraus Methoden aufrufen lassen, die in der WebAssembly enthalten sind: Mit der Methode MonoClass erhält der Aufrufer Zugriff auf eine in der WebAssembly enthaltene Klasse. Der erste Parameter enthält den Namespace – hier eine leere Zeichenkette –, der zweite Parameter den Klassennamen – dies zeigt Zeile 12. War der Zugriff erfolgreich, wird eine von null verschiedene Zahl zurückgeliefert, die als Klassenreferenz dient. Existiert die Klasse nicht, gibt der Methodenaufruf die Zahl null zurück. Mithilfe von MonoMethod kann eine Methode auf dieser Klasse aufgerufen werden (Zeile 13): Dazu wird die Klassenreferenz übergeben, der Name der Zielmethode definiert und im dritten Parameter angegeben, ob die Methode statisch ist oder nicht. In unserem Beispiel ist sie das, daher wird hier true gesetzt. Auch in diesem Fall ist die Rückgabe eine von null verschiedene Methodenreferenz, wenn die Methode aufgelöst werden konnte. Schließlich wird der Methodenaufruf mittels MonoInvoke durchgeführt (Zeile 14): Als erster Parameter kann der Pointer für this angegeben werden, dann folgt die oben aufgelöste Methodenreferenz, dann die Parameteraufzählung. Da es sich um den Aufruf einer statischen Methode handelt, wird der this-Pointer nicht belegt.
Listing 3: Aufruf von Methoden in WebAssembly
01 <span class="hljs-meta">&lt;!DOCTYPE html&gt;</span> <br/>02 <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span> <br/>03 <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span> <br/>04 <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"X-UA-Compatible"</span> </span><br/><span class="hljs-tag"> <span class="hljs-attr">content</span>=<span class="hljs-string">"IE=edge"</span>&gt;</span> <br/>05 <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>WebAssembly Example<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span> <br/>06 <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=</span><br/><span class="hljs-tag"> <span class="hljs-string">"width=device-width, initial-scale=1"</span>&gt;</span> <br/>07 <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span> <br/>08 <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span> <br/>09 <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"index.js"</span>&gt;</span><span class="undefined"></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> <br/>10 <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript"> </span><br/><span class="javascript"><span class="hljs-number">11</span> <span class="hljs-built_in">document</span>.addEventListener(</span><br/><span class="javascript"> <span class="hljs-string">'WebAssemblyContentLoaded'</span>, () =&gt; { </span><br/><span class="javascript"><span class="hljs-number">12</span> <span class="hljs-keyword">const</span> klass = MonoClass(<span class="hljs-string">''</span>, <span class="hljs-string">'Hello'</span>); </span><br/><span class="javascript"><span class="hljs-number">13</span> <span class="hljs-keyword">const</span> method = MonoMethod(klass, <span class="hljs-string">'Yell'</span>,</span><br/><span class="javascript"> <span class="hljs-literal">true</span>); </span><br/><span class="javascript"><span class="hljs-number">14</span> MonoInvoke(<span class="hljs-number">0</span>, method, [<span class="hljs-string">'Hallo'</span>]); </span><br/><span class="javascript"><span class="hljs-number">15</span> }); </span><br/><span class="javascript"><span class="hljs-number">16</span> </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> <br/>17 <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span> <br/>18 <span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
All diese Methoden sowie das benutzerdefinierte Ereignis werden übrigens durch den von mono-wasm bereitgestellten Glue Code in index.js (Zeile 9) definiert. Bild 3 zeigt die Ausführung dieser Anwendung im Webbrowser Google Chrome unter macOS: Zunächst erscheint die aus dem Array hello zusammengesetzte Zeichenkette, dann wird die Methode Yell mit dem Wert Hack aufgerufen, dann aus JavaScript heraus mit der Zeichenkette Hallo, und schließlich werden beide korrekt umgewandelt auf der Konsole ausgegeben.
Doch auch der umgekehrte Weg ist möglich: der Zugriff auf das Document Object Model (DOM) der dargestellten Website aus C# heraus. Mono stellt dazu den Namespace Mono.WebAssembly bereit, der eine einfach zu verwendende Schnittstelle zur Interaktion mit dem DOM verfügbar macht. Auf dem globalen Objekt HtmlPage kann der C#-Entwickler zum Beispiel die Referenz auf das Document-Objekt der Website erhalten, wie Listing 4 zeigt. Dabei wurde die Yell-Methode dergestalt abgewandelt, dass sie das Ergebnis nicht nur auf der Konsole ausgibt, sondern zugleich auch den Inhalt der HTML-Seite dadurch ersetzt (Zeile 10). Die erfolgreiche Ausführung zeigt Bild 4.
Listing 4: Zugriff auf das DOM aus mono-wasm
<span class="hljs-symbol">01 </span><span class="hljs-keyword">using</span> Mono.WebAssembly; <br/><span class="hljs-number">02</span> <span class="hljs-keyword">using</span> <span class="hljs-keyword">System</span>; <br/><span class="hljs-number">03</span> <br/><span class="hljs-number">04</span> public class Hello { <br/><span class="hljs-number">05</span> // ... <br/><span class="hljs-number">06</span> <br/><span class="hljs-number">07</span> public static void Yell(string text) { <br/><span class="hljs-number">08</span> var yelled = $<span class="hljs-string">"{text.ToUpperInvariant()}!!!"</span>; <br/><span class="hljs-number">09</span> Console.WriteLine(yelled); <br/><span class="hljs-number">10</span> HtmlPage.Document.Body.InnerText = yelled; <br/><span class="hljs-number">11</span> } <br/><span class="hljs-number">12</span> }
Blazor: .NET im Browser
Die obigen Beispiele sind tatsächlich eher unspektakulär. Für uns als .NET-Entwickler stellt sich schließlich die Frage, welcher Mehrwert auf Basis dieser Technologie generiert werden kann. Microsoft selbst liefert mit seinem Experiment Blazor die Steilvorlage: Blazor ist ein Framework zur Implementierung von Single Page Applications (SPA). Der Name des Projekts rührt von der Idee „Razor im Browser“ her. Ganz im Gegensatz zu gängigen SPA-Frameworks wie Angular, Vue.js oder React wird die Logik hier allerdings nicht in JavaScript (oder einer darauf aufbauenden Sprache wie TypeScript) entwickelt, sondern in C#. Die Views wiederum schreibt man nicht in reinem HTML, sondern mithilfe der Templating-Engine Razor.Unter der Webadresse [9] können Sie Blazor direkt in Ihrem (entsprechend modernen) Browser ausprobieren – wohlgemerkt ohne Installation irgendeines Plug-ins, wie Sie es aus Java-Applet-, Flash- oder Silverlight-Zeiten noch gewohnt sein mögen. Natürlich funktioniert das auch auf gängigen Mobilgeräten mit aktueller Software. Da die Anwendung ein Responsive Design umsetzt, lässt sie sich auch auf Mobilgeräten sinnvoll bedienen Die Ausführung dieser Beispielanwendung zeigt Bild 5.
Unter der Haube scheint Mono durch: Wenn Sie in den Entwickler-Tools von Google Chrome (zu öffnen mit der Tastenkombination [F12] unter Windows und Linux sowie [Wahl Befehl I] unter macOS) in der Registerkarte Network den Netzwerkverkehr inspizieren, sehen Sie, wie die einzelnen .NET-Assemblies zur Ausführung durch die Mono-Runtime in WebAssembly heruntergeladen werden. Der Anblick ist dann wirklich nichts für schwache Nerven: In Bild 6 ist zu sehen, wie die Assembly System.IO.dll heruntergeladen wird – als ausführbare Datei inklusive MZ-Header und mit dem altbekannten Hinweis „This program cannot be run in DOS mode“. Und recht soll die Assembly behalten, denn im Browser funktioniert es allemal. Details zu Blazor haben wir Ihnen ebenfalls in dotnetpro 1/2018 vorgestellt [10].
FazitBlazor und Mono auf WebAssembly stehen beide noch am Anfang ihrer Entwicklung. Noch sind die entstehenden Applikationsbündel viel zu groß und die Kompilier- und Startzeiten viel zu langsam. Aber dennoch: WebAssembly schlägt zum ersten Mal eine ernst zu nehmende Brücke von .NET ins Web, die plattformübergreifend auf allen Geräten betrieben werden kann, solange ein halbwegs moderner Browser darauf läuft – ganz ohne Abhängigkeiten, ganz ohne Plug-in, mit dem vollen Funktionsumfang, der Webanwendungen schon heute zur Verfügung steht.Dies eröffnet interessante Migrationsstrategien für bestehenden Quelltext und öffnet die Web-Welt auch denjenigen Entwicklern, die sich bisher noch nicht an JavaScript herangetraut haben.Noch ist Blazor nur ein Experiment, doch hat die Entwicklerszene auf das Framework bislang überaus positiv reagiert. Wird Blazor zum Produkt, so erhalten auch die Basistechnologien wie die WebAssembly-Ausgabe von Mono entsprechenden Auftrieb. Doch unabhängig davon zeigt sich einmal wieder, dass das Web die Plattform zur Entwicklung von Anwendungen geworden ist – mit WebAssembly als einer Innovation unter vielen.
Fussnoten
- Holger Schwichtenberg, Die Rückkehr des Desktops, dotnetpro 9/2018, Seite 8 ff., http://www.dotnetpro.de/A1809NETCore
- Christian Liebel, Im Client (fast) angekommen, dotnetpro 1/2018, Seite 10 ff., http://www.dotnetpro.de/A1801WebSharp
- WebAssembly 1.0, https://webassembly.org
- WebAssembly – Features to add after the MVP, http://www.dotnetpro.de/SL1810Wasm1
- WebAssembly Studio, https://webassembly.studio
- Miguel de Icaza, Hello WebAssembly, http://www.dotnetpro.de/SL1810Wasm2
- Mono-Repository auf GitHub, http://www.dotnetpro.de/SL1810Wasm3
- mono-wasm auf GitHub, https://github.com/lrz/mono-wasm
- Blazer-Demo für den Browser, https://blazor-demo.github.io
- Jürgen Gutsch, C#-Code im Browser ausführen, dotnetpro 1/2018, Seite 16 ff., http://www.dotnetpro.de/A1801Blazor
- TeaVM, http://teavm.org
- Cheerp, https://www.leaningtech.com/cheerp