Compilation C++ JIT avec LLVM

Partie 5 – Bitcode, PCH, gestion des exceptions, liaison des modules et plus…

Emmanuel ROCHE

Traducteur

Thierry Jeanneret

L'objectif de ce tutoriel d'Emmanuel Roche est de vous apprendre à construire un compilateur C++ JIT avec LLVM.

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum. Commentez

- Titre : Compilation C++ JIT avec LLVM

- Auteur : Emmanuel ROCHE

- Traducteur : Thierry Jeanneret

- Parution : 22 février 2021

- 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

- Compilation C++ avec LLVM et clang - Partie 1 : générer du code C++ dynamiquement depuis Lua

Sommaire

- I. Introduction

- II. Frontal JIT pour Lua

- III. Sérialisation du bitcode pour un module LLVM

- IV. Écrire le bitcode dans un fichier

- V. Lire du bitcode à partir d’un fichier

- VI. Mécanisme de caching du bitcode

- VII. Gestion des entêtes précompilés (PCH – Precompiled Headers)

- VII-A. Génération des PCH

- VII-B. Utiliser les PCH

- VIII. Émulation de la désactivation du TLS

- IX. Tests unitaires scriptés C++ - Première tentative

- X. Construction et destruction de globaux

- XI. La gestion des exceptions C++ (sous Win64)

- XII. Test unitaires C++ scriptés – Seconde tentative

- XIII. Processus d’édition de liens des modules JIT

- XIV. Conclusion

- XV. Remerciements Developpez.com

I - Introduction

Voilà maintenant quelque temps que je vous présentais mon dernier article sur mes expériences avec le compilateur JIT. J’ai beaucoup joué avec ce code ces derniers jours. Si vous ne vous en souvenez pas, mon but premier était de pouvoir générer du code C++ directement depuis le langage de scripting Lua. C’est exactement ce que j’ai fait, et en passant j’ai construit un “frontal” pour mon compilateur en Lua, qui évolue encore et que j’utilise maintenant pour exécuter la plupart de mes tests. Au cours de cette démarche j’ai aussi travaillé sur la génération d’entêtes précompilés (PCH), les constructeurs et destructeurs de modules LLVM, les tests unitaires C++ depuis des scripts, la gestion d’exceptions, et les soucis de liaison des modules. Je pense donc qu’il est grand temps d’arrêter de coder un moment et d’essayer de partager ce que j’ai appris sur tous ces points dans l’hypothèse que ceci puisse intéresser quelqu’un (ou en réalité, juste pour que je me souvienne de ce que j’ai fait si je dois y revenir un jour.

II - Frontal JIT pour Lua

Comme mentionné plus haut, j’ai construit une classe JITCompiler dans mon environnement Lua, que je peux ensuite utiliser pour construire et utiliser un objet C++ JIT. Je ne pense pas qu’il vaille la peine d’entrer dans tous les détails de la configuration des interfaces Lua pour obtenir ce résultat. Disons juste que j’ai utilisé l’excellente bibliothèque sol3 pour générer ces interfaces et que j’ai simplement fourni les interfaces de ma classe NervJIT class et quelques fonctions init/uninit pour pouvoir complètement activer/désactiver l’environnement JIT LLVM directement depuis Lua.

Voici une rapide indication de ce à quoi ressemblent les interfaces actuellement :

1. #include <llvm_common.h>

2. #include <core_lua.h>

3. #include <NervJIT.h>

5. namespace nv

6. {

8. void loadLLVMBindings(sol::state& lua)

9. {

10. logTRACE2("Loading Lua bindings for LLVM module.");

12. auto space = lua["nv"].get_or_create<sol::table>();

14. space["initLLVM"] = &initLLVM;

15. space["uninitLLVM"] = &uninitLLVM;

17. SOL_BEGIN_ENUM(space, "LLVMHeaderType")

18. SOL_ENUM("SYSTEM", nv::NervJIT::HEADER_SYSTEM);

19. SOL_ENUM("ANGLED", nv::NervJIT::HEADER_ANGLED);

20. SOL_ENUM("QUOTED", nv::NervJIT::HEADER_QUOTED);

21. SOL_END_ENUM()

23. SOL_BEGIN_CLASS(space, "NervJIT", NervJIT)

24. SOL_CALL_CONSTRUCTORS(class_t());

25. SOL_CLASS_FUNC(loadModuleFromFiles);

26. SOL_CLASS_FUNC(loadModuleFromFile);

27. SOL_CLASS_FUNC(loadModuleFromBuffer);

28. SOL_CLASS_FUNC(generatePCHFromFile);

29. SOL_CLASS_FUNC(generatePCHFromBuffer);

30. SOL_CLASS_FUNC(generateBitcodeFromFile);

31. SOL_CLASS_FUNC(generateBitcodeFromBuffer);

32. SOL_CLASS_FUNC(loadModuleBitcode);

33. SOL_CLASS_FUNC(usePCHFile);

34. SOL_CLASS_FUNC(clearMacroDefinitions);

35. SOL_OV2_FUNCS(addMacroDefinition, void(std::string), void(const std::string&, const std::string&));

36. SOL_CLASS_FUNC(clearHeaderSearchPaths);

37. SOL_CLASS_FUNC(addHeaderSearchPath);

38. SOL_CLASS_FUNC(addCurrentProcess);

39. SOL_CLASS_FUNC(addDynamicLib);

40. SOL_CUSTOM_FUNC(linkModule) = [](class_t& obj, const std::string& outfile, sol::table t, bool onlyNeeded, bool internalize, bool optimize, bool preserveUseListOrder)

41. {

42. U32 count = t.size();

43. std::vector<std::string> inputs(count);

45. for (U32 i = 0; i < count; ++i)

46. {

47. inputs[i] = t[i + 1];

48. }

50. obj.linkModule(outfile, inputs, onlyNeeded, internalize, optimize, preserveUseListOrder);

52. };

53. SOL_CUSTOM_FUNC(setupCommandLine) = [](class_t& obj, sol::table t) {

54. U32 count = t.size();

55. std::vector<std::string> args(count);

57. for (U32 i = 0; i < count; ++i)

58. {

59. args[i] = t[i + 1];

60. }

61. obj.setupCommandLine(args);

62. };

64. SOL_CUSTOM_FUNC(call) = [](class_t& obj, const std::string& name) {

65. auto func = (void(*)())obj.lookup(name);

66. CHECK(func, "Cannot find function with name "<<name);

67. try {

68. func();

69. }

70. catch(const std::exception& e) {

71. logERROR("Exception catched from JIT code: "<<e.what());

72. }

73. catch(...) {

74. logERROR("Unknown exception catched from JIT code.");

75. }

76. };

77. SOL_END_CLASS()

79. logTRACE2("Done loading Lua bindings for LLVM module.");

80. }

82. }

84. NV_REGISTER_BINDINGS(LLVM)

N’essayez pas de compiler le code fourni ci-dessus : il contient une série de macros que j’ai définies moi-même par ailleurs, donc vous ne pourrez pas le compiler tel quel. Mais vous pouvez quand même vous faire une idée de ce dont nous disposerons ensuite dans Lua.

Beaucoup des méthodes NervJIT disponibles dans ce fichier d’interface sont nouvelles : nous n’en parlons dans aucun des articles précédents, mais ne vous en faites pas : nous les décrirons plus loin dans ce billet.

Le premier test que j’ai effectué en Lua a été d’essayer d’étendre les interfaces Lua directement depuis Lua lui-même ! J’ai donc écrit le script C++ suivant :

1. #include <core_lua.h>

2. #include <lua/LuaManager.h>

3. #include <NervApp.h>

5. using namespace nv;

7. extern "C" void loadLuaBaseExtensions()

8. {

9. auto& lman = LuaManager::instance();

10. auto& lua = lman.getMainState();

12. lua["nvFileExists"] = &fileExists;

13. logDEBUG("Done loading Lua extensions.");

14. };

Ensuite je charge ce script directement depuis Lua par quelque chose comme ceci :

1. local startTime = nv.SystemTime.getCurrentTime()

2. logDEBUG("JIT: Loading Lua extensions...")

3. self:loadBitcodeForFile(self.script_dir.."lua_base_extensions.cpp")

5. -- self.jit:generateBitcodeFromFile(self.script_dir.."lua_base_extensions.cpp", self.bc_dir.."lua_base_extensions.bc")

6. -- self.jit:loadModuleBitcode(self.bc_dir.."lua_base_extensions.bc")

8. -- self.jit:loadModuleFromFile(self.script_dir.."lua_base_extensions.cpp")

9. local endTime = nv.SystemTime.getCurrentTime()

10. logDEBUG(string.format("Script compiled in %.3fms", (endTime - startTime)*1000.0))

12. self.jit:call("loadLuaBaseExtensions")

- Et ça a marché sans problème sérieux ! Si vous y réfléchissez un peu c’est déjà une fonction plutôt élégante : dans le frontal Lua JITCompiler, nous avons aussi un “loadBitcodeForBuffer” par exemple, donc ceci signifie que vous pouvez envisager d’étendre Lua avec des extensions C++ concrètes sans même quitter le script Lua où vous voulez utiliser cette extension par un code tel que :

1. local jit = require "base.JITCompiler"

3. jit:loadBitcodeForBuffer[[

4. #include <core_lua.h>

5. #include <lua/LuaManager.h>

6. #include <NervApp.h>

8. using namespace nv;

10. extern "C" void loadLuaBaseExtensions()

11. {

12. auto& lman = LuaManager::instance();

13. auto& lua = lman.getMainState();

15. lua["nvFileExists"] = &fileExists;

16. logDEBUG("Done loading Lua extensions.");

17. };

18. ]]

20. jit:execute("loadLuaBaseExtensions")

22. if nvFileExists("C:/temp/dummy.txt") then

23. logDEBUG("Yes! my nvFileExists function is really available!")

24. else

25. logDEBUG("Never mind! My nvFileExists function is really available anyway! :-)")

26. end

III - Sérialisation du bitcode pour un module LLVM

Dans la section précédente nous avons vu que nous pouvons « charger » du « bitcode » soit depuis un fichier source C++ ou depuis du code C++ provenant d’un tampon mémoire ( c’est-à-dire une simple chaîne de caractères en L ua). Maintenant ce “bitcode” est quelque chose de nouveau que nous n’avons pas encore mentionné dans les articles précédents. Mais la raison est vraiment assez simple  :

- Initialement, dans ma classe NervJIT je fournissais simplement un fichier source C++ et générais un module LLVM en mémoire directement à partir de ce fichier. Mais avec mes divers tests de compilation, j’ai commencé à réaliser que compiler à partir de fichiers source prendrait toujours un temps significatif dans certains cas, et qu’il ferait sens d’essayer de cacher le module résultant de la compilation d’un script donné, de sorte que nous puissions simplement réutiliser le module correspondant dans le cas où le contenu de ce script n’a pas été modifié la prochaine fois que nous en avons besoin. C’est là que le bitcode entre en jeu LLVM peut lire/écrire ses modules de représentation interne (IR) en tant que fichiers bitcode (habituellement nous utiliserons l’extension .bc pour ceux-ci).

En conséquence, dans notre compilateur NervJIT, plutôt que de générer directement un objet Module à partir d’un fichier source donné, nous écrivons plutôt ce Module généré comme fichier bitcode (cette étape n’est exécutée que si elle est vraiment nécessaire), puis nous chargeons l’objet Module en lisant le contenu du fichier bitcode, avant d’injecter ce module dans notre instance LLJIT.

IV - Écrire le bitcode dans un fichier

La fonction principale pour générer le fichier bitcode est la suivante :

1. void NervJITImpl::generateBitcode(std::string outFile)

2. {

3. auto& compilerInvocation = compilerInstance->getInvocation();

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

5. std::string prevFile = std::move(frontEndOptions.OutputFile);

6. frontEndOptions.OutputFile = std::move(outFile);

8. // keep a copy of the current program action:

9. auto prevAction = frontEndOptions.ProgramAction;

10. frontEndOptions.ProgramAction = clang::frontend::EmitBC;

12. if (!compilerInstance->ExecuteAction(*emit_bc_action))

13. {

14. ERROR_MSG("Cannot execute emit_bc_action with compiler instance!");

15. }

17. // Restore the previous values:

18. frontEndOptions.OutputFile = std::move(prevFile);

19. frontEndOptions.ProgramAction = prevAction;

20. }

Notez qu’ici la fonction est appelée après que nous avons fourni un fichier ou un tampon d’entrée, par l’une des fonctions suivantes :

1. void NervJITImpl::setInputFile(const std::string& filename)

2. {

3. auto& compilerInvocation = compilerInstance->getInvocation();

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

5. frontEndOptions.Inputs.clear();

6. frontEndOptions.Inputs.push_back(clang::FrontendInputFile(llvm::StringRef(filename), clang::InputKind(clang::Language::CXX)));

7. }

9. void NervJITImpl::setInputBuffer(llvm::MemoryBuffer* buf)

10. {

11. auto& compilerInvocation = compilerInstance->getInvocation();

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

13. frontEndOptions.Inputs.clear();

14. frontEndOptions.Inputs.push_back(clang::FrontendInputFile(buf, clang::InputKind(clang::Language::CXX)));

15. }

Je pense qu’il vaut la peine d’expliquer un peu ce que nous faisons dans generateBitcode  : à la base, nous utilisons le frontal clang lui-même pour générer le fichier bitcode pour nous à partir du fichier source. En fait, je pense que je fournis ici une implémentation simulant un appel tel que  :

1. clang -emit-llvm -o foo.bc -c foo.c

Ainsi, nous conservons tout comme avant : la ligne de commande de configuration de l’invocation du compilateur, le chemin de recherche additionnel des entêtes, les définitions du préprocesseur, etc. Mais ensuite nous n’exécutons pas le EmitLLVMOnlyAction normal que nous avons utilisé jusqu’ici, à la place :

1. Nous surchargeons le champ ProgramAction dans les «frontendOptions» de l’invocation par clang::frontal::EmitBC (j’assume que la valeur par défaut que je lirais ici en fonction de ma configuration d’invocation par défaut devrait être clang::frontal::EmitLLVMOnly) ;

2. Nous faisons pointer l’option du frontal OutputFile vers l’endroit où nous voulons écrire le fichier .bc dans le processus ;

3. Finalement nous demandons l’exécution de l’action “emit_bc_action” sur l’instance de notre compilateur . Notez que cette action est créée avant tout dans le nouveau constructeur de notre NervJITImpl :

1. action = std::make_unique<clang::EmitLLVMOnlyAction>(tsContext->getContext());

2. emit_bc_action = std::make_unique<clang::EmitBCAction>();

Une fois l’action terminée avec succès, nous récupérons le Module résultant écrit dans OutputFile et nous nettoyons les options du frontal (au cas où), en restaurant les valeurs par défaut que nous utilisions auparavant.

Il y a beaucoup d’actions similaires “emit LLVM” disponibles dans clang, telles que EmitLLVM, EmitLLVMOnly, EmitBC, etc. De ce que je comprends, EmitLLVMOnly générera un objet Module, mais n’écrira rien dans le fichier de sortie, EmitBC génère le bitcode et l’écrit en sortie et EmitLLVM pourrait générer du code assembleur, je n’en suis pas sûr. De toute façon, si vous avez besoin d’investiguer à ce propos vous pouvez démarrer par le fichier source clang/lib/frontal/CompilerInstance.cpp comme référence.

Il existe d’autres manières d’écrire un Module LLVM dans un fichier bitcode, par exemple, la fonction auxiliaire de LLVM WriteBitcodeToFile  : nous y reviendrons plus loin dans cet article.

V - Lire du bitcode à partir d’un fichier

La seconde partie du contrat est ensuite d’être capable de lire le bitcode contenu dans un fichier donné pour en recréer l’objet LLVM. Ceci est fait dans la classe NervJIT en utilisant la fonction auxiliaire LLVM parseIRFile :

1. void NervJITImpl::loadModuleBitcode(const std::string& bcfile)

2. {

3. // Note: we could also use getLazyIRFileModule here?

5. llvm::SMDiagnostic Err;

6. std::unique_ptr<Module> module(llvm::parseIRFile(llvm::StringRef(bcfile), Err, *tsContext->getContext()));

7. if(!module) {

8. THROW_MSG("Cannot load IR module from file "<<bcfile);

9. }

11. loadModule(std::move(module));

12. }

Cette partie est réellement directe, notez juste que nous utilisons une fonction auxiliaire void loadModule(std::unique_ptr<Module> module); pour exécuter «l’injection» de notre Module IR nouvellement créé dans notre session d’exécution JIT, car il y a d’autres considérations importantes à prendre en compte ici. ⇒ Nous reviendrons sur ce point un peu plus tard.

VI - Mécanisme de caching du bitcode

Une fois le précédent mécanisme de sérialisation implémenté, il était assez facile de mettre en place une couche de caching simple et minimaliste par-dessus : lorsque nous générons le bitcode à partir d’un fichier source, seuls quelques éléments clés peuvent modifier le résultat de la compilation :

1. Les arguments de la ligne de commande que nous avons fournis pour configurer l’instance du compilateur ;

2. Les chemins de recherche additionnels que nous avons spécifiés ;

3. Les paramètres que nous avons fournis au préprocesseur ;

4. Le contenu réel du script source que nous voulons compiler.

⇒ Donc dans ma classe JITCompiler, j’utilise des méthodes spécifiques pour définir et en même temps mettre en cache les paramètres de compilation (arguments de la ligne de commande, entêtes, macros) d’après lesquels je génère un hash SHA256, et je mets à jour le hash à chaque fois que les paramètres sont modifiés en appelant une fonction Lua simple telle que :

1. -- Méthode utilisée pour générer une valeur de hachage à partir d’une liste de chaînes de caractères :

2. function Class:computeStringListHash(list)

3. return nv.sha256_from_buffer(table.concat(list, " "))

4. end

La fonction réelle sha256_from_buffer est implémentée en C++ en utilisant ces sources comme modèles : http://www.zedwood.com/article/cpp-sha256-function.

Ensuite, lorsque je dois compiler un fichier ou un tampon, je génère aussi un hachage pour le contenu du code source, puis j’obtiens un « hachage global » prenant aussi en compte les hachages de configuration évoqués auparavant :

1. -- Obtenir le hache d’un tampon donné dans le contexte de compilation courant :

2. function Class:getContextualBufferHash(buf)

3. local hash = nv.sha256_from_buffer(buf)

4. return nv.sha256_from_buffer(self.current_context_hash .. hash)

5. end

Puis je transforme simplement cette chaîne de hachage en nom de fichier bitcode en ajoutant l’extension “.bc”

Finalement, pour savoir si je peux utiliser le résultat en bitcode d’une compilation mise en cache je dois juste vérifier si le fichier existe, et sinon, je compile et écris le fichier dans le processus :

1. function Class:buildBitcodeForBuffer(buf, force)

2. -- First we should compute the hash of the input buffer:

3. local bufHash = self:getContextualBufferHash(buf)

5. local bcfile = self.bc_dir..bufHash..".bc"

6. if force or not nv.fileExists(bcfile) then

7. logDEBUG("Generating bytecode file ", bcfile, "...")

8. local startTime = nv.SystemTime.getCurrentTime()

9. self.jit:generateBitcodeFromBuffer(buf, bcfile)

10. local endTime = nv.SystemTime.getCurrentTime()

11. logDEBUG(string.format("Bitcode compiled in %.3fms", (endTime - startTime)*1000.0))

12. end

14. return bcfile

15. end

17. function Class:loadBitcodeForBuffer(buf, force)

18. local bcfile = self:buildBitcodeForBuffer(buf, force)

19. -- Finally we load the generated bytecode:

20. self.jit:loadModuleBitcode(bcfile)

21. end

Note importante : il existe malheureusement deux limitations importantes à ce mécanisme de mise en cache (mais je peux vivre avec elles pour le moment, donc ce système est suffisant pour mes besoins) :

1. Comme décrit plus haut, les « haches de contexte de compilation » ne prennent pas en compte les changements apportés aux entêtes inclus dans le script source. Donc si vous n’êtes pas prudent à ce sujet vous pouvez finir par utiliser un Module qui n’incorpore pas les derniers changements que vous aurez apportés à vos entêtes uniquement. (Mais évidemment il y a quelques options permettant de corriger ce problème au besoin).

2. Avec ce système vous produirez beaucoup de fichiers au format « nom_de_hachage.bc » tels que : “fffdc84aec07abae174c32795464209fb8e85bd2b6e3fea29521ce0da6bb8831.bc” dans un répertoire donné, et chaque fois que vous modifierez le contenu de votre script (ou à d’autres éléments du contexte de compilation) vous obtiendrez un hachage différent pour un code contenu quasiment ou totalement identique. Donc vous avez les deux faces de la pièce : d’un côté c’est bien, car cela signifie que si durant vos tests vous revenez à un hachage déjà généré, vous disposez déjà du fichier de cache et vous épargnez du temps de compilation. De l’autre côté, évidemment, vous obtenez des « fichiers cache orphelins » qui s’accumuleront avec le temps et qui nécessiteront un nettoyage de temps en temps. (Mais de nouveau, ça ne semble pas être une limitation trop sérieuse de mon point de vue, et je pense qu’il y a plusieurs « manières appropriées » de traiter ce point.)

VII - Gestion des entêtes précompilés (PCH – Precompiled Headers)

Je pense que le point important suivant à discuter ici est le support des entêtes précompilés par votre compilateur JIT. C’est important, car souvent nous pouvons construire de « gros modules » à partir de plusieurs fichiers sources C++ et utiliser les PCH peut sauver bien du temps de compilation dans ce cas.

Nous devons donc gérer deux parties pour ce faire : d’un côté nous devons fournir les moyens de générer un fichier PCH et de l’autre nous devons fournir un fichier PCH en « entrée » lorsque nous compilons ensuite les fichiers source.

VII-A - Génération des PCH

Voici la fonction principale que j’utilise pour générer le fichier PCH :

1. void NervJITImpl::generatePCH(std::string outFile)

2. {

3. auto& compilerInvocation = compilerInstance->getInvocation();

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

5. std::string prevFile = std::move(frontEndOptions.OutputFile);

6. frontEndOptions.OutputFile = std::move(outFile);

8. // keep a copy of the current program action:

9. auto prevAction = frontEndOptions.ProgramAction;

10. frontEndOptions.ProgramAction = clang::frontend::GeneratePCH;

12. if (!compilerInstance->ExecuteAction(*gen_pch_action))

13. {

14. ERROR_MSG("Cannot execute gen_pch_action with compiler instance!");

15. }

17. // Restore the previous values:

18. frontEndOptions.OutputFile = std::move(prevFile);

19. frontEndOptions.ProgramAction = prevAction;

20. }

Comme vous pouvez le voir, l’idée est très similaire à ce que nous avons déjà fait pour générer les fichiers bitcode des modules dans la section précédente : de nouveau, nous “conservons tout le reste tel quel” et lorsque nous faisons une requête de génération d’un fichier PCH nous changeons juste le ProgramAction et le OutputFile des options du frontal de l’invocation de notre compilateur. L’action que nous utilisons cette fois est la suivante (aussi construite dans le constructeur du NervJITImpl) :

1. gen_pch_action = std::make_unique<clang::GeneratePCHAction>();

Et fondamentalement, c’est tout ! Par convention vous spécifierez une extension « .pch » pour votre nom de fichier dans ce cas, et si l’action est exécutée sans erreur ce fichier est généré et votre prochaine étape sera de l’utiliser pour exécuter la véritable génération du module.

Note : cette implémentation fait, je pense, la même chose que ce que vous obtenez par la version ligne de commande :

1. clang -cc1 test.h -emit-pch -o test.h.pch

VII-B - Utiliser les PCH

Il est très simple de spécifier qu’un fichier PCH donné doit être utilisé pour les compilations suivantes, nous avons juste à mettre à jour les options appropriées du préprocesseur :

1. void NervJIT::usePCHFile(std::string pchfile)

2. {

3. auto& compilerInvocation = impl->compilerInstance->getInvocation();

4. auto& opts = compilerInvocation.getPreprocessorOpts();

5. opts.ImplicitPCHInclude = std::move(pchfile);

6. }

Ensuite vous pouvez générer votre module et le bitcode comme d’habitude, et le contenu du PCH sera utilisé comme attendu. Pas trop à ajouter à ce stade, d’accord ?

Du côté de Lua j’utilise aussi un mécanisme de caching des PCH strictement équivalent au mécanisme de caching du bitcode décrit plus tôt. Donc de mon côté je génère aussi un fichier pch dont le nom correspond au contenu du fichier qui a été utilisé pour générer le PCH incluant les haches du contexte de compilation.

VIII - Émulation de la désactivation du TLS

En poursuivant mes tests de script C++, j’ai été confronté à un autre problème sérieux lorsque j’ai essayé d’utiliser des variables statiques dans mes fonctions. Par exemple j’obtiens une erreur de compilation lorsque j’essaie de compiler cette fonction minimale de test :

1. #include <core_common.h>

3. extern "C" void test_func() {

4. static int t = (int)1000000*cos(34.5);

5. logDEBUG("My int value is: "<<t);

6. }

⇒ Ceci provoque une erreur de symbole manquant :

JIT session error: Symbols not found: [ __emutls_v._Init_thread_epoch, __emutls_get_address, _Init_thread_header, _Init_thread_footer, _Init_thread_abort ]

Les “_Init_thread_header”, “_Init_thread_footer”, “_Init_thread_abort” ont été aisés à trouver, conduisant à la partie vcruntime de l’environnement d’exécution C de Microsoft. Je les ai donc ajoutés dans mon module llvm_syms d’où je réexporte tous les symboles requis qui sont autrement manquants lorsque je lie mes modules JIT.

Pour mémoire, dans mes billets précédents, j’ai appelé ce module d’exportation llvm_helper, puis j’ai vraiment essayé d’exporter tous les symboles dont j’avais besoin directement depuis ma bibliothèque partagée nvLLVM. Mais à la fin j’ai dû revenir à l’utilisation d’un module auxiliaire dédié que j’ai nommé llvm_syms pour exporter ces symboles manquants, car sinon j’obtenais des symboles dupliqués lorsque j’essayais de lier la bibliothèque nvLLVM à mes bibliothèques de liaison Lua LLVM…

Mais ensuite je n’ai pas du tout pu trouver d’où __emutls_v._Init_thread_epoch et __emutls_get_address pouvaient bien venir. En plongeant plus profondément dans les sources de LLVM, j’ai finalement réalisé que ces fonctions étaient en fait utilisées pour une sorte de couche d’émulation TLS (TLS est l’acronyme de Thread Local Storage, Stockage local au thread) qui est utilisée sur certaines plateformes lorsque le TLS natif est indisponible.

⇒ Normalement vous pouvez activer/désactiver l’émulation TLS explicitement par les arguments de ligne de commande de clang -femulated-tls (ou -fno-emulated-tls), et ceci mettra à jour les entrées EmulatedTLS et ExplicitEmulatedTLS des codegen options de la configuration de l’invocation de votre compilateur. Mais ça n’a pas semblé marcher pour moi comme je l’attendais : dans mon compilateur NervJIT, il semble que j’utilisais toujours l’émulation TLS. Ça m’a pris pas mal de temps pour comprendre ce qui se passait là, mais j’ai finalement lu plus attentivement les commentaires de la fonction JITTargetMachineBuilder::detectHost() :

1. /// Create a JITTargetMachineBuilder for the host system.

2. ///

3. /// Note: TargetOptions is default-constructed, then EmulatedTLS and

4. /// ExplicitEmulatedTLS are set to true. If EmulatedTLS is not

5. /// required, these values should be reset before calling

6. /// createTargetMachine.

7. static Expected<JITTargetMachineBuilder> detectHost();

1. auto jtmb = CHECK_LLVM(JITTargetMachineBuilder::detectHost());

3. // Resetting emulateTLS to false:

4. auto& tgtOpts = jtmb.getOptions();

5. tgtOpts.EmulatedTLS = false;

6. tgtOpts.ExplicitEmulatedTLS = false;

8. targetMachine = CHECK_LLVM(jtmb.createTargetMachine());

9. DEBUG_MSG("Target machine using emulated TLS: "<<(targetMachine->useEmulatedTLS() ? "YES":"NO"));

11. auto dl = CHECK_LLVM(jtmb.getDefaultDataLayoutForTarget());

12. DEBUG_MSG("Default layout prefix for target: '" << dl.getGlobalPrefix() << "'");

14. DEBUG_MSG("Creating LLJIT object.");

16. LLJITBuilder llb;

17. llb.setJITTargetMachineBuilder(std::move(jtmb)).setNumCompileThreads(2);

19. lljit = CHECK_LLVM(llb.create());

21. DEBUG_MSG("Done creating LLJIT object.")

Et avec cette modification, les symboles “__emutls_XXX” disparurent immédiatement ; à leur place j’ai obtenu deux nouveaux symboles (évidemment liés au problème courant) : _tls_index et _Init_thread_epoch. Pourtant, il s'avère que ces deux symboles étaient disponibles dans l’environnement d’exécution C de Microsoft cette fois ! Donc de nouveau, exportation du nécessaire depuis mon module llvm_syms :

1. #pragma comment(linker, "/export:_Init_thread_header")

2. #pragma comment(linker, "/export:_Init_thread_footer")

3. #pragma comment(linker, "/export:_Init_thread_abort")

4. #pragma comment(linker, "/export:_tls_index")

5. #pragma comment(linker, "/export:_Init_thread_epoch")

Et ça a marché ! J’ai pu compiler et exécuter la fonction de test mentionnée sans aucun problème. Ouf ! Ce n’était pas simple…

IX - Tests unitaires scriptés C++ - Première tentative

À ce point, je me sentais suffisamment confiant pour envisager d’essayer quelque chose de « plus grand » : donc j’ai pensé que je devrais essayer des tests unitaires C++ directement avec ces scripts. Malheureusement, il semble a posteriori que c’était un peu prématuré. Explications

Jusqu’à présent, j’ai utilisé l’environnement de test Boost pour construire des tests unitaires normaux, mais j’ai décidé que je devais saisir cette occasion pour améliorer mon environnement de test avec quelque chose de nouveau. J’étais particulièrement intéressé par des solutions entêtes seules, donc, évidemment, j’ai vite trouvé le projet Catch/Catch2, qui semblait très prometteur et intéressant.

⇒ Donc bien sûr j’ai décidé de l’essayer… mais je n’ai pas eu beaucoup de chance sur cette voie. J’ai passé un temps significatif à essayer de comprendre ce qui ne marchait pas (en ajoutant des affichages de débogage Catch partout puis en essayant d’exécuter les scripts test) et j’ai bien progressé, mais en ce moment cette option ne fonctionne toujours pas pour moi.

En tout cas, en voyant que Catch ne me conduirait nulle part avant longtemps, j’ai pensé que je devrais aussi tester l’environnement Boost… mais ici non plus, je ne suis pas parvenu à le faire fonctionner pour le moment : j’ai essayé beaucoup de choses, mais avec celui-ci je reste coincé par des erreurs de segmentation… dommage.

Conclusion : le premier essai d’exécuter des scripts de test unitaire C++ ne s’est pas bien déroulé du tout. Mais tout de même, comme je l’ai dit, j’ai découvert quelques choses intéressantes en essayant de comprendre pourquoi l’environnement Catch ne fonctionnait pas comme prévu :

1. Premièrement j’ai constaté que mes variables globales/statiques n’étaient simplement pas construites dans mes scripts C++ ;

2. Et aussi, que les exceptions C++ n’étaient apparemment pas gérées correctement dans mon code JIT.

⇒ Les deux étaient des problèmes plutôt sérieux, donc je devais investiguer.

X - Construction et destruction de globaux

Comme je l’ai dit, les « globaux » ne fonctionnaient pas dans mon code JIT, donc j’ai préparé un test de référence minimal pour étudier ce problème :

1. local jit = import "base.JITCompiler"

3. jit:runFunction("test_func", [[

4. #include <iostream>

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

8. class MyTest {

9. public:

10. MyTest() {

11. std::cout << "Creating a MyTest object." << std::endl;

12. }

14. ~MyTest() {

15. std::cout << "Deleting a MyTest object." << std::endl;

16. }

18. void hello() {

19. std::cout << "Hello!" << std::endl;

20. }

21. };

23. static MyTest test;

25. extern "C" void test_func() {

26. DEBUG_MSG("Running test function.");

27. //test.hello();

28. }

29. ]])

En exécutant ce script Lua, je m’attendais d’abord à recevoir le message “Creating a MyTest object.” et ensuite à la fin du script (lorsque mon compilateur JIT est finalement chargé), le message “Deleting a MyTest object.”, mais bien sûr je n’ai d’abord rien obtenu.

Dans le script C++ ci-dessus, j’ai déclaré un objet static MyTest test; , mais en fait ce problème et sa solution sont exactement les mêmes si je n’utilise pas le spécificateur static.

Bon, retour chez mon meilleur ami (Google lol !) : à la recherche d’explications sur ce qui peut se passer et que faire… et finalement j’ai trouvé que ceci avait affaire avec l’initialisation et désinitialisation des modules LLVM. Plus précisément, lorsque vous chargez un module dans votre session JIT, vous êtes supposé exécuter les fonctions du constructeur disponibles dans ce module et vous devriez aussi exécuter les fonctions du destructeur lorsque ce module sort de la portée ou que vous détruisez la session JIT.

Maintenant, le problème est que ce processus d’init/uninit a évolué significativement dernièrement :

• initialement vous « collectiez » tous les constructeurs dans un llvm::orc::CtorDtorRunner, puis ajoutiez votre module au JIT, puis exécutiez ces constructeurs comme suit :

1. llvm::orc::CtorDtorRunner R(lljit->getMainJITDylib());

2. R.add(llvm::orc::getConstructors(*module));

4. DEBUG_MSG("Adding module from bytecode.");

5. auto err = lljit->addIRModule(ThreadSafeModule(std::move(module), *tsContext));

6. checkLLVMError(std::move(err));

8. checkLLVMError(R.run());

⇒ Pour plus de détails, vous pouvez jeter un œil à cette discussion : https://lists.llvm.org/pipermail/llvm-dev/2019-March/131057.html ;

- puis à un moment donné dans l’implémentation de la version 10 de LLVM les fonctions runConstructors()/runDestructors() ont été introduites dans la classe LLJIT. Donc vous devriez plutôt faire :

1. DEBUG_MSG("Adding module from bytecode.");

2. auto err = lljit->addIRModule(ThreadSafeModule(std::move(module), *tsContext));

3. checkLLVMError(std::move(err));

5. checkLLVMError(lljit->runConstructors());

Et maintenant dans la version 11 de LLVM (la version courante sur git), les fonctions runConstructors()/runDestructors() ont été supprimées et remplacées par les fonctions initialize(…)/uninitialize(…) :

1. DEBUG_MSG("Adding module from bytecode.");

2. auto err = lljit->addIRModule(ThreadSafeModule(std::move(module), *tsContext));

3. checkLLVMError(std::move(err));

5. checkLLVMError(lljit->initialize(lljit->getMainJITDylib()));

⇒ Pour plus de détails sur cette dernière implémentation vous pouvez commencer par cette page : https://groups.google.com/forum/#!msg/llvm-dev/DU5YYthVbrY/wXR1zZ7TAAAJ

J’ai testé les trois options décrites ci-dessus (car je suis passé temporairement à LLVM version 10.0.0 à un moment donné du processus) : toutes semblaient donner des résultats similaires, donc maintenant je reste à la dernière version officiellement disponible, utilisant les fonctions initialize()/uninitialize(). Et donc voici la fonction loadModule(std::unique_ptr<Module> module) que j’ai mentionnée plus tôt, que j’utilise pour m’assurer que les globaux du module sont initialisés de manière appropriée :

1. void NervJITImpl::loadModule(std::unique_ptr<Module> module)

2. {

3. // llvm::orc::CtorDtorRunner R(lljit->getMainJITDylib());

4. // R.add(llvm::orc::getConstructors(*module));

6. DEBUG_MSG("Adding module from bytecode.");

7. auto err = lljit->addIRModule(ThreadSafeModule(std::move(module), *tsContext));

8. checkLLVMError(std::move(err));

10. // Now we should try to run the static initializers if any:

11. DEBUG_MSG("Calling dyn lib initialize()");

12. checkLLVMError(lljit->initialize(lljit->getMainJITDylib()));

13. // checkLLVMError(lljit->runConstructors());

14. // checkLLVMError(R.run());

15. DEBUG_MSG("Done calling dyn lib initialize()");

16. }

Et bien sûr j’appelle aussi uninitialize() pour mon JITDylib principal dans le destructeur du NervJITImpl pour un nettoyage correct :

1. NervJITImpl::~NervJITImpl()

2. {

3. // We should uninitialize our main dyn library here:

4. DEBUG_MSG("Uninitializing main JIT lib.");

5. checkLLVMError(lljit->deinitialize(lljit->getMainJITDylib()));

6. DEBUG_MSG("Done uninitializing main JIT lib.");

7. }

Malheureusement ce n’était pas encore suffisant pour que mon script de test minimal fonctionne correctement : bien sûr je pouvais voir maintenant que mon constructeur global était appelé comme attendu, mais ensuite le destructeur correspondant n’était pas appelé lorsque je détruisais mon compilateur JIT, et à la place j’obtenais inévitablement une erreur de segmentation à la toute fin de l’exécution de mon programme. Bien, à ce stade il me semblait que le destructeur global était enregistré avec mon gestionnaire de “program level” atexit() ce qui est exactement le problème décrit par Lang Hames dans la seconde partie de cette page : et c’est là que le LocalCXXRuntimeOverrides est supposé intervenir.

Après quelques investigations, j’ai trouvé dans les sources de LLVM 10.0.0 que le fichier llvm/tools/lli/lli.cpp contient un exemple d’usage (censément) fonctionnel de cet utilitaire :

1. orc::MangleAndInterner Mangle(J->getExecutionSession(), J->getDataLayout());

3. orc::LocalCXXRuntimeOverrides CXXRuntimeOverrides;

4. ExitOnErr(CXXRuntimeOverrides.enable(J->getMainJITDylib(), Mangle));

6. // Then later on destruction, we call:

7. CXXRuntimeOverrides.runDestructors();

J’ai donc essayé d’utiliser ceci dans mon code, mais une fois encore ça n’a pas réellement marché pour moi :

1. D’abord, c’est uniquement compatible avec la version 10.0.0 de LLVM : dans la version 11.0.0, il semble que la classe LLJIT enregistre déjà des symboles absolus pour les noms que cet utilitaire essaie aussi d’enregistrer, et vous obtenez donc des problèmes de duplicate symbols :

1. Error LocalCXXRuntimeOverrides::enable(JITDylib &JD,

2. MangleAndInterner &Mangle) {

3. SymbolMap RuntimeInterposes;

4. RuntimeInterposes[Mangle("__dso_handle")] =

5. JITEvaluatedSymbol(toTargetAddress(&DSOHandleOverride),

6. JITSymbolFlags::Exported);

7. RuntimeInterposes[Mangle("__cxa_atexit")] =

8. JITEvaluatedSymbol(toTargetAddress(&CXAAtExitOverride),

9. JITSymbolFlags::Exported);

11. return JD.define(absoluteSymbols(std::move(RuntimeInterposes)));

12. }

2. Même en utilisant LLVM 10.0.0, j’obtenais encore mon erreur de segmentation lorsque j’utilisais cet utilitaire… Ça m’a pris assez longtemps pour comprendre ce qui se passait, mais j’ai finalement réalisé que c’était parce que d’une certaine manière, le code que je génère dans mon compilateur JIT n’enregistre pas le destructeur avec le gestionnaire “__cxa_atexit” , mais plutôt avec l’ancien gestionnaire “atexit”, obsolète. Malheureusement, je n’ai pas réellement d’idée du pourquoi pour le moment.

J’ai essayé différentes options de ligne de commande, “-fno-use-cxa-atexit”, “-fuse-cxa-atexit”, “-fregister-global-dtors-with-atexit”, mais aucune d’elles n’a aidé à utiliser le gestionnaire cxa_atexit. Mais souvenez-vous que je suis sur une plateforme Windows 10 et que j’utilise tous les flags de compatibilité de Visual Studio… c’est peut-être une partie de l’explication (ou peut-être que je dois essayer plus fort…).

Mais de toute façon cela m’a donné une idée : je pourrais simplement imiter ce que fait l’utilitaire LocalCXXRuntimeOverrides utility et ainsi fournir mon propre gestionnaire atexit. J’ai donc abouti à ce code :

1. typedef void(*ExitFunc)();

2. std::vector<ExitFunc> exitFuncList;

4. static int AtExitOverride(ExitFunc func)

5. {

6. DEBUG_MSG("Registering destructor in my AtExit override: "<<(const void*)func);

7. exitFuncList.push_back(func);

8. return 0;

9. }

11. static void runDestructors() {

12. // We execute the functions in LIFO order:

13. while(!exitFuncList.empty()) {

14. DEBUG_MSG("Executing one at exit function.")

15. exitFuncList.back()();

16. exitFuncList.pop_back();

17. DEBUG_MSG("Done executing one at exit function.")

18. }

19. }

21. NervJITImpl::NervJITImpl()

22. {

23. // Creating lljit object here.

25. {

26. auto& JD = lljit->getMainJITDylib();

27. SymbolMap RuntimeInterposes;

29. // We need to manually take care of the atexit function itself:

30. RuntimeInterposes[(*mangler)("atexit")] =

31. JITEvaluatedSymbol(toTargetAddress(&AtExitOverride),

32. JITSymbolFlags::Exported);

33. checkLLVMError(JD.define(absoluteSymbols(std::move(RuntimeInterposes))));

34. }

35. }

37. NervJITImpl::~NervJITImpl()

38. {

39. // We should uninitialize our main dyn library here:

40. DEBUG_MSG("Uninitializing main JIT lib.");

41. checkLLVMError(lljit->deinitialize(lljit->getMainJITDylib()));

42. runDestructors();

43. DEBUG_MSG("Done uninitializing main JIT lib.");

44. }

⇒ Et avec ces améliorations mon script de test a finalement fonctionné ! Il m’a donné ce type de sortie :

1. [Debug]               Bitcode compiled in 1221.376ms

2. [DEBUG]: Adding module from bytecode.

3. [DEBUG]: Calling dyn lib initialize()

4. Creating a MyTest object.

5. [DEBUG]: Registering destructor in my AtExit override: 000002B73EE00340

6. [DEBUG]: Done calling dyn lib initialize()

7. Running test function.

8. [DEBUG]: Deleting NervJIT object.

9. [DEBUG]: Uninitializing main JIT lib.

10. [DEBUG]: Executing one at exit function.

11. Deleting a MyTest object.

12. [DEBUG]: Done executing one at exit function.

13. [DEBUG]: Done uninitializing main JIT lib.

14. [DEBUG]: Deleted NervJIT object.

15. Deleted LogManager object.

Et ceci conclut cette section sur le support de la construction et de la destruction des globaux dans notre compilateur JIT. Il est temps de passer maintenant au second problème significatif que j’ai découvert en essayant d’exécuter mes tests unitaires C++ avec Catch depuis le code JIT : le support des exceptions C++.

XI - La gestion des exceptions C++ (sous Win64)

D’après ce que j’ai lu sur Internet (principalement depuis les archives des emails de llvm.org) la gestion des exceptions en C++ sous windows 64 ne semble pas réellement supportée à l’intérieur du code compilé JIT : c’est un sujet plutôt complexe, je ne prétends donc pas tout comprendre, mais j’ai construit le script de test minimal suivant pour ce thème :

1. local jit = import "base.JITCompiler"

3. jit:runFunction("test_func", [[

4. #include <iostream>

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

8. extern "C" void test_func() noexcept(false) {

9. DEBUG_MSG("Begin test.")

10. try {

11. DEBUG_MSG("I'm throwing an exception.");

12. throw std::exception("My exception message");

13. }

14. catch(const std::exception& e) {

15. DEBUG_MSG("Catched exception: "<<e.what());

16. }

17. catch(...) {

18. DEBUG_MSG("Catched exception.");

19. }

20. DEBUG_MSG("End test.")

22. throw std::exception("Throwing from extern C :-)!");

23. DEBUG_MSG("Real end test.")

24. }

25. ]])

Et bien sûr ceci n’a pas du tout fonctionné pour moi (le programme plante simplement après le message “I'm throwing an exception.”).

Pourtant nous devrions être capables de compiler ce code sans souci, mais gardez en mémoire que nous devons autoriser la gestion des exceptions en utilisant d’abord les arguments de ligne de commande “-fcxx-exceptions”, “-fexceptions” and “-fexternc-nounwind”.

Donc, de nouveau, j’ai passé un temps significatif en ligne à essayer de trouver une solution appropriée à ce problème (ou au strict minimum quelques indications sur la manière de le gérer) et finalement j’ai trouvé cette page de revue des commits : https://reviews.llvm.org/D35103.

En réalité je ne suis pas vraiment sûr de ce qui se passe avec cette liste de commits. C’est assez vieux (ça a été fermé en mars 2018), mais cette « propriété » ne semble plus être présente nulle part dans les sources de la version courante de LLVM, donc ?

⇒ De toute façon c’est le seul modèle valable que j’ai trouvé donc je devais vraiment essayer de l’utiliser. Fondamentalement, j’ai repris de ce code l’implémentation complète de la classe SingleSectionMemoryManager et l’ai conservé à peu près tel quel : le seul changement réel que j’ai apporté à ce niveau a été dans le constructeur de SingleSectionMemoryManager : là je n’enregistre pas de symbole pour la fonction “_CxxThrowException” (j’ai d’abord essayé, mais ça n’a pas semblé fonctionner : et de toute façon avec la classe LLJIT vous êtes supposé procéder différemment je le crains, donc j’y reviendrai juste après) :

1. #if defined(_WIN64)

2. // cf. https://reviews.llvm.org/D35103

3. #define NV_JIT_HANDLE_EXCEPTIONS 1

4. #endif

6. #if NV_JIT_HANDLE_EXCEPTIONS

7. class SingleSectionMemoryManager : public llvm::SectionMemoryManager {

8. struct Block {

9. uint8_t *Addr = nullptr, *End = nullptr;

10. void Reset(uint8_t *Ptr, uintptr_t Size);

11. uint8_t *Next(uintptr_t Size, unsigned Alignment);

12. };

13. Block Code, ROData, RWData;

15. public:

16. uint8_t *allocateCodeSection(uintptr_t Size, unsigned Align, unsigned ID,

17. llvm::StringRef Name) final;

19. uint8_t *allocateDataSection(uintptr_t Size, unsigned Align, unsigned ID,

20. llvm::StringRef Name, bool RO) final;

22. void reserveAllocationSpace(uintptr_t CodeSize, uint32_t CodeAlign,

23. uintptr_t ROSize, uint32_t ROAlign,

24. uintptr_t RWSize, uint32_t RWAlign) final;

26. bool needsToReserveAllocationSpace() override { return true; }

28. using llvm::SectionMemoryManager::EHFrameInfos;

30. SingleSectionMemoryManager();

32. void deregisterEHFrames() override;

34. bool finalizeMemory(std::string *ErrMsg) override;

36. private:

37. uintptr_t ImageBase = 0;

38. };

40. void SingleSectionMemoryManager::Block::Reset(uint8_t *Ptr, uintptr_t Size) {

41. assert(Ptr != nullptr && "Bad allocation");

42. Addr = Ptr;

43. End = Ptr ? Ptr + Size : nullptr;

44. }

46. uint8_t *SingleSectionMemoryManager::Block::Next(uintptr_t Size,

47. unsigned Alignment) {

48. uintptr_t Out = (uintptr_t)Addr;

50. // Align the out pointer properly

51. if (!Alignment)

52. Alignment = 16;

53. Out = (Out + Alignment - 1) & ~(uintptr_t)(Alignment - 1);

55. // RuntimeDyld should have called reserveAllocationSpace with an amount that

56. // will fit all required alignemnts...but assert on this to make sure.

57. assert((Out + Size) <= (uintptr_t)End && "Out of bounds");

59. // Set the next Addr to deliver at the end of this one.

60. Addr = (uint8_t *)(Out + Size);

61. return (uint8_t *)Out;

62. }

64. uint8_t *SingleSectionMemoryManager::allocateCodeSection(uintptr_t Size,

65. unsigned Align,

66. unsigned ID,

67. StringRef Name) {

68. return Code.Next(Size, Align);

69. }

71. uint8_t *SingleSectionMemoryManager::allocateDataSection(

72. uintptr_t Size, unsigned Align, unsigned ID, StringRef Name, bool RO) {

73. return RO ? ROData.Next(Size, Align) : RWData.Next(Size, Align);

74. }

76. void SingleSectionMemoryManager::reserveAllocationSpace(

77. uintptr_t CodeSize, uint32_t CodeAlign, uintptr_t ROSize, uint32_t ROAlign,

78. uintptr_t RWSize, uint32_t RWAlign) {

79. // FIXME: Ideally this should be one contiguous block, with Code, ROData,

80. // and RWData pointing to sub-blocks within, but setting the correct

81. // permissions for that wouldn't work unless we over-allocated to have each

82. // Block.Base aligned on a page boundary.

83. const unsigned SecID = 0;

84. Code.Reset(SectionMemoryManager::allocateCodeSection(CodeSize, CodeAlign,

85. SecID, "code"),

86. CodeSize);

88. ROData.Reset(SectionMemoryManager::allocateDataSection(ROSize, ROAlign, SecID,

89. "rodata", true/*RO*/),

90. ROSize);

92. RWData.Reset(SectionMemoryManager::allocateDataSection(RWSize, RWAlign, SecID,

93. "rwdata", false/*RO*/),

94. RWSize);

96. ImageBase =

97. (uintptr_t)std::min(std::min(Code.Addr, ROData.Addr), RWData.Addr);

98. }

100. // FIXME: Rather than this static and overriding _CxxThrowException via

101. // DynamicLibrary::AddSymbol, a better route would be to transform the call

102. // to _CxxThrowException(Arg0, Arg1) -> RaiseSEHException(Arg0, Arg1, this)

103. // where 'this' is the SingleSectionMemoryManager instance.  This could probably

104. // be done with clang, and definitely possible by injecting an llvm-IR function

105. // into the module with the name '_CxxThrowException'

106. //

107. static SEHFrameHandler sFrameHandler;

109. void SingleSectionMemoryManager::deregisterEHFrames() {

110. sFrameHandler.DeRegisterEHFrames(ImageBase, EHFrames);

111. EHFrameInfos().swap(EHFrames);

112. }

114. bool SingleSectionMemoryManager::finalizeMemory(std::string *ErrMsg) {

115. sFrameHandler.RegisterEHFrames(ImageBase, EHFrames);

116. ImageBase = 0;

117. return SectionMemoryManager::finalizeMemory(ErrMsg);

118. }

120. SingleSectionMemoryManager::SingleSectionMemoryManager() {

121. // Override Windows _CxxThrowException to call into our local version that

122. // can throw to and from the JIT.

123. //   that function should not be called here, instead, we register an absolute symbol in our JIT libs.

124. //   sys::DynamicLibrary::AddSymbol(

125. //       "_CxxThrowException",

126. //       (void *)(uintptr_t)&SEHFrameHandler::RaiseSEHException);

127. }

129. #endif

Je pense que la modification que j’ai faite dans le symbole “_CxxThrowException” est en fait ce qui est suggéré dans le commentaire “” ci-dessus.

Même chose pour la classe SEHFrameHandler : je pourrais juste la laisser telle quelle et elle compilerait très bien :

1. #ifdef NV_JIT_HANDLE_EXCEPTIONS

3. // Map an "ImageBase" to a range of adresses that can throw.

4. //

5. class SEHFrameHandler {

6. typedef SingleSectionMemoryManager::EHFrameInfos EHFrameInfos;

7. typedef std::vector<std::pair<DWORD, DWORD>> ImageRanges;

8. typedef std::map<uintptr_t, ImageRanges> ImageBaseMap;

9. ImageBaseMap m_Map;

11. static void MergeRanges(ImageRanges &Ranges);

12. uintptr_t FindEHFrame(uintptr_t Caller);

14. public:

15. static __declspec(noreturn) void __stdcall RaiseSEHException(void *, void *);

16. void RegisterEHFrames(uintptr_t ImageBase, const EHFrameInfos &Frames,

17. bool Block = true);

18. void DeRegisterEHFrames(uintptr_t ImageBase, const EHFrameInfos &Frames);

19. };

21. // Merge overlaping ranges for faster searching with throwing PC

22. void SEHFrameHandler::MergeRanges(ImageRanges &Ranges) {

23. std::sort(Ranges.begin(), Ranges.end());

25. ImageRanges Merged;

26. ImageRanges::iterator It = Ranges.begin();

27. auto Current = *(It)++;

28. while (It != Ranges.end()) {

29. if (Current.second + 1 < It->first) {

30. Merged.push_back(Current);

31. Current = *(It);

32. } else

33. Current.second = std::max(Current.second, It->second);

34. ++It;

35. }

36. Merged.emplace_back(Current);

37. Ranges.swap(Merged);

38. }

40. // Find the "ImageBase" for Caller/PC who is throwing an exception

41. uintptr_t SEHFrameHandler::FindEHFrame(uintptr_t Caller) {

42. for (auto &&Itr : m_Map) {

43. const uintptr_t ImgBase = Itr.first;

44. for (auto &&Rng : Itr.second) {

45. if (Caller >= (ImgBase + Rng.first) && Caller <= (ImgBase + Rng.second))

46. return ImgBase;

47. }

48. }

49. return 0;

50. }

52. // Register a range of adresses for a single section that

53. void SEHFrameHandler::RegisterEHFrames(uintptr_t ImageBase,

54. const EHFrameInfos &Frames, bool Block) {

55. if (Frames.empty())

56. return;

57. assert(m_Map.find(ImageBase) == m_Map.end());

59. ImageBaseMap::mapped_type &Ranges = m_Map[ImageBase];

60. ImageRanges::value_type *BlockRange = nullptr;

61. if (Block) {

62. // Merge all unwind adresses into a single contiguous block for faster

63. // searching later.

64. Ranges.emplace_back(std::numeric_limits<DWORD>::max(),

65. std::numeric_limits<DWORD>::min());

66. BlockRange = &Ranges.back();

67. }

69. for (auto &&Frame : Frames) {

70. assert(m_Map.find(DWORD64(Frame.Addr)) == m_Map.end() &&

71. "Runtime function should not be a key!");

73. PRUNTIME_FUNCTION RFunc = reinterpret_cast<PRUNTIME_FUNCTION>(Frame.Addr);

74. const size_t N = Frame.Size / sizeof(RUNTIME_FUNCTION);

75. if (BlockRange) {

76. for (PRUNTIME_FUNCTION It = RFunc, End = RFunc + N; It < End; ++It) {

77. BlockRange->first = std::min(BlockRange->first, It->BeginAddress);

78. BlockRange->second = std::max(BlockRange->second, It->EndAddress);

79. }

80. } else {

81. for (PRUNTIME_FUNCTION It = RFunc, End = RFunc + N; It < End; ++It)

82. Ranges.emplace_back(It->BeginAddress, It->EndAddress);

83. }

85. ::RtlAddFunctionTable(RFunc, N, ImageBase);

86. }

88. if (!Block)

89. MergeRanges(Ranges); // Initial sort and merge

90. }

92. void SEHFrameHandler::DeRegisterEHFrames(uintptr_t ImageBase,

93. const EHFrameInfos &Frames) {

94. if (Frames.empty())

95. return;

97. auto Itr = m_Map.find(ImageBase);

98. if (Itr != m_Map.end()) {

99. // Remove the ImageBase from lookup

100. m_Map.erase(Itr);

102. // Unregister all the PRUNTIME_FUNCTIONs

103. for (auto &&Frame : Frames)

104. ::RtlDeleteFunctionTable(reinterpret_cast<PRUNTIME_FUNCTION>(Frame.Addr));

105. }

106. }

108. // Adapted from VisualStudio/VC/crt/src/vcruntime/throw.cpp

109. #ifdef _WIN64

110. #define _EH_RELATIVE_OFFSETS 1

111. #endif

112. // The NT Exception # that we use

113. #define EH_EXCEPTION_NUMBER ('msc' | 0xE0000000)

114. // The magic # identifying this version

115. #define EH_MAGIC_NUMBER1 0x19930520

116. #define EH_PURE_MAGIC_NUMBER1 0x01994000

117. // Number of parameters in exception record

118. #define EH_EXCEPTION_PARAMETERS 4

120. // A generic exception record

121. struct EHExceptionRecord {

122. DWORD ExceptionCode;

123. DWORD ExceptionFlags;               // Flags determined by NT

124. _EXCEPTION_RECORD *ExceptionRecord; // Extra exception record (unused)

125. void *ExceptionAddress;             // Address at which exception occurred

126. DWORD NumberParameters; // No. of parameters = EH_EXCEPTION_PARAMETERS

127. struct EHParameters {

128. DWORD magicNumber;            // = EH_MAGIC_NUMBER1

129. void *pExceptionObject;       // Pointer to the actual object thrown

130. struct ThrowInfo *pThrowInfo; // Description of thrown object

131. #if _EH_RELATIVE_OFFSETS

132. DWORD64 pThrowImageBase; // Image base of thrown object

133. #endif

134. } params;

135. };

137. __declspec(noreturn) void __stdcall

138. SEHFrameHandler::RaiseSEHException(void *CxxExcept, void *Info) {

139. // DEBUG_MSG("Entering SEHFrameHandler::RaiseSEHException()!");

141. uintptr_t Caller;

142. static_assert(sizeof(Caller) == sizeof(PVOID), "Size mismatch");

144. USHORT Frames = CaptureStackBackTrace(1, 1, (PVOID *)&Caller, NULL);

145. assert(Frames && "No frames captured");

146. (void)Frames;

148. const DWORD64 BaseAddr = sFrameHandler.FindEHFrame(Caller);

149. if (BaseAddr == 0)

150. _CxxThrowException(CxxExcept, (_ThrowInfo *)Info);

152. // A generic exception record

153. EHExceptionRecord Exception = {

154. EH_EXCEPTION_NUMBER,      // Exception number

155. EXCEPTION_NONCONTINUABLE, // Exception flags (we don't do resume)

156. nullptr,                  // Additional record (none)

157. nullptr,                  // Address of exception (OS fills in)

158. EH_EXCEPTION_PARAMETERS,  // Number of parameters

159. {EH_MAGIC_NUMBER1, CxxExcept, (struct ThrowInfo *)Info,

160. #if _EH_RELATIVE_OFFSETS

161. BaseAddr

162. #endif

163. }};

165. // const ThrowInfo* pTI = (const ThrowInfo*)Info;

167. #ifdef THROW_ISWINRT

168. if (pTI && (THROW_ISWINRT((*pTI)))) {

169. // The pointer to the ExceptionInfo structure is stored sizeof(void*)

170. // infront of each WinRT Exception Info.

171. ULONG_PTR *EPtr = *reinterpret_cast<ULONG_PTR **>(CxxExcept);

172. EPtr--;

174. WINRTEXCEPTIONINFO **ppWei = reinterpret_cast<WINRTEXCEPTIONINFO **>(EPtr);

175. pTI = (*ppWei)->throwInfo;

176. (*ppWei)->PrepareThrow(ppWei);

177. }

178. #endif

180. // If the throw info indicates this throw is from a pure region,

181. // set the magic number to the Pure one, so only a pure-region

182. // catch will see it.

183. //

184. // Also use the Pure magic number on Win64 if we were unable to

185. // determine an image base, since that was the old way to determine

186. // a pure throw, before the TI_IsPure bit was added to the FuncInfo

187. // attributes field.

188. if (Info != nullptr) {

189. #ifdef THROW_ISPURE

190. if (THROW_ISPURE(*pTI))

191. Exception.params.magicNumber = EH_PURE_MAGIC_NUMBER1;

192. #if _EH_RELATIVE_OFFSETS

193. else

194. #endif // _EH_RELATIVE_OFFSETS

195. #endif // THROW_ISPURE

197. // Not quite sure what this is about, but pThrowImageBase can never be 0

198. // here, as that is used to mark when an "ImageBase" was not found.

199. #if 0 && _EH_RELATIVE_OFFSETS

200. if (Exception.params.pThrowImageBase == 0)

201. Exception.params.magicNumber = EH_PURE_MAGIC_NUMBER1;

202. #endif // _EH_RELATIVE_OFFSETS

203. }

205. // Hand it off to the OS:

206. #if defined(_M_X64) && defined(_NTSUBSET_)

207. RtlRaiseException((PEXCEPTION_RECORD)&Exception);

208. #else

209. RaiseException(Exception.ExceptionCode, Exception.ExceptionFlags,

210. Exception.NumberParameters, (PULONG_PTR)&Exception.params);

211. #endif

212. }

214. #endif

Ensuite nous devons apporter les changements requis pour rendre le code fourni compatible avec la classe LLJIT de LLVM 11.0.0 :

1. Premièrement nous devons injecter le symbole pour “_CxxThrowException” dans notre librairie dynamique JIT, donc j’utilise ce code modifié pour y parvenir

1. NervJITImpl::NervJITImpl()

2. {

3. // Creating lljit object here.

5. {

6. auto& JD = lljit->getMainJITDylib();

7. SymbolMap RuntimeInterposes;

9. // We need to manually take care of the atexit function itself:

10. RuntimeInterposes[(*mangler)("atexit")] =

11. JITEvaluatedSymbol(toTargetAddress(&AtExitOverride),

12. JITSymbolFlags::Exported);

13. #if NV_JIT_HANDLE_EXCEPTIONS

14. RuntimeInterposes[(*mangler)("_CxxThrowException")] =

15. JITEvaluatedSymbol(toTargetAddress(&SEHFrameHandler::RaiseSEHException),

16. JITSymbolFlags::Exported);

17. #endif

19. checkLLVMError(JD.define(absoluteSymbols(std::move(RuntimeInterposes))));

20. }

21. }

Et ensuite nous devons aussi dire explicitement à notre objet LLJIT d’utiliser ce nouveau “SingleSectionMemoryManager” que nous venons d’implémenter. Dans le code de référence de « l’interpréteur clang » c’était fait comme suit :

1. // (Warning: this code doesn't apply when building a LLJIT object)

2. static llvm::ExecutionEngine *

3. createExecutionEngine(std::unique_ptr<llvm::Module> M, std::string *ErrorStr) {

4. llvm::EngineBuilder EB(std::move(M));

5. EB.setErrorStr(ErrorStr);

6. EB.setMemoryManager(llvm::make_unique<SingleSectionMemoryManager>());

7. llvm::ExecutionEngine *EE = EB.create();

8. EE->finalizeObject();

9. return EE;

10. }

… Mais bien sûr cela ne s’applique pas pour nous, car nous n’avons aucun composant ExecutionEngine dans notre objet LLJIT autant que je le sache. Donc en cherchant dans les sources de LLJIT, j’ai finalement trouvé comment vous êtes supposé le faire dans les nouvelles implémentations : vous devez fournir un ObjectLinkingLayerCreator à votre LLJIT builder comme montré ci-dessous :

1. LLJITBuilder llb;

2. llb.setJITTargetMachineBuilder(std::move(jtmb)).setNumCompileThreads(2);

3. #if NV_JIT_HANDLE_EXCEPTIONS

4. // We use our custom memory manager here:

5. llb.setObjectLinkingLayerCreator([](ExecutionSession &ES, const Triple &triple) -> std::unique_ptr<ObjectLayer> {

6. auto GetMemMgr = []() { return std::make_unique<SingleSectionMemoryManager>(); };

7. auto ObjLinkingLayer = std::make_unique<RTDyldObjectLinkingLayer>(ES, std::move(GetMemMgr));

9. // Note sure this is needed/appropriate ?

10. if (triple.isOSBinFormatCOFF()) {

11. ObjLinkingLayer->setOverrideObjectFlagsWithResponsibilityFlags(true);

12. ObjLinkingLayer->setAutoClaimResponsibilityForObjectSymbols(true);

13. }

15. return std::unique_ptr<ObjectLayer>(std::move(ObjLinkingLayer));

16. });

17. #endif

19. lljit = CHECK_LLVM(llb.create());

À ce point, j’ai commencé à devenir nerveux : par expérience je dirais qu’ajouter autant de code lorsque vous ne comprenez pas pleinement ce qui se passe peut habituellement vous conduire sur une voie : plantées, scories, et plus de plantées… jusqu’à ce que vous compreniez finalement complètement ce que vous faites (et que vous commenciez à réaliser combien fou et stupide vous étiez auparavant…). Mais ça devait être mon jour de chance, car… cela a juste fonctionné !

Note : j’ai aussi mis à jour mes interfaces LLVM pour pouvoir intercepter les exceptions provenant de mon code JIT puisque cette implémentation était aussi supposée en fournir le support :

1. SOL_CUSTOM_FUNC(call) = [](class_t& obj, const std::string& name) {

2. auto func = (void(*)())obj.lookup(name);

3. CHECK(func, "Cannot find function with name "<<name);

4. try {

5. func();

6. }

7. catch(const std::exception& e) {

8. logERROR("Exception catched from JIT code: "<<e.what());

9. }

10. catch(...) {

11. logERROR("Unknown exception catched from JIT code.");

12. }

13. };

Puis en exécutant le script de test ci-dessus j’ai obtenu les résultats (corrects) suivants :

1. [DEBUG]: Adding module from bytecode.

2. [DEBUG]: Calling dyn lib initialize()

3. [DEBUG]: Done calling dyn lib initialize()

4. Begin test.

5. I'm throwing an exception.

6. Catched exception: My exception message

7. End test.

8. [Error]         Exception catched from JIT code: Throwing from extern C :-)!

9. [DEBUG]: Deleting NervJIT object.

10. [DEBUG]: Uninitializing main JIT lib.

11. [DEBUG]: Done uninitializing main JIT lib.

12. [DEBUG]: Deleted NervJIT object.

XII - Test unitaires C++ scriptés – Seconde tentative

Après ces bons résultats avec la construction/destruction des globaux et la gestion des exceptions C++, j’ai pensé que je devrais réessayer mon système de tests unitaires scriptés. Et en fait, après des recherches supplémentaires, j’ai trouvé un environnement de test à entêtes seuls nommé Lest (cf. https://github.com/martinmoene/lest), il n’est pas aussi complexe/évolué que Catch2, mais dans ma situation c’était plutôt une bonne chose ! Parce que j’ai pu le faire fonctionner sans trop de problèmes dans mon code JIT :

1. local jit = import "base.JITCompiler"

3. jit:runFunction("test_func", [[

4. #include <test/lest.hpp>

5. #include <iostream>

6. #include <core_common.h>

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

10. using namespace std;

12. const lest::test specification[] =

13. {

14. CASE( "Empty string has length zero (succeed)" )

15. {

16. EXPECT( 0 == string(  ).length() );

17. EXPECT( 0 == string("").length() );

19. EXPECT_NOT( 0 < string("").length() );

20. },

22. CASE( "Text compares lexically (fail)" )

23. {

24. EXPECT( string("hello") > string("world") );

25. },

27. CASE( "Unexpected exception is reported" )

28. {

29. EXPECT( (throw std::runtime_error("surprise!"), true) );

30. },

32. CASE( "Unspecified expected exception is captured" )

33. {

34. EXPECT_THROWS( throw std::runtime_error("surprise!") );

35. },

37. CASE( "Specified expected exception is captured" )

38. {

39. EXPECT_THROWS_AS( throw std::bad_alloc(), std::bad_alloc );

40. },

42. CASE( "Expected exception is reported missing" )

43. {

44. EXPECT_THROWS( true );

45. },

47. CASE( "Specific expected exception is reported missing" )

48. {

49. EXPECT_THROWS_AS( true, std::runtime_error );

50. },

51. };

53. extern "C" void test_func()

54. {

55. char* argv[] = { "my_dummy_app.exe" };

56. int argc = 1;

57. DEBUG_MSG("Running session.");

58. int res = lest::run( specification, argc, argv /*, std::cout */  );

59. if(res!=0) {

60. DEBUG_MSG("lest detected "<<res<<" failing tests.");

61. }

62. DEBUG_MSG("Done running session.");

63. };

65. ]])

67. logDEBUG("Done running tests.")

Le script ci-dessus a produit les résultats escomptés (avec les exceptions C++ activées évidemment) :

1. [DEBUG]: Adding module from bytecode.

2. [DEBUG]: Calling dyn lib initialize()

3. [DEBUG]: Registering destructor in my AtExit override: 000001C804B8B490

4. [DEBUG]: Done calling dyn lib initialize()

5. Running session.

6. (21): failed: Text compares lexically (fail): string("hello") > string("world") for "hello" > "world"

7. (26): failed: got unexpected exception with message "surprise!": Unexpected exception is reported: (throw std::runtime_error("surprise!"), true)

8. (41): failed: didn't get exception: Expected exception is reported missing: true

9. (46): failed: didn't get exception of type std::runtime_error: Specific expected exception is reported missing: true

10. 4 out of 7 selected tests failed.

11. lest detected 4 failing tests.

12. Done running session.

13. [Debug]               Done running tests.

14. [DEBUG]: Deleting NervJIT object.

15. [DEBUG]: Uninitializing main JIT lib.

16. [DEBUG]: Executing one at exit function.

17. [DEBUG]: Done executing one at exit function.

18. [DEBUG]: Done uninitializing main JIT lib.

19. [DEBUG]: Deleted NervJIT object.

XIII - Processus d’édition de liens des modules JIT

Maintenant, il y a une dernière chose dont j’aimerais parler dans cet article terriblement long : en essayant de construire des tests supplémentaires avec Lest, je voulais essayer le support d’« autoenregistrement du test » pour de multiples unités de traduction (cf. https://github.com/martinmoene/lest/blob/master/example/13-module-auto-reg-1.cpp). J’ai donc préparé trois scripts C++ séparés :

1. // nv_land_test.cpp file

2. #define lest_FEATURE_AUTO_REGISTER 1

3. #include <test/lest.hpp>

4. #include <iostream>

5. #include <core_common.h>

7. #define TEST_CASE( name ) lest_CASE( specification(), name )

9. using namespace std;

11. lest::tests & specification()

12. {

13. static lest::tests tests;

14. return tests;

15. }

17. TEST_CASE( "Empty string has length zero (succeed)" )

18. {

19. EXPECT( 0 == string(  ).length() );

20. EXPECT( 0 == string("").length() );

22. EXPECT_NOT( 0 < string("").length() );

23. }

24. extern "C" void nv_land_tests()

25. {

26. char* argv[] = { "my_dummy_app.exe" };

27. int argc = 1;

28. DEBUG_MSG("Running session.");

29. int res = lest::run( specification(), argc, argv /*, std::cout */  );

30. if(res!=0) {

31. DEBUG_MSG("lest detected "<<res<<" failing tests.");

32. }

33. DEBUG_MSG("Done running session.");

34. };

1. // nv_land_1_spec.cpp

2. #define lest_FEATURE_AUTO_REGISTER 1

3. #include <test/lest.hpp>

4. #include <iostream>

5. #include <core_common.h>

7. #define TEST_CASE( name ) lest_CASE( specification(), name )

9. extern lest::tests & specification();

11. TEST_CASE( "A passing test" "[pass]" )

12. {

13. EXPECT( 42 == 42 );

14. }

1. // nv_land_2_spec.cpp

2. #define lest_FEATURE_AUTO_REGISTER 1

3. #include <test/lest.hpp>

4. #include <iostream>

5. #include <core_common.h>

7. #define TEST_CASE( name ) lest_CASE( specification(), name )

9. extern lest::tests & specification();

11. TEST_CASE( "A failing test" "[fail]" )

12. {

13. EXPECT( 42 == 7 );

14. }

Puis j’aimerais essayer de charger ces trois fichiers en tant que « modules séparés » dans ma session JIT :

1. local jit = import "base.JITCompiler"

3. jit:usePCHFile("")

4. jit:loadScript("test/nvland/nv_land_tests")

5. jit:loadScript("test/nvland/nv_land_1_spec")

6. jit:loadScript("test/nvland/nv_land_2_spec")

8. jit:execute("nv_land_tests")

10. logDEBUG("Done running tests.")

Mais bien sûr ça ne fonctionne pas, car aussitôt que j’essaie de charger le second script LLVM lève une erreur de duplication de symbole (et c’est parfaitement logique) :

[ERROR]: LLVM error: Duplicate definition of symbol '??_7success@lest@@6B@'

Donc je devais réellement comprendre comment faire pour lier ensemble plusieurs modules et corriger proprement ce type de problèmes de duplication de symboles. Et j’ai trouvé l’outil llvm-link qui semble fournir exactement cette prestation : il va nous permettre de prendre en entrée une série de modules et de les fusionner en un seul module, résolvant ainsi le problème comme nous le souhaitons.

À partir de là j’ai construit les fonctions auxiliaires suivantes (en utilisant les sources de llvm-link comme référence) :

1. static void linkFiles(LLVMContext &Context, Linker &L,

2. const std::vector<std::string> &Files,

3. unsigned Flags, bool internalize) {

5. // Filter out flags that don't apply to the first file we load.

6. unsigned ApplicableFlags = Flags & Linker::Flags::OverrideFromSrc;

7. // Similar to some flags, internalization doesn't apply to the first file.

8. bool InternalizeLinkedSymbols = false;

10. for (const auto &File : Files) {

11. llvm::SMDiagnostic dErr;

12. std::unique_ptr<Module> M(llvm::parseIRFile(llvm::StringRef(File), dErr, Context));

14. if (!M.get()) {

15. THROW_MSG("Cannot load module file: "<< File);

16. }

18. if(verifyModule(*M, &errs())) {

19. THROW_MSG("Module file "<<File<<" is broken.");

20. }

22. bool Err = false;

23. if (InternalizeLinkedSymbols) {

24. Err = L.linkInModule(

25. std::move(M), ApplicableFlags, [](Module &M, const StringSet<> &GVS) {

26. internalizeModule(M, [&GVS](const GlobalValue &GV) {

27. return !GV.hasName() || (GVS.count(GV.getName()) == 0);

28. });

29. });

30. } else {

31. Err = L.linkInModule(std::move(M), ApplicableFlags);

32. }

34. if (Err) {

35. THROW_MSG("Error while linking in module.");

36. }

38. // Internalization applies to linking of subsequent files.

39. InternalizeLinkedSymbols = internalize;

41. // All linker flags apply to linking of subsequent files.

42. ApplicableFlags = Flags;

43. }

44. }

46. void NervJITImpl::linkModule(const std::string& outFile, const std::vector<std::string>& inputList, bool onlyNeeded, bool internalize, bool optimize, bool preserveUseListOrder)

47. {

48. auto Composite = std::make_unique<Module>("llvm-link", *tsContext->getContext());

49. llvm::Linker L(*Composite);

51. unsigned Flags = Linker::Flags::None;

52. if (onlyNeeded)

53. Flags |= Linker::Flags::LinkOnlyNeeded;

55. linkFiles(*tsContext->getContext(), L, inputList, Flags, internalize);

57. if(verifyModule(*Composite, &errs())) {

58. THROW_MSG("Composite module is broken.");

59. }

61. if(optimize) {

62. optimizeModule(Composite.get());

63. }

65. // cf. https://stackoverflow.com/questions/13903686/writing-module-to-bc-bitcode-file

66. std::error_code EC;

67. llvm::raw_fd_ostream OS(outFile, EC, llvm::sys::fs::F_None);

68. WriteBitcodeToFile(*Composite, OS, preserveUseListOrder);

69. OS.flush();

70. }

⇒ L’idée ci-dessus est d’utiliser l’objet llvm::Linker pour effectuer les étapes de liaison requises, puis d’écrire le module composite peuplé en un fichier bitcode avec la fonction WriteBitcodeToFile().

Puis j’implémente les fonctions frontales nécessaires en Lua pour utiliser les nouvelles caractéristiques de ce nouveau module :

1. function Class:linkModule(files, force)

2. -- For each file, we generate the bitcode:

3. local bcfiles = {}

4. for _,file in pairs(files) do

5. file = self:findFile(file)

6. table.insert(bcfiles, self:buildBitcodeForFile(file, force))

7. end

9. local modHash = self:computeStringListHash(bcfiles)

11. -- Now we should link the module and generate the corresponding hash:

12. local modfile = self.bc_dir..modHash..".bc"

14. if force or not nv.fileExists(modfile) then

15. logDEBUG("Linking module file ", modfile, "...")

16. local startTime = nv.SystemTime.getCurrentTime()

17. self.jit:linkModule(modfile, bcfiles, true, true, true, true)

18. local endTime = nv.SystemTime.getCurrentTime()

19. logDEBUG(string.format("Linked module  in %.3fms", (endTime - startTime)*1000.0))

20. end

22. return modfile

23. end

25. function Class:loadModule(files, force)

26. local modfile = self:linkModule(files, force)

27. self.jit:loadModuleBitcode(modfile)

28. end

Et finalement, je peux mettre mon script de test à jour pour m’assurer que je puisse lier tous mes scripts C++ ensemble pour construire un module fonctionnel avant de le charger dans une session JIT :

1. local jit = import "base.JITCompiler"

3. jit:usePCHFile("")

4. -- jit:loadScript("test/nvland/nv_land_tests")

5. -- jit:loadScript("test/nvland/nv_land_1_spec")

6. -- jit:loadScript("test/nvland/nv_land_2_spec")

7. jit:loadModule{

8. "test/nvland/nv_land_tests",

9. "test/nvland/nv_land_1_spec",

10. "test/nvland/nv_land_2_spec",

11. }

13. jit:execute("nv_land_tests")

15. logDEBUG("Done running tests.")

Et cette fois, cela devrait fonctionner très bien :

1. [DEBUG]: Adding module from bytecode.

2. [DEBUG]: Calling dyn lib initialize()

3. [DEBUG]: Done calling dyn lib initialize()

4. [Debug]               Running session.

5. (12): failed: A failing test[fail]: 42 == 7 for 42 == 7

6. 1 out of 3 selected tests failed.

7. [Debug]               lest detected 1 failing tests.

8. [Debug]               Done running session.

9. [Debug]               Done running tests.

XIV - Conclusion

Bien, tout d’abord, je suis absolument désolé d’avoir fait un si long article… J’aurais sûrement dû le partager en plus petites sections :-S Mais nous y sommes quand même. Si vous êtes parvenu jusqu’ici, félicitations ! Et j’espère que vous trouverez des informations intéressantes et des idées dans le code et les détails décrits ici.

Comme d’habitude, au cas où cela puisse vous être utile vous trouverez ici un package contenant les sources C++ les plus récents pour ce compilateur NervJIT :

nv_llvm_20200427.zip (lien alternatif sur DVP)

Et pour moi il est temps de prendre un café et de faire une pause avant de passer à quelques tests de plus !

Bon codage à chacun !

XV - Remerciements Developpez.com

Ce tutoriel est la traduction de JIT Compiler with LLVM - Part 5 - Bitcode, PCH, exceptions handling, module linking and more…. 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.