Wie das Projekt „Roslyn“ im Hintergrund C# revolutioniert

Projekt Roslyn: Das steckt hinter der neuen .NET-Compiler-Plattform
Kommentare

Obwohl er traditionell im Hintergrund bleibt, ist der Compiler für die C# Entwicklung ein unverzichtbares Werkzeug. Lassen Sie uns in diesem Artikel gemeinsam über die Konsequenzen der neuen Compilerplattform „Roslyn“ nachdenken und einen genaueren Blick auf deren Funktionsweise werfen.

Quellcode geht rein, ein Assembly kommt heraus. Solange kein Fehler im Compiler steckt – was äußerst selten der Fall ist – ist die Komponente eine im Alltag relativ uninteressante Black Box. Was passiert, wenn der Compiler sich plötzlich öffnen würde? Was, wenn der Quellcode Open Source wird und alle Aspekte für jede und jeden als .NET-Klassenbibliothek zur Verfügung steht? Diesen Schritt hat Microsoft mit dem Projekt „Roslyn“ gewagt. In Visual Studio 2015 hat Roslyn die Betaphase hinter sich gelassen, wurde in .NET-Compiler-Plattform umbenannt und hat den alten C#-Compiler ersetzt.

Es gibt unzählige Einsatzbereiche von .NET: Full Clients, Apps, Webseiten, Web Services, Kommandozeilentools, Installer, Datenbanklogik etc. In diesen Bereichen gibt es spezifische Frameworks, Bibliotheken oder NuGet-Pakete. Eine Komponente haben aber alle diese C#-Programme gemeinsam: Den Compiler. Er ist das Arbeitspferd aller C#-Entwicklerinnen und -Entwickler.

Der C#-Compiler im Wandel

Der C#-Compiler hat sich im Laufe der Zeit gewandelt. In den ersten Versionen war er ziemlich schlank, da die Funktionen von C# ziemlich nahe an den Möglichkeiten waren, die die zugrundeliegende Plattform – die Common Language Runtime (CLR) und die Microsoft Intermediate Language (MSIL) – bot. Viel von der Arbeit, die in anderen Programmiersprachen der Compiler machen musste, erledigt in .NET der nachgeschaltete Just in Time-Compiler (kurz JIT oder „jitter“), der in .NET sprachenunabhängig ist. Im Laufe der Zeit wurden aber mehr und mehr wichtige Funktionen in die Sprache C# integriert, die kein unmittelbares Gegenstück in der CLR haben. Beispiele dafür sind Lambda-Ausdrücke, Linq und Expression Trees oder async/await. Es ist Aufgabe des C#-Compilers, solche Sprachelemente in IL zu übersetzen. Wenn Sie einmal sehen möchten, was der C#-Compiler der aktuellen Version alles zu tun hat, schreiben Sie ein paar Zeilen asynchronen Code mit Lambdas und async/await. Anschließend verwenden Sie ein Tool wie ILDasm.exe oder .NET-Reflector und sehen Sie sich den generierten IL-Code an. Sie werden überrascht sein, wie stark der entstehende IL-Code von Ihrem C#-Quellcode abweicht.

Ein Compiler reichte nicht

Neben den funktionalen Erweiterungen der Sprache C# gab es eine wichtige Parallelentwicklung. Die Erwartungen von uns Programmiererinnen und Programmierern an die Entwicklungsumgebung Visual Studio stiegen von Version zu Version. Wir wurden verwöhnt mit Funktionen wie

  • der Anzeige von Compilerfehlern schon während des Tippens,
  • IntelliSense,
  • der intelligenten Umbenennung,
  • den mächtigen Such- und Navigationsfunktionen wie Find all References oder Call Tree,
  • Tooltips, die eine komplexe, semantische Analyse des C#-Codes zur Editierzeit erfordern und vieles mehr.

Diese Features erscheinen aus Sicht der Anwenderinnen und Anwender simpel. Ihre Umsetzung erfordert jedoch tiefgreifendes Verständnis des C#-Codes und der zugehörigen Solution- und Projektstruktur. Der ursprünglich in C++ entwickelte C#-Compiler war dafür nie entworfen worden. Er enthält zwar intern alles, um Funktionen wie die oben genannten umzusetzen, dieses Wissen ist jedoch in einer Black Box verborgen und steht außen nicht zur Verfügung. Als Konsequenz musste Microsoft eine zweite Compiler-Infrastruktur für die Entwicklungsumgebung nachbauen.

Diese Notwendigkeit war nicht auf Microsoft beschränkt. Wollte man bisher ein Tool schreiben, das C#-Code analysiert und vielleicht sogar automatische Verbesserungen des Quellcodes anbietet, musste man ebenfalls zumindest einen Teil des C#-Compilers nachbauen. Da war ein Unterfangen, das sich nur wenige große, spezialisierte Teams in der Vergangenheit leisten konnten.

Zeit für einen Neustart

Aus eigener Erfahrung weiß jede C#-Entwicklerin und jeder C#-Entwickler, dass Code, dessen Aufgabe sich im Laufe der Zeit stark erweitert und verändert, nicht besser wird. Auch der Code eines Compilers ist nicht immun gegen das Phänomen von historisch gewachsenem Spaghetticode. Vor einigen Jahren entschied das C#-Team bei Microsoft daher, dass es an der Zeit war, aufzuräumen. Das Projekt mit Codenamen „Roslyn“ war geboren. Roslyn sollte nicht nur eine kleine Verbesserungsmaßnahme werden. Microsoft hatte für dieses Projekt weitreichende Ziele:

  • Der neue C#-Compiler sollte nicht mehr in C++ sondern in C# geschrieben werden. Eine mutige Entscheidung wenn man bedenkt, wie wichtig die Themen Performance und Kompatibilität bei einem Compiler sind.
  • Der Compiler sollte zur modularen Klassenbibliothek werden. Jeder – auch Programmiererinnen und Programmierer von Entwicklungsumgebungen oder Refactoring-Werkzeugen – soll Teile verwenden oder durch alternative Implementierungen ersetzen können.
  • Roslyn sollte nicht nur auf den Compiler-Kern beschränkt sein. Das Projekt sollte auch eine API entwickeln, die mit Workspaces (z. B. Solutions, Projekten, Dokumenten) umgehen kann und Schnittstellen für Visual Studio Erweiterungen zum Refactoring von Code definieren.

Angesichts der Projektgröße und der Wichtigkeit des C#-Compilers ist es kein Wunder, dass sich das Projekt Roslyn über viele Jahre zog. Es gab eine große Anzahl an Preview-Versionen und im Laufe der Zeit sogar schon Unkenrufe, in denen behauptet wurde, dass es das Projekt nie aus dem Beta-Status schaffen würde. Visual Studio 2015 beweist das Gegenteil. Roslyn, jetzt .NET-Compiler-Plattform genannt, ist in Produktion und hat den alten C#-Compiler abgelöst. Nicht nur das. Als eine Folge des Offenheitstrends bei Microsoft steht der Quelltext des C#-Compilers heute als Open-Source-Projekt auf Github zur Verfügung und ist nicht mehr an die Windows-Plattform gebunden.

Roslyn-Sourcecode

Wer tief in die .NET-Compiler-Plattform einsteigen möchte, findet den Quellcode auf GitHub. Um einen Blick in den Quellcode zu werfen oder ein Detail nachzuschlagen, ist es nicht unbedingt notwendig, das GitHub Repository zu klonen. Im Web findet man den Roslyn-Quellcode als angenehm zu durchsuchende Webseite. Wer Reference Source für das .NET Framework kennt, wird sich auf der Roslyn-Webseite sofort zurecht finden. Es stehen die gleichen Funktionen zum Suchen und Navigieren (z. B. Go To Definition, Find All References, Namespace Explorer etc.) im Browser zur Verfügung.

Der Grundaufbau der .NET-Compiler-Plattform

Die .NET-Compiler-Plattform ist in drei Schichten gegliedert. Abbildung 1 stellt sie grafisch dar. Die Grundlage bildet die Compiler Pipeline mit den zugehörigen APIs. Sie implementieren alle Basisfunktionen, die der Compiler zum Übersetzen von C#-Quellcode in IL braucht:

  • Der Parser zerlegt den C#-Quellcode in sogenannte Token. Token sind nicht mehr weiter zerlegbare Sprachelemente wie Schlüsselwörter, Identifier, Konstanten etc. Das Ergebnis des Parsers ist der Syntax Tree. Er ist gedanklich vergleichbar mit dem DOM, den der Parser eines Browsers aus HTML-Code im Speicher aufbaut.
  • Im zweiten Schritt der Pipeline werden Symbole behandelt. Der Compiler extrahiert Informationen über Deklarationen aus C#-Quellcode und importierten Metadaten. Das Ergebnis ist eine hierarchische Symboltabelle, die alle benannten Symbole beschreibt.
  • Der Binder verbindet die Identifier aus dem Syntax Tree (Ergebnis des ersten Schritts) mit den Symbolen (Ergebnis des zweiten Schritts).
  • Der letzte Schritt ist der Emitter. Er erzeugt den IL-Code des entstehenden Assemblies.

Die Compiler API hat keine Abhängigkeiten zu Visual Studio und kann daher ohne weiteres in eigenen Projekten eingesetzt werden. Am Ende des Artikels beschreibe ich Anwendungsbeispiele, bei denen wir in eigenen Projekten und bei Kunden Roslyn bereits außerhalb von Visual Studio eingesetzt haben.

Abb. 1: Systemarchitektur von Roslyn (Quelle)

Abb. 1: Systemarchitektur von Roslyn (Quelle)

Immutables im Compiler-API

Für jede der oben genannten Compilerfunktionen enthält die .NET-Compiler-Plattform ein API in Form von Datenstrukturen und Funktionen. Ein wesentliches Merkmal dieses API ist, dass alle Datenstrukturen nach außen immutable, also unveränderbar sind. Unveränderbare Datenstrukturen sind von Natur aus Threadsafe, Locks sind daher auch bei parallelem Zugriff auf sie nicht notwendig. Das wirkt sich entsprechend positiv auf die Performance und Antwortzeit von Compiler und Entwicklungsumgebung aus.

Jeder Aufruf des Compiler-API liefert einen unveränderlichen Snapshot an Ergebnissen (repräsentiert durch Microsoft.CodeAnalysis.CSharp.CSharpCompilation, kurz Compilation genannt). Für den eigentlichen Übersetzungsvorgang des Quellcodes in Assemblies passt das gut. Wie ist das aber in Visual Studio während der Arbeit an einem Programm? Wird hier der Code nicht laufend verändert? Wenn Sie in Visual Studio beim Programmieren wenige Momente inaktiv sind, wird im Hintergrund das Compiler-API mit dem aktuellen Stand des Quellcodes aufgerufen. Die Verarbeitung läuft dabei hochgradig parallel ab. Sollten Sie den Code weiter ändern, werden eventuell noch laufende Prozesse unterbrochen und der Code wird bei ihrer nächsten, kurzen Inaktivität neu analysiert.

Natürlich wäre diese Implementierung viel zu langsam, wenn Roslyn naiv programmiert wäre und jede Änderung zu einem kompletten Neuberechnen oder Kopieren des gesamten Objektmodells im Speicher führen würde. Diese Herausforderung im Roslyn-Projekt hat quasi als Nebenprodukt eine neue Kategorie von Collections im .NET-Framework hervorgebracht: Effiziente Immutable Collections. Sie sind ab .NET 4.5 einsetzbar. Ihr Einsatz ist nicht auf Roslyn-bezogene Programme beschränkt. Als .NET-Entwicklerin oder -Entwickler können Sie die Immutable Collections von .NET auch für ihre eigenen Programme nutzen. Das entsprechende NuGet-Paket heißt Microsoft.Bcl.Immutable. Will man mit der .NET-Compiler-Plattform Lösungen entwickeln, ist ein Verständnis über Immutables im Allgemeinen und die Collections aus dem genannten NuGet-Paket im Speziellen unumgänglich. Microsoft bietet zu diesem Thema umfangreiche Informationen in der MSDN und Video auf Channel9.

Die .NET-Compiler-Plattform in Visual Studio

Visual Studio 2015 wurde vollständig auf die neuen APIs der .NET-Compiler-Plattform umgestellt. Funktionen wie Go To Definition oder Find all References sowie die Refactoring-Tools nutzen also jetzt die im Projekt Roslyn entwickelten APIs. Die Solution- und Projektverwaltung wurde außerdem auf das Workspace-API von Roslyn umgebaut. Sie ist die Grundlage für alle Aktionen (z. B. Codeanalyse, Refactorings), die auf Solution- oder Projektebene stattfinden. Visual Studio baut zwar auf der Workspace API auf, diese hat jedoch keine Abhängigkeit auf Visual Studio. Insofern könnten Hersteller alternativer Entwicklungsumgebungen ebenfalls auf das Workspace-API von Roslyn aufbauen.

Syntax Tree

Lassen Sie uns jetzt einen genaueren Blick auf die Funktionsweise des .NET-Compiler-Plattform-API werfen. Der oben bereits erwähnte Syntax Tree ist ein Schlüsselelement. Er bildet den Quelltext komplett ab. Aus einem Syntaxbaum lässt sich also der Quellcode verlustfrei wieder herstellen. Selbst Whitespaces, Kommentare und Preprocessor-Direktiven bleiben erhalten. Diese Tatsache ist wichtig, damit man über den Umweg der Syntaxbaum-Manipulation nicht nur die Logik des Codes, sondern auch dessen Formatierung zum Beispiel im Rahmen von Refactoring-Prozessen umbauen kann.

Compilerfehler sind im Syntaxbaum als spezielle Knoten enthalten. Die rote Wellenlinie unter Syntaxfehlern, die man aus Visual Studio kennt, bauen genau darauf auf.

Syntaxbäume sind in Roslyn wie oben erwähnt unveränderbar. Will man z. B. im Rahmen eines Refactoring-Tools den Quelltext durch Manipulation des Syntaxbaums verändern, muss ein neuer Syntaxbaum erstellt werden. Die .NET-Immutable-Collections sorgen dafür, dass dies effizient geschieht und nicht Speicher durch unnötiges Kopieren unveränderter Teilbäume verschwendet wird.

Der Roslyn Syntax Visualizer, der in Abbildung 2 zu sehen ist, ist ein gutes Werkzeug, um mit Syntaxbäumen vertraut zu werden. Er stellt die gerade geöffnete C#-Datei als Syntaxbaum dar und erlaubt es Ihnen, den Baum interaktiv durchzusehen. Dabei werden Sie auf folgende Typen von Knoten stoßen:

  • Syntax Nodes (in Abbildung 2 blau dargestellt): Sie repräsentieren den Hauptinhalt des C#-Programms (z. B. Deklarationen, Statements, Expressions etc.). Alle Syntax-Node-Klassen sind von der Klasse Microsoft.CodeAnalysis.CSharp.CSharpSyntaxNode abgeleitet und tragen den Postfix Syntax (z. B. VariableDeclarationSyntax, UsingDirectiveSyntax etc.).
  • Syntax Tokens (in Abbildung 2 grün dargestellt): Syntax Tokens sind die kleinste Einheit der Sprache (z. B. Schlüsselwörter, Literale etc.). Sie werden im Syntaxbaum durch den Value Type Microsoft.CodeAnalysis.SyntaxToken repräsentiert.
  • Syntax Trivia (in Abbildung 2 rot dargestellt): Sie sind für dir Programmlogik nicht relevant (z. B. Kommentare, Whitespaces etc.) und können praktisch überall im Programm vorkommen. Sie können daher konzeptionell nicht als Kinder von Syntax Tokens angesehen werden. Ein Kommentar, der in einer Codezeile nach dem Semikolon steht und die Zeile näher beschreibt, ist schließlich nicht ein Kindknoten der Codezeile. Aus diesem Grund gibt es in der Klasse SyntaxToken die Eigenschaften LeadingTrivia und TrailingTrivia, über die man auf die angrenzenden Syntax-Trivia-Knoten zugreifen kann. Die Trivias selbst sind in der Roslyn API durch die Klasse Microsoft.CodeAnalysis.SyntaxTrivia abgebildet.
stropek_roslyn_2

Abb. 2: Visualisierung des Syntaxbaums mit Roslyn (Quelle)

Jeder Knoten im Syntaxbaum hat zwei Properties Span und FullSpan (enthält auch führende oder nachfolgende Trivias), mit deren Hilfe die genaue Position des zugehörigen Textes im C#-Sourcecode ermittelt werden kann (in Abbildung 2 am Ende jeder Zeile zu sehen, z. B. „[0..13)“). Diese Information ist wichtig für Editoren, um visuelle Hinweise an die richtige Stelle des Textfensters setzen zu können.

Semantic Model

Das Semantic Model der .NET-Compiler-Plattform ermöglicht ein tieferes Verständnis des Quellcodes. Im Syntaxbaum kann an vielen Stellen zum Beispiel ein Identifier mit überall gleichem Namen verwendet werden. Nicht alle dieser Vorkommen beziehen sich aber zwangsläufig auf das gleiche Symbol. Einmal kann eine lokale Variable gemeint sein, an anderer Stelle ein Feld und an einer dritten Stelle könnte ein Typ referenziert werden. Diese Zusammenhänge sind im semantischen Modell abgebildet. Es enthält nicht nur Daten aus dem C#-Quellcode. Symbole können auch aus externen Quellen, zum Beispiel aus referenzierten Assemblies, kommen, für die es überhaupt keinen Syntaxbaum gibt.

Anwendungsbeispiele

Neugierig geworden? Das Roslyn-Team hat umfangreiche, dokumentierte Beispiele für verschiedene Anwendungsfälle erstellt. Diese stehen im GitHub Repository von Roslyn zur Verfügung. Ich empfehle Lesern, die tiefer in die Programmierung mit Roslyn einsteigen möchten, mit den Schritt-für-Schritt Anleitungen („Getting Started Guides“) zu beginnen.

Schnell und überall: Datenzugriff mit Entity Framework Core 2.0

Dr. Holger Schwichtenberg (www.IT-Visions.de/5Minds IT-Solutions)

C# 7.0 – Neues im Detail

Christian Nagel (CN innovation)

Auch wenn die Entwicklung von Visual-Studio-Erweiterungen oder die Erweiterung des C#-Compilers nicht mein Geschäft ist, hat mir Roslyn in den letzten Jahren bereits in einer Reihe von Situationen geholfen. Hier drei beispielhafte Anwendungsfälle, die Sie vielleicht auf Ideen bringt, wo Sie Roslyn im Entwicklungsalltag einsetzen könnten:

  • Codegenerator: Mit Roslyn ist das Einlesen und Analysieren von C#-Code nicht viel schwieriger als das Verarbeiten von XML. In einem Kundenprojekt habe ich mit Roslyn Datenmodellklassen eingelesen und mit Hilfe der Template-Engine T4 automatisch passenden ViewModel-Code nach kundenspezifischen Konventionen erzeugt.
  • Entfernen von #if-Blöcken: In eine größere Codebasis wurden im Laufe der Zeit unzählige, bedingt zu kompilierende Codeblöcke mit #if eingefügt, um den Code für zwei unterschiedliche Zielplattformen vorzubereiten. Als eine dieser Zielplattformen wegfiel, konnten alle nicht mehr notwendigen #if– bzw. #else-Blöcke durch ein kleines, auf Roslyn basierendes Tool automatisch entfernt werden. Mühsame, manuelle Arbeit entfiel und die Wartbarkeit wurde wesentlich verbessert.
  • Codeformatierung: Eine größere, gewachsene Codebasis war im Laufe der Zeit zu einem unleserlichen Kauderwelsch geworden. Mithilfe von Roslyn-Tools konnte zumindest die Formatierung des Codes in kürzester Zeit vereinheitlicht und verbessert werden (siehe dazu auch .NET Codeformatter Tool).

Fazit

Die neue .NET-Compiler-Plattform aka „Roslyn“ ist weit mehr als nur eine unwichtige Änderung im Hintergrund. Seit dem viele Jahre zurückliegenden Start dieses Projekt hat das Microsoft-Team rund um den C#-Macher Anders Hejlsberg viel geleistet. Modulare Architektur, höchste Anforderungen an Kompatibilität und Performance, offene API ohne Abhängigkeiten zu Visual Studio, der Schritt zu Open Source und GitHub, Plattformunabhängigkeit – man könnte diese Liste noch um viele Verbesserungen erweitern. Statt sich abzuschotten und Entwickler durch Zwang an sich zu binden, hat Microsoft Innovationen von außen in Sachen C#-Compiler Tür und Tor geöffnet. Dinge wie Refactoring-Tools, automatische Analyse von Codequalität oder C#-Unterstützung in alternativen Entwicklungsumgebungen waren vor Roslyn eine große Herausforderungen und nur für wenige Teams machbar. Roslyn hat die Einstiegshürde massiv gesenkt. Entwickler, die in diesen Bereichen arbeiten möchten, können sich jetzt auf ihre eigentliche Lösung konzentrieren, statt erst den C#-Compiler nachprogrammieren zu müssen.

Natürlich hat die neue Compiler-Plattform noch Ecken und Kanten. Die Konzeptdokumentation könnte beispielsweise besser sein. In der Praxis hilft oft nur ein Blick in den Roslyn-Sourcecode oder das Durchsehen der gelieferten Beispiele. Für eine erste Version ist meiner Meinung nach Roslyn aber sehr gut gelungen. Es macht Spaß, damit eigene Lösungen umzusetzen.

Aufmacherbild: Security concept: Lock on digital screen, contrast, 3d render via Shutterstock / Urheberrecht: jijomathaidesigners

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -