Clang a prouvé qu'il était un compilateur mature pour C et C++, au même titre que GCC et les compilateurs de Microsoft. Ce qui le rend spécial, cependant, c'est qu'il n'est pas seulement un compilateur : c'est aussi une infrastructure pour construire des outils. Grâce à son architecture basée sur des bibliothèques, les composants peuvent être réutilisés plus facilement, et de nouvelles fonctionnalités peuvent être intégrées plus flexiblement dans d'autres projets.
La conception de Clang
Comme beaucoup d’autres conceptions de compilateurs, le compilateur Clang comporte trois phases :
- Le front end analyse le code source, vérifie les erreurs et construit un arbre syntaxique abstrait (AST) spécifique au langage pour représenter le code d'entrée.
- L'optimiseur effectue des optimisations sur la représentation générée par le front end.
- Le back end génère le code final à exécuter par la machine ; il dépend de la cible.

Quelle est la différence entre Clang et les autres compilateurs ?
La différence la plus importante dans sa conception est que Clang est basé sur LLVM ; l'idée derrière LLVM est d'utiliser la représentation intermédiaire LLVM (IR), qui est comme le bytecode pour Java. LLVM IR est conçue pour accueillir les analyses et transformations de niveau intermédiaire que l'on trouve dans la section optimiseur d'un compilateur. Elle a été conçue avec de nombreux objectifs spécifiques à l'esprit, notamment la prise en charge d'optimisations légères à l'exécution, d'optimisations inter-fonctions/interprocédurales, d'analyses de programmes entiers et de transformations de restructuration agressives, etc. Son aspect le plus important, cependant, est qu'elle est elle-même définie comme un langage de premier ordre avec une sémantique bien définie.
Avec cette conception, nous pouvons réutiliser une grande partie du compilateur pour créer d'autres compilateurs. Par exemple, nous pouvons ne changer que le front end pour traiter d'autres langages.

I- Le front end
Clang est conçu pour être modulaire, et chaque phase de compilation est réalisée par un module spécifique. Voici quelques-uns des projets impliqués dans la phase front end :

Comme pour tout parseur front end, nous avons besoin d'un lexeur et d'une analyse sémantique. Le front end de Clang peut être exécuté en passant l'argument -cc1. Il prend en charge plusieurs fonctionnalités, dont la génération de l'AST :
clang -cc1 -ast-dump test.c
Cette ligne de commande est gérée par la fonction cc1_main ; voici la séquence de quelques-unes des méthodes intéressantes qui sont exécutées :

La méthode ExecuteAction a un paramètre de type FrontendAction ; son but est de spécifier quelle action front end exécuter. FrontendAction est abstraite, nous devons donc en hériter pour implémenter une action front end concrète.
Explorons toutes les actions front end implémentées par Clang en utilisant CQLinq; pour cela, nous pouvons rechercher toutes les classes qui en héritent directement ou indirectement.
from t in Types
let depth0 = t.DepthOfDeriveFrom(“clang.FrontendAction”)
where depth0 >= 0 orderby depth0
select new { t, depth0 }

De nombreuses actions front end sont disponibles ; par exemple, ASTDumpAction permet de générer l'AST sans créer l'exécutable final. Presque toutes les actions front end héritent de ASTFrontendAction, ce qui signifie qu'elles travaillent avec l'AST généré.
Ce qui est intéressant dans cette conception, c'est que nous pouvons facilement brancher notre propre FrontendAction ; il nous suffit d'en implémenter une nouvelle.
Comment effectuer un traitement sur l'AST ?
Chaque ASTFrontendAction crée un ou plusieurs ASTConsumers ; la classe ASTConsumer est abstraite, et nous devons implémenter notre propre consommateur d'AST pour répondre à nos besoins spécifiques.
La FrontendAction invoquera le consommateur d'AST comme le montre le graphe suivant.

Recherchons toutes les classes ASTConsumer en utilisant CQLinq:
from t in Types
let depth0 = t.DepthOfDeriveFrom(“clang.ASTConsumer”)
where depth0 == 1
select new { t, depth0 }

CodeGenerator est un exemple de consommateur d'AST
Comme mentionné précédemment, l'une des forces de LLVM est son utilisation de l'IR, et la générer exige de traiter l'AST. CodeGenerator est la classe dérivée d'ASTConsumer responsable de la génération de l'IR. Fait intéressant, ce traitement est isolé dans un autre projet appelé ClangCodeGen.
Voici quelques classes impliquées dans la génération de l'IR LLVM :

II- L'optimiseur
Pour expliquer cette phase, je ne peux pas le dire mieux que Chris Lattner, le père de LLVM, dans cet article:
« Pour donner une intuition du fonctionnement des optimisations, il est utile de parcourir quelques exemples. Il existe de nombreux types d'optimisations de compilateur, il est donc difficile de fournir une recette pour résoudre un problème arbitraire. Cela dit, la plupart des optimisations suivent une structure simple en trois parties :
- Chercher un motif à transformer.
- Vérifier que la transformation est sûre/correcte pour l’instance correspondante.
- Effectuer la transformation, en mettant à jour le code.
Explorons les passes de LLVMCore en recherchant les classes qui héritent de la classe « Pass ».
from t in Types
let depth0 = t.DepthOfDeriveFrom(“llvm.Pass”)
where t.ParentProject.Name==”LLVMCore” && depth0 >= 0 orderby depth0
select new { t, depth0 }

Bien sûr, de nombreuses autres passes existent dans d'autres modules LLVM.
III- Le back end
Comme les autres phases, le back end est responsable de la génération de la sortie pour une cible spécifique. Dans Clang, il est hautement modulaire. Prenons LLVMX86Target, le module qui génère le code pour la cible x86, comme exemple.
Voici un graphe montrant tous les modules impliqués dans la génération des binaires pour la cible x86.

De nombreux modules sont impliqués dans cette phase, chacun avec une responsabilité spécifique. Cela favorise la cohésion, des API propres et la séparation des préoccupations, rendant le système plus facile à comprendre pour les développeurs car ils peuvent se concentrer sur des morceaux plus petits de l'architecture globale.
Conclusion
Le duo LLVM/Clang n'est pas seulement un compilateur C/C++ ; c'est aussi une infrastructure pour construire des outils, et son comportement est facile à étendre. De nombreux outils sont inclus prêts à l'emploi dans le code source de LLVM/Clang, et beaucoup d'autres se trouvent sur le web.
Si vous avez besoin d'un parseur C/C++ pour construire un outil, Clang est un très bon candidat.
