Compilation C++ avec LLVM et clang

Générer du code C++ dynamiquement depuis Lua

L'objectif de ce tutoriel d'Emmanuel Roche est de vous apprendre à générer du code C++ dynamiquement depuis Lua.

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum. 1 commentaire Donner une note  l'article (5)

Article lu   fois.

Les deux auteur et traducteur

Traducteur : Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Ces derniers temps, j’ai généré passablement d’interfaces pour Lua (principalement pour des expériences de génération de maillages avec OpenCascade, ce qui est en passant très marrant/intéressant, mais qui n’est pas le sujet ici…). Et une chose qui m’a ennuyé à la fin était : je veux bien utiliser Lua pour générer une configuration ou exécuter des calculs préliminaires, mais je n’aimerais pas l’utiliser pour une boucle de mise à jour continue dans un moteur de jeu par exemple (en fait j’ai essayé ça il y a longtemps et même avec LuaJIT vous vous heurtez vite à des limites de performance).

Plutôt, je veux que ma boucle de mise à jour soit en C++ pur, mais alors vous perdez une bonne partie des avantages du scripting, car vous devez avoir quelque part ce code de mise à jour prêt à fonctionner d’une manière ou d’une autre dans votre monde C++. Vous pouvez bien sûr songer à un système de « boucle générique » dans laquelle vous injecteriez des séquences « d’opérations », mais il reste toujours le même problème : quelque part vous devez avoir défini des classes ou des fonctions C++ représentant ces opérations si vous voulez les appeler.

À partir de là, j’ai commencé mon voyage à la recherche de la manière de générer du code C++ dynamiquement (depuis Lua) : si je peux faire cela, ma « passe de configuration Lua » pourra aussi être utilisée pour configurer et construire une fonction de boucle C++ qui sera spécifique à chaque expérience que je veux réaliser, tout en permettant de maintenir une performance maximale.

Alors, c’est parti !

II. Compilateur Tiny C

La première chose que j’ai trouvée fut le projet Tiny C (Tiny C Compiler project), qui semble absolument impressionnant ! Voici un exemple de ce que vous pouvez faire avec la bibliothèque libtcc par exemple (ceci est le code source du fichier officiel libtcc_test.c) :

 
Cacher/Afficher le codeSélectionnez

Vous voyez donc que vous pouvez compiler du code C, le mélanger avec des symboles déjà définis dans votre processus courant, récupérer vos nouvelles fonctions C, etc. C’est magnifique, mais… malheureusement, ce n’était pas suffisant pour remplir le contrat dans mon cas 🙁. La plupart des modules que j’ai définis ou construits sont en C++, pas en C : pour y accéder avec ce type de code généré dynamiquement, je devrais fournir une interface C pour toutes les fonctions/classes auxquelles je pourrais « vouloir accéder dynamiquement un jour »… Et ça ressemble exactement à la limitation initiale mentionnée plus haut : je ne veux pas avoir à préparer un code intermédiaire spécial pour tous les éléments C++ auxquels je peux vouloir accéder ! Générer les interfaces Lua est déjà assez douloureux Image non disponible !

J’ai donc décidé de continuer à chercher une autre solution qui serait plus « compatible avec C++ ». Et c’est alors que j’ai trouvé cet article : Compiler du code C++ en mémoire avec Clang.

A priori, je ne voulais pas réellement aller par là, car Clang me semblait être un monstre géant, je pensais donc qu’il allait être très douloureux de mettre cette option sur les rails. Mais à la fin, j’ai réalisé qu’il n’y a de toute façon pas tant de choix sur ce sujet et j’ai donc décidé que je devrais l’essayer et voir où cela me mènerait.

III. Compiler LLVM et Clang

Pour les étapes de compilation, j’ai utilisé les pages suivantes comme référence :

Je suis sous Windows 10 et j’utilise Visual Studio 2017 comme compilateur de base, les instructions suivantes peuvent donc ne pas fonctionner pour vous si vous êtes sur une plateforme différente.

Comme mentionné sur la page référencée juste au-dessus (Getting Started…), vous devez d’abord vous assurer que votre entrée de configuration Git core.autocrlf est bien mise à false. Notez que vous pouvez obtenir toutes les valeurs de votre configuration Git par :

git config --list

Ensuite, la première étape réelle indispensable est évidemment d’obtenir les sources, mais c’est vraiment simple : git clone https://github.com/llvm/llvm-project.git.

Puis je crée un petit script batch pour exécuter la compilation, car je veux en encapsuler tous les détails :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
REM cf. https://clang.llvm.org/get_started.html
    REM and cf. https://www.llvm.org/docs/CMake.html

    set flavor=%~1
    echo Building %dep_llvm% on %flavor%

    set bdir=%NV_DEPS_DIR%\build\%dep_llvm%
    mkdir "%bdir%\build"

    cd /d "%bdir%\build"
    echo LLVM/Clang build dir is: %cd%

    set idir=%NV_DEPS_DIR%\%flavor%\%dep_llvm%

    REM Python 2.7 or higher is required:
    set PREV_PATH=%PATH%
    set PATH=%NV_TOOLS_DIR%\%tool_python2%\bin;%PATH%

    REM %CMAKE% -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%idir% -DLLVM_ENABLE_PROJECTS=clang -A x64 -Thost=x64 ..\llvm
    %CMAKE% -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%idir% -DLLVM_ENABLE_PROJECTS=clang ..\llvm

    REM %JOM% /K /S /j 8 /NOLOGO
    REM %JOM% install
    nmake
    nmake install
    
    set PATH=%PREV_PATH%
    echo Done building LLVM/Clang.

Dans le script batch ci-dessus, la valeur “flavor” que j’utilise pour le moment est la chaîne “msvc64”. Ensuite, tout ce que je fais se résume fondamentalement à créer le répertoire de construction dédié, puis j’appelle CMake pour générer les fichiers de compilation (j’évite habituellement la compilation depuis l’EDI, je préfère donc le générateur de Makefiles NMAKE).

Finalement, j’appelle nmake et nmake install pour achever le job.

Python 2.7 est requis pour que l’étape de configuration de CMake fonctionne ici, je l’ai donc ajouté dans le PATH avant d’appeler Cmake.

Le générateur de Makefiles pour NMAKE ne supporte pas les arguments -A x64 ou ‑Thost=x64 sur sa ligne de commande, je les ai donc enlevés… mais ça ne semblait pas être un problème pour moi (je suis sur une machine Windows x64 et je ne cible de toute façon que des architectures x64).

J’ai d’abord essayé la compilation en utilisant JOM plutôt que NMAKE, mais ça ne semblait pas marcher directement pour moi 🙁. JOM persistait à ne rien compiler du tout… Je suis donc passé à NMAKE, sans trop y réfléchir, celui-là fonctionne bien, mais bon sang… qu’il est leeeeeeennt ! 🙁 La compilation a pris environ 8 h pour moi. Un jour, si j’en ai l’opportunité, j’aimerais réessayer JOM je pense.

Et… étonnamment, après avoir attendu un trrrrèèèèèssss loooonnnnngggg temps, la compilation s’est terminée correctement ! Cette partie était clairement plus facile que je ne l’attendais :😀!

IV. Construire un compilateur Just-In-Time en bibliothèque partagée

Lorsque j’ai eu les binaires/bibliothèques de LLVM/Clang compilés et installés dans un répertoire approprié, j’ai commencé l’intégration dans mon propre projet, en essayant de construire une bibliothèque partagée dédiée qui encapsulerait la génération dynamique de code C++. J’ai nommé le module nvLLVM et j’ai commencé avec comme base l’article de Matthieu Brucher mentionné plus haut.

Voici les deux fichiers d’en-tête principaux que j’ai créés pour ce module :

  • d’abord le fichier llvm_common.h, qui sert d’interface d’exportation me permettant de retrouver ma fonction de test plus tard :
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
#ifndef LLVM_COMMON_
#define LLVM_COMMON_
 
#if defined(_MSC_VER) || defined(__CYGWIN__) || defined(__MINGW32__) || defined(__BCPLUSPLUS__) || defined(__MWERKS__)
#if defined(NV_LIB_STATIC)
#define NVLLVM_EXPORT
#elif defined(NVLLVM_LIB)
#define NVLLVM_EXPORT __declspec(dllexport)
#else
#define NVLLVM_EXPORT __declspec(dllimport)
#endif
#else
#define NVLLVM_EXPORT
#endif
 
#if defined(_WIN32) && !defined(_WIN32_WINNT)
#define _WIN32_WINNT 0x0602
#endif
 
#include <string>
 
NVLLVM_EXPORT void runClang(const std::string& file);
 
#endif
  • Puis l’en-tête llvm_precomp.h, qui contient la plupart des en-têtes requis par LLVM/Clang pour construire notre fonction de test :
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
#ifndef LLVM_PRECOMP_
#define LLVM_PRECOMP_
 
#include <llvm_common.h>
 
// cf. https://docs.microsoft.com/fr-fr/cpp/preprocessor/warning?view=vs-2019
#pragma warning( push )
#pragma warning( disable : 4244 ) // 'initializing': conversion from '_Ty' to '_Ty1', possible loss of data
#pragma warning( disable : 4624 ) // destructor was implicitly defined as deleted
#pragma warning( disable : 4141 ) // 'inline': used more than once
#pragma warning( disable : 4291 ) // no matching operator delete found; memory will not be freed if initialization throws an exception
 
#include <sstream>
#include <llvm/InitializePasses.h>
#include <llvm/ExecutionEngine/ExecutionEngine.h>
#include <llvm/ExecutionEngine/MCJIT.h>
#include <llvm/ExecutionEngine/SectionMemoryManager.h>
#include <llvm/IR/DataLayout.h>
#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/PassManager.h>
#include <llvm/Passes/PassBuilder.h>
#include <llvm/Support/MemoryBuffer.h>
#include <llvm/Support/TargetSelect.h>
#include <llvm/Support/TargetRegistry.h>
#include <llvm/Support/Host.h>
#include <llvm/Support/raw_ostream.h>
 
#include "llvm/ExecutionEngine/JITSymbol.h"
#include "llvm/ExecutionEngine/Orc/CompileUtils.h"
#include "llvm/ExecutionEngine/Orc/Core.h"
#include "llvm/ExecutionEngine/Orc/ExecutionUtils.h"
#include "llvm/ExecutionEngine/Orc/IRCompileLayer.h"
#include "llvm/ExecutionEngine/Orc/JITTargetMachineBuilder.h"
#include "llvm/ExecutionEngine/Orc/RTDyldObjectLinkingLayer.h"
 
#include <clang/Basic/DiagnosticOptions.h>
#include <clang/Basic/Diagnostic.h>
#include <clang/Basic/FileManager.h>
#include <clang/Basic/FileSystemOptions.h>
#include <clang/Basic/LangOptions.h>
#include <MemoryBufferCache.h>
// #include <clang/Basic/MemoryBufferCache.h>
#include <clang/Basic/SourceManager.h>
#include <clang/Basic/TargetInfo.h>
#include <clang/CodeGen/CodeGenAction.h>
#include <clang/Frontend/CompilerInstance.h>
#include <clang/Frontend/CompilerInvocation.h>
#include <clang/Frontend/TextDiagnosticPrinter.h>
#include <clang/Lex/HeaderSearch.h>
#include <clang/Lex/HeaderSearchOptions.h>
#include <clang/Lex/Preprocessor.h>
#include <clang/Lex/PreprocessorOptions.h>
#include <clang/Parse/ParseAST.h>
#include <clang/Sema/Sema.h>
#include <clang/AST/ASTContext.h>
#include <clang/AST/ASTConsumer.h>
 
#pragma warning( pop )
 
#endif

J’ai fait quelques changements à ce niveau en comparaison de la version fournie par Matthieu Brucher :

  • j’ai désactivé un tas d’avertissements du compilateur Visual Studio 2017 (rien de trop sérieux, je pense… ou au moins rien sur lequel je pourrais agir autrement : je ne vais pas modifier les fichiers d’en-tête de LLVM !) qui polluaient les sorties de ma compilation ;
  • j’ai dû remplacer le fichier d’inclusion clang/Basic/MemoryBufferCache.h par une version locale de ce fichier : la version LLVM que j’utilise depuis git est la version 11.0.0git (telle que rapportée par la config de CMake pour LLVM au moins, voyez ci-dessous). Dans cette version, le fichier clang/Basic/MemoryBufferCache.h n’existe plus. Par chance, j’ai pu trouver en ligne les fichiers d’en-tête et d’implémentation que j’ai ajoutés au module :
 
Cacher/Afficher le codeSélectionnez
 
Cacher/Afficher le codeSélectionnez

Puis vient le fichier d’implémentation principal où j’essaie de reproduire le processus de compilation dynamique en C++ :

 
Cacher/Afficher le codeSélectionnez

Je n’ai pas beaucoup modifié le début de ce fichier, mais j’ai dû ensuite remplacer quelques unique_ptr par les conteneurs IntrusiveRefCntPtr fournis par LLVM (c’était indispensable, puisque le code initial ne compilait pas).

J’ai aussi ajouté quelques sorties de débogage à l’appel de fonctions qui étaient définies comme arguments dans le code C++ fourni (comme, dans cet exemple simple, je m’attends simplement à trouver les fonctions nv_add et nv_sub).

V. Fichier de configuration pour Cmake

Une chose qui manquait dans l’article original de Matthieu Brucher était les fichiers de configuration de compilation autour de ce type de module partagé. Pour ma part, j’utilise CMake pour mon projet et voici ce à quoi je suis arrivé jusqu’ici.

À la racine de ce module nvLLVM, j’ai le fichier cmakelist.txt suivant :

 
Cacher/Afficher le codeSélectionnez

Ensuite, j’ai un répertoire src où je mets les fichiers .cpp et le fichier CMake suivant :

 
Cacher/Afficher le codeSélectionnez

Comme vous pouvez le voir ci-dessus, j’ai fait quelques tests dans les fichiers CMake avant de trouver la manière de construire ma bibliothèque proprement.

La première chose à relever est que les bibliothèques LLVM sont statiques et utilisent le runtime C statique, alors que la plupart de mes autres modules utilisent le runtime C dynamique. J’ai donc dû ici uniquement construire un module partagé et spécifier la valeur de CMAKE_CXX_FLAGS à /MT.

J’ai aussi passé pas mal de temps à essayer de trouver avec quelles bibliothèques de LLVM et Clang je devais lier exactement. Au début, je liais avec le fichier LLVM-C.lib, mais c’était une mauvaise idée, car, comme résultat, j’ai obtenu une erreur à la création de mon ExecutionEngine sur l’appel à auto executionEngine = builder.create(); disant que JIT has not been linked in… Au lieu de cela, vous devez vraiment lier à toutes les bibliothèques LLVM que vous obtenez lorsque vous appelez llvm-config –libs (comme c’est le cas dans le fichier CMake ci-dessus). Notez que cette liste n’inclut pas la bibliothèque LLVM-C.

⇒ Avec les fichiers CMake et sources ci-dessus, j’ai pu générer avec succès mon module nvLLVM.dll ! C’est un fichier géant de 49 MB, mais il ne dépend plus d’aucune bibliothèque LLVM supplémentaire (comme LLVM-C.dll) et je peux l’utiliser avec succès dans une application de test simple avec un appel de test à la fonction de test runClang() que j’ai définie ici ! Donc, ce module semble contenir un compilateur C++ complet, fonctionnel et indépendant, ce qui est absolument incroyable de mon point de vue !

VI. Application de test du compilateur Just-In-Time

L’application de test minimale que j’ai utilisée ici était simplement :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
#include <iostream>
 
#define DEBUG_MSG(msg) std::cout << msg << std::endl;
 
#include <llvm_common.h>
 
int main(int argc, char *argv[])
{
    DEBUG_MSG("Running clang compilation...");
    runClang("W:/Projects/NervSeed/temp/test1.cxx");
    DEBUG_MSG("Done running clang compilation.");
 
    return 0;
}

Avec comme fichier CMake :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
SET(TARGET_NAME "test_nvLLVM")
SET(TARGET_DIR "./")

ADD_DEFINITIONS(-D_CRT_SECURE_NO_WARNINGS)

FILE(GLOB_RECURSE SOURCE_FILES "*.cpp" )

INCLUDE_DIRECTORIES (${SRC_DIR}/nvLLVM/include)

ADD_EXECUTABLE (${TARGET_NAME} ${SOURCE_FILES})
TARGET_LINK_LIBRARIES(${TARGET_NAME} nvLLVM)

SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES PREFIX "")

COMPRESS_BINARY_TARGET()

INSTALL(TARGETS ${TARGET_NAME}
    RUNTIME DESTINATION ${TARGET_DIR}
    LIBRARY DESTINATION ${TARGET_DIR})

INSTALL_PDB()

J’ai obtenu les sorties suivantes :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
//  (... lots of LLVM statistics here since they are enabled in my code above...)
===-------------------------------------------------------------------------===
                          ... Statistics Collected ...
===-------------------------------------------------------------------------===

2 file-search - Number of directory cache misses.
2 file-search - Number of directory lookups.
1 file-search - Number of file cache misses.
1 file-search - Number of file lookups.

[DEBUG]: Using target triple: x86_64-pc-windows-msvc
[DEBUG]: Retrieving nv_add/nv_sub functions...
[ERROR]: The meaning of life is: 42!
[ERROR]: The meaning of life is really: 42!
[DEBUG]: leaving runClang() function.

Bien sûr le contenu de test1.cxx que j’ai fourni ci-dessus est simplement (comme on pouvait l’attendre) :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
int nv_add(int a, int b)
{
    return a+b;
}
 
int nv_sub(int a, int b)
{
    return a-b;
}

Bien… De ce que je comprends, ces résultats signifient que le compilateur a compilé avec succès le code de ce fichier test1, optimisé ce code et l’a chargé dans le contexte de LLVM, de sorte que nous avons pu l’utiliser directement comme nous l’avons fait, en récupérant les pointeurs de fonction et en appelant ces fonctions ! N’est-ce pas stupéfiant ?!

VII. Prochaines étapes

Maintenant que j’ai un JIT de base initial fonctionnel, il y a pas mal de recherches/tests à faire dans cette direction.

  • J’ai trouvé cet article officiel Building a JIT: Starting out with KaleidoscopeJIT, qui semble très prometteur et souple, je dois donc absolument l’étudier plus en détail et l’essayer si possible.
  • Je dois aussi tenter de lier avec mes modules C++ existant pour voir si tout fonctionne comme attendu.
  • J’ai aussi noté que nous pouvons fournir des entrées “from memory” plutôt que “from file” (je pense ?) : ce serait une excellente chose à avoir !
  • Et je dois garder en mémoire que mon but final est de pouvoir générer du code C++ avec Lua. Je devrai vraiment nettoyer et revoir le code ci-dessus pour le rendre plus « prêt pour la production », et ensuite générer les interfaces nécessaires, bien sûr. Image non disponible

Mais c’est tout pour aujourd’hui, de toute façon ! Tous les points restants seront pour une prochaine fois !

VIII. Notes et références additionnelles

J’ai aussi trouvé cet article The simplest way to compile C++ with Clang at runtime :

  • celui-là semble essayer de gérer les choses à un niveau encore plus élevé, en appelant juste la fonction « main » que vous trouverez typiquement dans l’exécutable de Clang lui-même si je comprends correctement ;
  • ça semble un peu trop élevé à mon goût, mais il mentionne aussi le concept d’« injecter le module compilé » dans un objet « JIT » et il fournit aussi un projet compagnon sur GitHub pour construire un JIT à partir de zéro  : JitFromScratch.

Nous avons aussi ce dépôt GitHub avec quelques « tutoriels Clang » : https://github.com/loarabia/Clang-tutorial.

Le code semble assez vieux, je ne sais pas si ça en vaut encore la peine.

Ceci peut aussi valoir la lecture un jour : Using libclang to Parse C++ (aka libclang 101).

La documentation en ligne de LLVM couvre aussi certains sujets/aspects intéressants https://llvm.org/docs/Reference.html. Par exemple : ORC Design and Implementation.

IX. Remerciements Developpez.com

Ce tutoriel est la traduction de Dynamic C++ compilation with LLVM & clang. Nous tenons à remercier Thierry Jeanneret pour la traduction, Thibaut Cuvelier pour la relecture technique, Malick pour la mise au gabarit et Claude Leloup pour la relecture orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Licence Creative Commons
Le contenu de cet article est rédigé par Emmanuel Roche et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.