Clang hat sich – ebenso wie GCC und die Compiler von Microsoft – als ausgereifter Compiler für C und C++ bewährt. Das Besondere daran ist jedoch, dass Clang nicht nur ein Compiler, sondern zugleich eine Infrastruktur zum Erstellen von Werkzeugen ist. Dank seiner bibliotheksbasierten Architektur lassen sich Komponenten leichter wiederverwenden und neue Funktionen flexibler in andere Projekte integrieren.
Das Design von Clang
Wie viele andere Compiler ist auch Clang in drei Phasen gegliedert:
- Das Frontend analysiert den Quellcode, prüft ihn auf Fehler und erstellt einen sprachspezifischen Abstract Syntax Tree (AST), der den Eingabecode repräsentiert.
- Der Optimierer führt Optimierungen an der vom Frontend erzeugten Repräsentation durch.
- Das Backend erzeugt den endgültigen, von der Maschine auszuführenden Code und ist von der jeweiligen Zielplattform abhängig.

Worin unterscheidet sich Clang von anderen Compilern?
Der wichtigste Unterschied im Design besteht darin, dass Clang auf LLVM basiert. Die Idee hinter LLVM ist die Verwendung der LLVM Intermediate Representation (IR), die in gewisser Weise mit Java-Bytecode vergleichbar ist. LLVM IR wurde für Analysen und Transformationen auf mittlerer Ebene entwickelt, wie sie im Optimierungsteil eines Compilers vorkommen. Zu den konkreten Zielen gehören leichtgewichtige Laufzeitoptimierungen, funktionsübergreifende beziehungsweise interprozedurale Optimierungen, die Analyse des gesamten Programms und weitreichende Umstrukturierungen. Besonders wichtig ist jedoch, dass LLVM IR selbst als vollwertige Sprache mit klar definierter Semantik spezifiziert ist.
Mit diesem Design lässt sich ein großer Teil des Compilers für andere Compiler wiederverwenden. So kann beispielsweise lediglich das Frontend ausgetauscht werden, um andere Sprachen zu verarbeiten.

I – Frontend
Clang ist modular aufgebaut, und jede Kompilierungsphase wird von einem eigenen Modul übernommen. An der Frontend-Phase sind unter anderem folgende Projekte beteiligt:

Wie bei jedem Frontend-Parser benötigen wir einen Lexer und eine semantische Analyse. Das Clang-Frontend lässt sich mit dem Argument -cc1 ausführen. Es unterstützt verschiedene Funktionen, darunter die Erzeugung eines AST:
clang -cc1 -ast-dump test.c
Diese Befehlszeile wird von der Funktion cc1_main verarbeitet. Nachfolgend sehen Sie die Abfolge einiger interessanter Methoden, die dabei ausgeführt werden:

Die Methode ExecuteAction besitzt einen Parameter vom Typ FrontendAction. Damit wird festgelegt, welche Frontend-Aktion ausgeführt werden soll. FrontendAction ist abstrakt; für eine konkrete Frontend-Aktion muss daher davon abgeleitet werden.
Sehen wir uns mithilfe vonCQLinqalle von Clang implementierten Frontend-Aktionen an. Dazu suchen wir nach allen Klassen, die direkt oder indirekt davon erben.
from t in Types
let depth0 = t.DepthOfDeriveFrom(“clang.FrontendAction”)
where depth0 >= 0 orderby depth0
select new { t, depth0 }

Es stehen zahlreiche Frontend-Aktionen zur Verfügung. ASTDumpAction ermöglicht beispielsweise die Erzeugung des AST, ohne eine endgültige ausführbare Datei zu erstellen. Fast alle Frontend-Aktionen erben von ASTFrontendAction, arbeiten also mit dem erzeugten AST.
Interessant an diesem Design ist, dass sich eine eigene FrontendAction sehr einfach einbinden lässt: Wir müssen lediglich eine neue implementieren.
Wie können wir den AST verarbeiten?
Jede ASTFrontendAction erzeugt einen oder mehrere ASTConsumer. Die Klasse ASTConsumer ist abstrakt; für unsere spezifischen Anforderungen müssen wir daher einen eigenen AST-Consumer implementieren.
Die FrontendAction ruft den AST-Consumer auf, wie die folgende Grafik zeigt.

Suchen wir mithilfe vonCQLinq:
from t in Types
let depth0 = t.DepthOfDeriveFrom(“clang.ASTConsumer”)
where depth0 == 1
select new { t, depth0 }

CodeGenerator ist ein Beispiel für einen AST-Consumer
Wie bereits erwähnt, liegt eine der Stärken von LLVM in der Verwendung der IR. Um sie zu erzeugen, muss der AST verarbeitet werden. CodeGenerator ist die von ASTConsumer abgeleitete Klasse, die für die Erzeugung der IR verantwortlich ist. Interessanterweise ist diese Verarbeitung in einem eigenen Projekt namens ClangCodeGen gekapselt.
An der Erzeugung der LLVM IR sind unter anderem folgende Klassen beteiligt:

II – Optimierer
Diese Phase lässt sich kaum besser erklären als von Chris Lattner, dem Vater von LLVM, in diesemBeitrag:
„Um ein Gefühl dafür zu vermitteln, wie Optimierungen funktionieren, ist es hilfreich, einige Beispiele durchzugehen. Es gibt viele verschiedene Arten von Compiler-Optimierungen, weshalb sich kaum ein allgemeines Rezept für beliebige Probleme angeben lässt. Die meisten Optimierungen folgen jedoch einer einfachen Struktur aus drei Schritten:
- Nach einem Muster suchen, das transformiert werden soll.
- Prüfen, ob die Transformation für die gefundene Instanz sicher und korrekt ist.
- Die Transformation durchführen und den Code entsprechend aktualisieren.
Untersuchen wir die LLVMCore-Passes, indem wir nach Klassen suchen, die von der Klasse „Pass“ erben.
from t in Types
let depth0 = t.DepthOfDeriveFrom(“llvm.Pass”)
where t.ParentProject.Name==”LLVMCore” && depth0 >= 0 orderby depth0
select new { t, depth0 }

Natürlich gibt es in anderen LLVM-Modulen noch viele weitere Passes.
III – Backend
Wie die anderen Phasen ist auch das Backend für die Erzeugung der Ausgabe für ein bestimmtes Ziel verantwortlich. In Clang ist es stark modularisiert. Als Beispiel dient LLVMX86Target, das Modul zur Codeerzeugung für die x86-Zielplattform.
Die folgende Grafik zeigt alle Module, die an der Erzeugung von Binärdateien für die x86-Zielplattform beteiligt sind.

An dieser Phase sind viele Module beteiligt, die jeweils eine klar abgegrenzte Aufgabe erfüllen. Das fördert Kohäsion, saubere APIs und eine klare Trennung der Verantwortlichkeiten. Für Entwickler wird das System dadurch leichter verständlich, weil sie sich auf kleinere Teile der Gesamtarchitektur konzentrieren können.
Fazit
LLVM und Clang bilden nicht nur einen C/C++-Compiler, sondern zugleich eine Infrastruktur zur Entwicklung von Werkzeugen, deren Verhalten sich leicht erweitern lässt. Viele Tools sind bereits im LLVM/Clang-Quellcode enthalten, zahlreiche weitere sind im Web verfügbar.
Wenn Sie für die Entwicklung eines Tools einen C/C++-Parser benötigen, ist Clang eine sehr gute Wahl.
