Compilation C++ avec LLVM et clang

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

Emmanuel ROCHE

Traducteur

Thierry Jeanneret

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. Commentez

- Titre : Compilation C++ avec LLVM et clang

- Auteur : Emmanuel ROCHE

- Traducteur : Thierry Jeanneret

- Parution : 26 juin 2020

- Licence : 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.

Voir également :

- Compilation C++ JIT avec LLVM - Partie 4 : dépendance à l’environnement d’exécution C

- Compilation C++ JIT avec LLVM - Partie 3 : Corriger le crash du ModulePassManager

- Compilation C++ avec LLVM - Partie 2 : construire un compilateur C++ JIT fonctionnel avec LLVM

Sommaire

- I. Introduction

- II. Compilateur Tiny C

- III. Compiler LLVM et Clang

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

- V. Fichier de configuration pour Cmake

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

- VII. Prochaines étapes

- VIII. Notes et références additionnelles

- IX. Remerciements Developpez.com

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) :

1. /*

2. * Simple Test program for libtcc

3. *

4. * libtcc can be useful to use tcc as a "backend" for a code generator.

5. */

6. #include <stdlib.h>

7. #include <stdio.h>

8. #include <string.h>

10. #include "libtcc.h"

12. /* this function is called by the generated code */

13. int add(int a, int b)

14. {

15. return a + b;

16. }

18. /* this strinc is referenced by the generated code */

19. const char hello[] = "Hello World!";

21. char my_program[] =

22. "#include <tcclib.h>\n" /* include the "Simple libc header for TCC" */

23. "extern int add(int a, int b);\n"

24. "#ifdef _WIN32\n" /* dynamically linked data needs 'dllimport' */

25. " __attribute__((dllimport))\n"

26. "#endif\n"

27. "extern const char hello[];\n"

28. "int fib(int n)\n"

29. "{\n"

30. "    if (n <= 2)\n"

31. "        return 1;\n"

32. "    else\n"

33. "        return fib(n-1) + fib(n-2);\n"

34. "}\n"

35. "\n"

36. "int foo(int n)\n"

37. "{\n"

38. "    printf(\"%s\\n\", hello);\n"

39. "    printf(\"fib(%d) = %d\\n\", n, fib(n));\n"

40. "    printf(\"add(%d, %d) = %d\\n\", n, 2 * n, add(n, 2 * n));\n"

41. "    return 0;\n"

42. "}\n";

44. int main(int argc, char **argv)

45. {

46. TCCState *s;

47. int i;

48. int (*func)(int);

50. s = tcc_new();

51. if (!s) {

52. fprintf(stderr, "Could not create tcc state\n");

53. exit(1);

54. }

56. /* if tcclib.h and libtcc1.a are not installed, where can we find them */

57. for (i = 1; i < argc; ++i) {

58. char *a = argv[i];

59. if (a[0] == '-') {

60. if (a[1] == 'B')

61. tcc_set_lib_path(s, a+2);

62. else if (a[1] == 'I')

63. tcc_add_include_path(s, a+2);

64. else if (a[1] == 'L')

65. tcc_add_library_path(s, a+2);

66. }

67. }

69. /* MUST BE CALLED before any compilation */

70. tcc_set_output_type(s, TCC_OUTPUT_MEMORY);

72. if (tcc_compile_string(s, my_program) == -1)

73. return 1;

75. /* as a test, we add symbols that the compiled program can use.

76. You may also open a dll with tcc_add_dll() and use symbols from that */

77. tcc_add_symbol(s, "add", add);

78. tcc_add_symbol(s, "hello", hello);

80. /* relocate the code */

81. if (tcc_relocate(s, TCC_RELOCATE_AUTO) < 0)

82. return 1;

84. /* get entry symbol */

85. func = tcc_get_symbol(s, "foo");

86. if (!func)

87. return 1;

89. /* run the code */

90. func(32);

92. /* delete the state */

93. tcc_delete(s);

95. return 0;

96. }

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  !

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 :

- Getting Started: Building and Running Clang (en anglais uniquement) ;

- Building LLVM with Cmake (en anglais uniquement).

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 :

1. REM cf. https://clang.llvm.org/get_started.html

2. REM and cf. https://www.llvm.org/docs/CMake.html

4. set flavor=%~1

5. echo Building %dep_llvm% on %flavor%

7. set bdir=%NV_DEPS_DIR%\build\%dep_llvm%

8. mkdir "%bdir%\build"

10. cd /d "%bdir%\build"

11. echo LLVM/Clang build dir is: %cd%

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

15. REM Python 2.7 or higher is required:

16. set PREV_PATH=%PATH%

17. set PATH=%NV_TOOLS_DIR%\%tool_python2%\bin;%PATH%

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

20. %CMAKE% -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=%idir% -DLLVM_ENABLE_PROJECTS=clang ..\llvm

22. REM %JOM% /K /S /j 8 /NOLOGO

23. REM %JOM% install

24. nmake

25. nmake install

27. set PATH=%PREV_PATH%

28. 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 :

1. #ifndef LLVM_COMMON_

2. #define LLVM_COMMON_

4. #if defined(_MSC_VER) || defined(__CYGWIN__) || defined(__MINGW32__) || defined(__BCPLUSPLUS__) || defined(__MWERKS__)

5. #if defined(NV_LIB_STATIC)

6. #define NVLLVM_EXPORT

7. #elif defined(NVLLVM_LIB)

8. #define NVLLVM_EXPORT __declspec(dllexport)

9. #else

10. #define NVLLVM_EXPORT __declspec(dllimport)

11. #endif

12. #else

13. #define NVLLVM_EXPORT

14. #endif

16. #if defined(_WIN32) && !defined(_WIN32_WINNT)

17. #define _WIN32_WINNT 0x0602

18. #endif

20. #include <string>

22. NVLLVM_EXPORT void runClang(const std::string& file);

24. #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 :

1. #ifndef LLVM_PRECOMP_

2. #define LLVM_PRECOMP_

4. #include <llvm_common.h>

6. // cf. https://docs.microsoft.com/fr-fr/cpp/preprocessor/warning?view=vs-2019

7. #pragma warning( push )

8. #pragma warning( disable : 4244 ) // 'initializing': conversion from '_Ty' to '_Ty1', possible loss of data

9. #pragma warning( disable : 4624 ) // destructor was implicitly defined as deleted

10. #pragma warning( disable : 4141 ) // 'inline': used more than once

11. #pragma warning( disable : 4291 ) // no matching operator delete found; memory will not be freed if initialization throws an exception

13. #include <sstream>

14. #include <llvm/InitializePasses.h>

15. #include <llvm/ExecutionEngine/ExecutionEngine.h>

16. #include <llvm/ExecutionEngine/MCJIT.h>

17. #include <llvm/ExecutionEngine/SectionMemoryManager.h>

18. #include <llvm/IR/DataLayout.h>

19. #include <llvm/IR/LLVMContext.h>

20. #include <llvm/IR/PassManager.h>

21. #include <llvm/Passes/PassBuilder.h>

22. #include <llvm/Support/MemoryBuffer.h>

23. #include <llvm/Support/TargetSelect.h>

24. #include <llvm/Support/TargetRegistry.h>

25. #include <llvm/Support/Host.h>

26. #include <llvm/Support/raw_ostream.h>

28. #include "llvm/ExecutionEngine/JITSymbol.h"

29. #include "llvm/ExecutionEngine/Orc/CompileUtils.h"

30. #include "llvm/ExecutionEngine/Orc/Core.h"

31. #include "llvm/ExecutionEngine/Orc/ExecutionUtils.h"

32. #include "llvm/ExecutionEngine/Orc/IRCompileLayer.h"

33. #include "llvm/ExecutionEngine/Orc/JITTargetMachineBuilder.h"

34. #include "llvm/ExecutionEngine/Orc/RTDyldObjectLinkingLayer.h"

36. #include <clang/Basic/DiagnosticOptions.h>

37. #include <clang/Basic/Diagnostic.h>

38. #include <clang/Basic/FileManager.h>

39. #include <clang/Basic/FileSystemOptions.h>

40. #include <clang/Basic/LangOptions.h>

41. #include <MemoryBufferCache.h>

42. // #include <clang/Basic/MemoryBufferCache.h>

43. #include <clang/Basic/SourceManager.h>

44. #include <clang/Basic/TargetInfo.h>

45. #include <clang/CodeGen/CodeGenAction.h>

46. #include <clang/Frontend/CompilerInstance.h>

47. #include <clang/Frontend/CompilerInvocation.h>

48. #include <clang/Frontend/TextDiagnosticPrinter.h>

49. #include <clang/Lex/HeaderSearch.h>

50. #include <clang/Lex/HeaderSearchOptions.h>

51. #include <clang/Lex/Preprocessor.h>

52. #include <clang/Lex/PreprocessorOptions.h>

53. #include <clang/Parse/ParseAST.h>

54. #include <clang/Sema/Sema.h>

55. #include <clang/AST/ASTContext.h>

56. #include <clang/AST/ASTConsumer.h>

58. #pragma warning( pop )

60. #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 :

1. //===- MemoryBufferCache.h - Cache for loaded memory buffers ----*- C++ -*-===//

2. //

3. // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.

4. // See https://llvm.org/LICENSE.txt for license information.

5. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

6. //

7. //===----------------------------------------------------------------------===//

9. // cf. https://clang.llvm.org/doxygen/MemoryBufferCache_8h_source.html

11. #ifndef LLVM_CLANG_BASIC_MEMORYBUFFERCACHE_H

12. #define LLVM_CLANG_BASIC_MEMORYBUFFERCACHE_H

14. #include "llvm/ADT/IntrusiveRefCntPtr.h"

15. #include "llvm/ADT/StringMap.h"

16. #include <memory>

18. namespace llvm {

19. class MemoryBuffer;

20. } // end namespace llvm

22. namespace clang {

24. /// Manage memory buffers across multiple users.

25. ///

26. /// Ensures that multiple users have a consistent view of each buffer.  This is

27. /// used by \a CompilerInstance when building PCMs to ensure that each \a

28. /// ModuleManager sees the same files.

29. ///

30. /// \a finalizeCurrentBuffers() should be called before creating a new user.

31. /// This locks in the current buffers, ensuring that no buffer that has already

32. /// been accessed can be purged, preventing use-after-frees.

33. class MemoryBufferCache : public llvm::RefCountedBase<MemoryBufferCache> {

34. struct BufferEntry {

35. std::unique_ptr<llvm::MemoryBuffer> Buffer;

37. /// Track the timeline of when this was added to the cache.

38. unsigned Index;

39. };

41. /// Cache of buffers.

42. llvm::StringMap<BufferEntry> Buffers;

44. /// Monotonically increasing index.

45. unsigned NextIndex = 0;

47. /// Bumped to prevent "older" buffers from being removed.

48. unsigned FirstRemovableIndex = 0;

50. public:

51. /// Store the Buffer under the Filename.

52. ///

53. /// \pre There is not already buffer is not already in the cache.

54. /// \return a reference to the buffer as a convenience.

55. llvm::MemoryBuffer &addBuffer(llvm::StringRef Filename,

56. std::unique_ptr<llvm::MemoryBuffer> Buffer);

58. /// Try to remove a buffer from the cache.

59. ///

60. /// \return false on success, iff \c !isBufferFinal().

61. bool tryToRemoveBuffer(llvm::StringRef Filename);

63. /// Get a pointer to the buffer if it exists; else nullptr.

64. llvm::MemoryBuffer *lookupBuffer(llvm::StringRef Filename);

66. /// Check whether the buffer is final.

67. ///

68. /// \return true iff \a finalizeCurrentBuffers() has been called since the

69. /// buffer was added.  This prevents buffers from being removed.

70. bool isBufferFinal(llvm::StringRef Filename);

72. /// Finalize the current buffers in the cache.

73. ///

74. /// Should be called when creating a new user to ensure previous uses aren't

75. /// invalidated.

76. void finalizeCurrentBuffers();

77. };

79. } // end namespace clang

81. #endif // LLVM_CLANG_BASIC_MEMORYBUFFERCACHE_H

1. //===- MemoryBufferCache.cpp - Cache for loaded memory buffers ------------===//

2. //

3. // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.

4. // See https://llvm.org/LICENSE.txt for license information.

5. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

6. //

7. //===----------------------------------------------------------------------===//

9. //  cf. https://clang.llvm.org/doxygen/MemoryBufferCache_8cpp_source.html

11. #include <llvm_precomp.h>

12. #include <llvm/Support/MemoryBuffer.h>

14. using namespace clang;

16. llvm::MemoryBuffer &

17. MemoryBufferCache::addBuffer(llvm::StringRef Filename,

18. std::unique_ptr<llvm::MemoryBuffer> Buffer) {

19. auto Insertion =

20. Buffers.insert({Filename, BufferEntry{std::move(Buffer), NextIndex++}});

21. assert(Insertion.second && "Already has a buffer");

22. return *Insertion.first->second.Buffer;

23. }

25. llvm::MemoryBuffer *MemoryBufferCache::lookupBuffer(llvm::StringRef Filename) {

26. auto I = Buffers.find(Filename);

27. if (I == Buffers.end())

28. return nullptr;

29. return I->second.Buffer.get();

30. }

32. bool MemoryBufferCache::isBufferFinal(llvm::StringRef Filename) {

33. auto I = Buffers.find(Filename);

34. if (I == Buffers.end())

35. return false;

36. return I->second.Index < FirstRemovableIndex;

37. }

39. bool MemoryBufferCache::tryToRemoveBuffer(llvm::StringRef Filename) {

40. auto I = Buffers.find(Filename);

41. assert(I != Buffers.end() && "No buffer to remove...");

42. if (I->second.Index < FirstRemovableIndex)

43. return true;

45. Buffers.erase(I);

46. return false;

47. }

49. void MemoryBufferCache::finalizeCurrentBuffers() { FirstRemovableIndex = NextIndex; }

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

1. #include <llvm_precomp.h>

3. #include <iostream>

5. #define NV_LLVM_VERBOSE 1

7. bool LLVMinit = false;

9. #define ERROR_MSG(msg) std::cout << "[ERROR]: "<<msg<< std::endl;

10. #define DEBUG_MSG(msg) std::cout << "[DEBUG]: "<<msg<< std::endl;

13. void InitializeLLVM()

14. {

15. if (LLVMinit)

16. {

17. return;

18. }

20. // We have not initialized any pass managers for any device yet.

21. // Run the global LLVM pass initialization functions.

22. llvm::InitializeNativeTarget();

23. llvm::InitializeNativeTargetAsmPrinter();

24. llvm::InitializeNativeTargetAsmParser();

26. auto& Registry = *llvm::PassRegistry::getPassRegistry();

28. llvm::initializeCore(Registry);

29. llvm::initializeScalarOpts(Registry);

30. llvm::initializeVectorization(Registry);

31. llvm::initializeIPO(Registry);

32. llvm::initializeAnalysis(Registry);

33. llvm::initializeTransformUtils(Registry);

34. llvm::initializeInstCombine(Registry);

35. llvm::initializeInstrumentation(Registry);

36. llvm::initializeTarget(Registry);

39. LLVMinit = true;

40. }

42. void runClang(const std::string& file)

43. {

44. InitializeLLVM();

46. clang::IntrusiveRefCntPtr<clang::DiagnosticOptions> diagnosticOptions = new clang::DiagnosticOptions;

47. // clang::DiagnosticOptions diagnosticOptions;

49. std::unique_ptr<clang::TextDiagnosticPrinter> textDiagnosticPrinter = std::make_unique<clang::TextDiagnosticPrinter>(llvm::outs(), diagnosticOptions.get());

50. // std:: unique_ptr <clang::DiagnosticIDs> diagIDs;

51. clang::IntrusiveRefCntPtr<clang::DiagnosticIDs> diagIDs;

53. clang::IntrusiveRefCntPtr<clang::DiagnosticsEngine> diagnosticsEngine = new clang::DiagnosticsEngine(diagIDs, diagnosticOptions, textDiagnosticPrinter.get());

55. clang::CompilerInstance compilerInstance;

56. auto& compilerInvocation = compilerInstance.getInvocation();

58. std::stringstream ss;

59. ss << "-triple=" << llvm::sys::getDefaultTargetTriple();

61. std::istream_iterator<std::string> begin(ss);

62. std::istream_iterator<std::string> end;

63. std::istream_iterator<std::string> i = begin;

64. std::vector<const char*> itemcstrs;

65. std::vector<std::string> itemstrs;

66. while (i != end) {

67. itemstrs.push_back(*i);

68. ++i;

69. }

71. for (unsigned idx = 0; idx < itemstrs.size(); idx++) {

72. // note: if itemstrs is modified after this, itemcstrs will be full

73. // of invalid pointers! Could make copies, but would have to clean up then...

74. itemcstrs.push_back(itemstrs[idx].c_str());

75. }

77. // clang::CompilerInvocation::CreateFromArgs(compilerInvocation, itemcstrs.data(), itemcstrs.data() + itemcstrs.size(), *diagnosticsEngine.release());

78. clang::CompilerInvocation::CreateFromArgs(compilerInvocation, llvm::ArrayRef(itemcstrs.data(), itemcstrs.size()), *diagnosticsEngine.get());

80. auto* languageOptions = compilerInvocation.getLangOpts();

81. auto& preprocessorOptions = compilerInvocation.getPreprocessorOpts();

82. auto& targetOptions = compilerInvocation.getTargetOpts();

83. auto& frontEndOptions = compilerInvocation.getFrontendOpts();

84. #ifdef NV_LLVM_VERBOSE

85. frontEndOptions.ShowStats = true;

86. #endif

87. auto& headerSearchOptions = compilerInvocation.getHeaderSearchOpts();

88. #ifdef NV_LLVM_VERBOSE

89. headerSearchOptions.Verbose = true;

90. #endif

91. auto& codeGenOptions = compilerInvocation.getCodeGenOpts();

93. // llvm::StringRef filename = "W:/Projects/NervSeed/temp/test1.cxx";

94. llvm::StringRef filename = file.c_str();

96. frontEndOptions.Inputs.clear();

97. frontEndOptions.Inputs.push_back(clang::FrontendInputFile(filename, clang::InputKind(clang::Language::CXX)));

99. targetOptions.Triple = llvm::sys::getDefaultTargetTriple();

100. compilerInstance.createDiagnostics(textDiagnosticPrinter.get(), false);

102. llvm::LLVMContext context;

103. std::unique_ptr<clang::CodeGenAction> action = std::make_unique<clang::EmitLLVMOnlyAction>(&context);

105. if (!compilerInstance.ExecuteAction(*action))

106. {

107. ERROR_MSG("Cannot execute action with compiler instance.");

108. }

110. std::unique_ptr<llvm::Module> module = action->takeModule();

111. if (!module)

112. {

113. ERROR_MSG("Cannot retrieve IR module.");

114. }

116. llvm::PassBuilder passBuilder;

117. llvm::LoopAnalysisManager loopAnalysisManager(codeGenOptions.DebugPassManager);

118. llvm::FunctionAnalysisManager functionAnalysisManager(codeGenOptions.DebugPassManager);

119. llvm::CGSCCAnalysisManager cGSCCAnalysisManager(codeGenOptions.DebugPassManager);

120. llvm::ModuleAnalysisManager moduleAnalysisManager(codeGenOptions.DebugPassManager);

122. passBuilder.registerModuleAnalyses(moduleAnalysisManager);

123. passBuilder.registerCGSCCAnalyses(cGSCCAnalysisManager);

124. passBuilder.registerFunctionAnalyses(functionAnalysisManager);

125. passBuilder.registerLoopAnalyses(loopAnalysisManager);

126. passBuilder.crossRegisterProxies(loopAnalysisManager, functionAnalysisManager, cGSCCAnalysisManager, moduleAnalysisManager);

128. llvm::ModulePassManager modulePassManager = passBuilder.buildPerModuleDefaultPipeline(llvm::PassBuilder::OptimizationLevel::O3);

129. modulePassManager.run(*module, moduleAnalysisManager);

131. llvm::EngineBuilder builder(std::move(module));

132. builder.setMCJITMemoryManager(std::make_unique<llvm::SectionMemoryManager>());

133. builder.setOptLevel(llvm::CodeGenOpt::Level::Aggressive);

135. std::string createErrorMsg;

136. // builder.setEngineKind(llvm::EngineKind::Interpreter);

137. builder.setEngineKind(llvm::EngineKind::JIT);

138. builder.setVerifyModules(true);

139. builder.setErrorStr(&createErrorMsg);

141. std::string triple = llvm::sys::getDefaultTargetTriple();

142. DEBUG_MSG("Using target triple: "<<triple);

143. auto executionEngine = builder.create();

145. if (!executionEngine)

146. {

147. ERROR_MSG("Cannot create execution engine.'"<<createErrorMsg<<"'");

148. }

150. DEBUG_MSG("Retrieving nv_add/nv_sub functions...");

151. typedef int(*AddFunc)(int,int);

152. typedef int(*SubFunc)(int,int);

154. AddFunc add = reinterpret_cast<AddFunc>(executionEngine->getFunctionAddress("nv_add"));

155. if(!add) {

156. ERROR_MSG("Cannot retrieve Add function.");

157. }

158. else {

159. int res = add(40,2);

160. ERROR_MSG("The meaning of life is: "<<res<<"!");

161. }

163. SubFunc sub = reinterpret_cast<SubFunc>(executionEngine->getFunctionAddress("nv_sub"));

164. if(!sub) {

165. ERROR_MSG("Cannot retrieve Sub function.");

166. }

167. else {

168. int res = sub(50,8);

169. ERROR_MSG("The meaning of life is really: "<<res<<"!");

170. }

172. DEBUG_MSG("leaving runClang() function.");

173. // return reinterpret_cast<Function>(executionEngine->getFunctionAddress(function));

174. }

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 :

1. SET(TARGET_DIR "./")

3. # https://llvm.org/docs/CMake.html

4. SET(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${LLVM_CLANG_DIR})

6. # message(STATUS "CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}")

8. # We should try to find the LLVM package:

9. find_package(LLVM REQUIRED CONFIG)

10. message(STATUS "Using LLVM ${LLVM_PACKAGE_VERSION}")

11. # message(STATUS "Using LLVMConfig.cmake in: ${LLVM_DIR}")

13. include_directories(${LLVM_INCLUDE_DIRS})

14. # message(STATUS "LLVM includes: ${LLVM_INCLUDE_DIRS}")

16. add_definitions(${LLVM_DEFINITIONS})

17. add_definitions(-D_SILENCE_CXX17_ITERATOR_BASE_CLASS_DEPRECATION_WARNING)

18. # add_definitions(-D_CRT_SECURE_NO_DEPRECATE -D_CRT_SECURE_NO_WARNINGS -D_CRT_NONSTDC_NO_DEPRECATE -D_CRT_NONSTDC_NO_WARNINGS -D_SCL_SECURE_NO_DEPRECATE -D_SCL_SECURE_NO_WARNINGS -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS)

19. # message(STATUS "Using LLVM definitions: ${LLVM_DEFINITIONS}")

21. # This is needed to ensure we use the same C runtime as the LLVM components:

22. SET(CMAKE_CXX_FLAGS "/EHsc /MD")

24. # Note: used llvm-config.exe --libs to retrieve the list of libraries below:

25. SET(LLVM_LIBS LLVMXRay LLVMWindowsManifest LLVMTableGen LLVMSymbolize LLVMDebugInfoPDB LLVMOrcJIT LLVMOrcError LLVMJITLink LLVMObjectYAML LLVMMCA LLVMLTO LLVMPasses LLVMCoroutines LLVMObjCARCOpts LLVMLineEditor LLVMLibDriver LLVMInterpreter LLVMFuzzMutate LLVMMCJIT LLVMExecutionEngine LLVMRuntimeDyld LLVMDWARFLinker LLVMDlltoolDriver LLVMOption LLVMDebugInfoGSYM LLVMCoverage LLVMXCoreDisassembler LLVMXCoreCodeGen LLVMXCoreDesc LLVMXCoreInfo LLVMX86Disassembler LLVMX86AsmParser LLVMX86CodeGen LLVMX86Desc LLVMX86Utils LLVMX86Info LLVMWebAssemblyDisassembler LLVMWebAssemblyCodeGen LLVMWebAssemblyDesc LLVMWebAssemblyAsmParser LLVMWebAssemblyInfo LLVMSystemZDisassembler LLVMSystemZCodeGen LLVMSystemZAsmParser LLVMSystemZDesc LLVMSystemZInfo LLVMSparcDisassembler LLVMSparcCodeGen LLVMSparcAsmParser LLVMSparcDesc LLVMSparcInfo LLVMRISCVDisassembler LLVMRISCVCodeGen LLVMRISCVAsmParser LLVMRISCVDesc LLVMRISCVUtils LLVMRISCVInfo LLVMPowerPCDisassembler LLVMPowerPCCodeGen LLVMPowerPCAsmParser LLVMPowerPCDesc LLVMPowerPCInfo LLVMNVPTXCodeGen LLVMNVPTXDesc LLVMNVPTXInfo LLVMMSP430Disassembler LLVMMSP430CodeGen LLVMMSP430AsmParser LLVMMSP430Desc LLVMMSP430Info LLVMMipsDisassembler LLVMMipsCodeGen LLVMMipsAsmParser LLVMMipsDesc LLVMMipsInfo LLVMLanaiDisassembler LLVMLanaiCodeGen LLVMLanaiAsmParser LLVMLanaiDesc LLVMLanaiInfo LLVMHexagonDisassembler LLVMHexagonCodeGen LLVMHexagonAsmParser LLVMHexagonDesc LLVMHexagonInfo LLVMBPFDisassembler LLVMBPFCodeGen LLVMBPFAsmParser LLVMBPFDesc LLVMBPFInfo LLVMAVRDisassembler LLVMAVRCodeGen LLVMAVRAsmParser LLVMAVRDesc LLVMAVRInfo LLVMARMDisassembler LLVMARMCodeGen LLVMARMAsmParser LLVMARMDesc LLVMARMUtils LLVMARMInfo LLVMAMDGPUDisassembler LLVMAMDGPUCodeGen LLVMMIRParser LLVMipo LLVMInstrumentation LLVMVectorize LLVMLinker LLVMIRReader LLVMAsmParser LLVMFrontendOpenMP LLVMAMDGPUAsmParser LLVMAMDGPUDesc LLVMAMDGPUUtils LLVMAMDGPUInfo LLVMAArch64Disassembler LLVMMCDisassembler LLVMAArch64CodeGen LLVMCFGuard LLVMGlobalISel LLVMSelectionDAG LLVMAsmPrinter LLVMDebugInfoDWARF LLVMCodeGen LLVMTarget LLVMScalarOpts LLVMInstCombine LLVMAggressiveInstCombine LLVMTransformUtils LLVMBitWriter LLVMAnalysis LLVMProfileData LLVMObject LLVMTextAPI LLVMBitReader LLVMCore LLVMRemarks LLVMBitstreamReader LLVMAArch64AsmParser LLVMMCParser LLVMAArch64Desc LLVMMC LLVMDebugInfoCodeView LLVMDebugInfoMSF LLVMBinaryFormat LLVMAArch64Utils LLVMAArch64Info LLVMSupport LLVMDemangle)

27. # SET(LLVM_LIBS LLVMCore LLVMPasses LLVMFrontendOpenMP LLVMOption

28. #     LLVMProfileData LLVMcoverage LLVMLTO LLVMMCJIT LLVM-C )

29. # LLVMJITLink LLVMExecutionEngine LLVM-C *

30. # LLVMSupport LLVMJITLink

31. SET(CLANG_LIBS clangAST clangBasic clangLex clangCodeGen clangFrontend clangEdit

32. clangSerialization clangSema clangDriver clangParse clangAnalysis)

34. # IF(MSVC)

35. #   set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /NODEFAULTLIB:MSVCRT")

36. # ENDIF()

38. # llvm_map_components_to_libnames(LLVM_LIBS support core clang)

39. # message(STATUS "Using LLVM libs: ${LLVM_LIBS}")

41. LINK_DIRECTORIES(${LLVM_CLANG_DIR}/lib)

43. INCLUDE_DIRECTORIES (include)

45. ADD_DEFINITIONS(-D_CRT_SECURE_NO_WARNINGS)

46. ADD_DEFINITIONS(-DNOMINMAX)

48. FILE(GLOB_RECURSE PUBLIC_HEADERS "include/*.h")

50. FILE(GLOB_RECURSE SOURCE_FILES "src/*.cpp" )

52. ADD_SUBDIRECTORY(src)

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

1. SET(TARGET_NAME "nvLLVM")

3. ADD_DEFINITIONS(-DNVLLVM_LIB)

5. ADD_LIBRARY (${TARGET_NAME} SHARED ${PUBLIC_HEADERS} ${SOURCE_FILES})

7. TARGET_LINK_LIBRARIES(${TARGET_NAME} PRIVATE ${CLANG_LIBS} ${LLVM_LIBS} ${FLAVOR_LIBS})

9. INSTALL(TARGETS ${TARGET_NAME}

10. RUNTIME DESTINATION ${TARGET_DIR}

11. LIBRARY DESTINATION ${TARGET_DIR})

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 :

1. #include <iostream>

3. #define DEBUG_MSG(msg) std::cout << msg << std::endl;

5. #include <llvm_common.h>

7. int main(int argc, char *argv[])

8. {

9. DEBUG_MSG("Running clang compilation...");

10. runClang("W:/Projects/NervSeed/temp/test1.cxx");

11. DEBUG_MSG("Done running clang compilation.");

13. return 0;

14. }

Avec comme fichier CMake :

1. SET(TARGET_NAME "test_nvLLVM")

2. SET(TARGET_DIR "./")

4. ADD_DEFINITIONS(-D_CRT_SECURE_NO_WARNINGS)

6. FILE(GLOB_RECURSE SOURCE_FILES "*.cpp" )

8. INCLUDE_DIRECTORIES (${SRC_DIR}/nvLLVM/include)

10. ADD_EXECUTABLE (${TARGET_NAME} ${SOURCE_FILES})

11. TARGET_LINK_LIBRARIES(${TARGET_NAME} nvLLVM)

13. SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES PREFIX "")

15. COMPRESS_BINARY_TARGET()

17. INSTALL(TARGETS ${TARGET_NAME}

18. RUNTIME DESTINATION ${TARGET_DIR}

19. LIBRARY DESTINATION ${TARGET_DIR})

21. INSTALL_PDB()

J’ai obtenu les sorties suivantes :

1. //  (... lots of LLVM statistics here since they are enabled in my code above...)

2. ===-------------------------------------------------------------------------===

3. ... Statistics Collected ...

4. ===-------------------------------------------------------------------------===

6. 2 file-search - Number of directory cache misses.

7. 2 file-search - Number of directory lookups.

8. 1 file-search - Number of file cache misses.

9. 1 file-search - Number of file lookups.

11. [DEBUG]: Using target triple: x86_64-pc-windows-msvc

12. [DEBUG]: Retrieving nv_add/nv_sub functions...

13. [ERROR]: The meaning of life is: 42!

14. [ERROR]: The meaning of life is really: 42!

15. [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) :

1. int nv_add(int a, int b)

2. {

3. return a+b;

4. }

6. int nv_sub(int a, int b)

7. {

8. return a-b;

9. }

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.

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.