I. Introduction▲
Dans l'article précédent sur ce sujet, nous avons vu comment corriger les plantées du module d’optimisation de la représentation intermédiaire (IR) ou les fuites de mémoire dues à une mauvaise configuration de construction. Maintenant, dans ce billet, je veux évoquer les problèmes que j’ai rencontrés lors d’une compilation JIT légèrement plus avancée : l’idée de base était de vérifier que nous pouvons vraiment nous fier à des bibliothèques externes provenant de notre code JIT compilé, ce qui est évidemment une caractéristique clé. Et ceci s’est révélé loin d’être aussi facile que je le pensais…
II. Test initial avec dépendance à la fonction Process▲
Si vous lisez cette section sur la page qui traite de la conception et l’implémentation de l’ORC v2, vous comprendrez que nous devrions normalement être à même de trouver « les symboles du processus » à partir de notre code compilé JIT si nous utilisons le générateur DynamicLibrarySearchGenerator::
GetForCurrentProcess(…) pour une bibliothèque JIT donnée.
En fait, c’est ce que j’ai fait initialement par défaut pour la bibliothèque principale du compilateur NervJIT :
lljit->
getMainJITDylib().addGenerator(cantFail(DynamicLibrarySearchGenerator::
GetForCurrentProcess(lljit->
getDataLayout().getGlobalPrefix())));
J'ai donc mis à jour mon projet de test avec une nouvelle « fonction jouet » :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
int
my_function(int
a, int
b)
{
return
a*
b;
}
int
main(int
argc, char
*
argv[])
{
// The regular code here to init JIT.
jit->
loadModuleFromFile("W:/Projects/NervSeed/temp/test5.cxx"
);
auto
func =
(int
(*
)())jit->
lookup("test_function"
);
CHECK_RET(func!=
nullptr
,1
,"Invalid test_function pointer."
);
DEBUG_MSG("test_function() result: "
<<
func());
// cleaning code here.
}
Et j’ai utilisé le contenu suivant pour le script test5.cxx :
2.
3.
4.
5.
int
my_function(int
, int
);
extern
"C"
int
test_function() {
return
my_function(4
,5
);
}
Et bien sûr, ça n’a pas fonctionné (ça aurait été trop simple, n’est-ce pas ? ) et a produit le message d’erreur suivant :
2.
JIT session error: Symbols not
found: [ ?my_function@@YAHHH@Z ]
[ERROR]: LLVM error: Failed to materialize symbols: {
(main, {
test_function }
) }
Donc, pour une certaine raison, le compilateur JIT ne peut pas lier avec la fonction my_function définie dans mon processus… Ceci m’a conduit à la réflexion suivante : peut-être est-ce parce mon application de test est juste une application normale ? Par conséquent, lorsque je la compile, mon compilateur verra la définition de la fonction. Cependant, ensuite, comme je ne l’utilise réellement nulle part dans mon code, le compilateur trouvera probablement que c’est une bonne occasion d’optimiser le code et de supprimer complètement cette fonction !
En conséquence, ma première idée fut de dire explicitement au compilateur « je peux avoir besoin d’accéder à ce symbole de fonction extérieurement un jour, donc tu ne dois pas le supprimer ! » Et sous Windows/MSVC, vous le faites habituellement par __declspec(dllexport), j’ai changé mon code ainsi :
2.
3.
4.
__declspec(dllexport) int
my_function(int
a, int
b)
{
return
a*
b;
}
Et cette fois, ça a marché : il semble que le symbole de fonction était résolu correctement depuis le processus de mon application de test lui-même. J’ai obtenu le résultat attendu :
[DEBUG]: test_function() result: 20
Mais ceci m’a conduit à m’interroger ensuite : ai-je vraiment besoin « d’exporter » la fonction ? Ne serait-il pas suffisant de m’assurer que cette fonction est définie quelque part dans l’exécutable (en admettant que ceci puisse avoir du sens sans « l’exporter », je ne suis pas sûr de cela) ? J’ai donc construit le code de test suivant pour tenter de clarifier ce point :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
// __declspec(dllexport)
int
my_function(int
a, int
b)
{
return
a*
b;
}
int
main(int
argc, char
*
argv[])
{
// The regular code here to init JIT.
int
res =
my_function((int
)getenv("v1"
),(int
)getenv("v2"
));
DEBUG_MSG("The dummy value is: "
<<
res);
jit->
loadModuleFromFile("W:/Projects/NervSeed/temp/test5.cxx"
);
auto
func =
(int
(*
)())jit->
lookup("test_function"
);
CHECK_RET(func!=
nullptr
,1
,"Invalid test_function pointer."
);
DEBUG_MSG("test_function() result: "
<<
func());
// cleaning code here.
}
Avec cette construction, le JIT n’a pas pu trouver my_function et, puisque je ne pense pas que le compilateur serait capable d’optimiser mon appel de fonction dans ce cas, je dirais qu’il semble que la fonction doive réellement être exportée pour être trouvée (bon, OK, c’est logique).
Quoi qu'il en soit, il est temps de passer à la partie la plus intéressante avec les dépendances aux bibliothèques dynamiques supplémentaires…
III. Test initial avec dépendance au module partagé nvCore▲
De nouveau, l’idée était encore simple : j’essayais d’obtenir une dépendance très simple/minimale à ma bibliothèque nvCore dans mon code JIT, quelque chose comme ces lignes dans le fichier test6.cxx :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
#include
<core_common.h>
#include
<NervApp.h>
using
namespace
nv;
extern
"C"
int
showRootPath() {
auto
&
app =
NervApp::
instance();
String path =
app.getRootPath();
logDEBUG("The nervApp root path is: "
<<
path);
return
5
;
}
Malheureusement, cette partie a été significativement moins facile que la précédente.
D’abord, j’ai dû mettre à jour le chemin de recherche des fichiers inclus, bien sûr. Dès lors, dans mon application de test, j’ai utilisé :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
jit->
addMacroDefinition("_CRT_SECURE_NO_WARNINGS"
);
jit->
addHeaderSearchPath("W:/Projects/NervSeed/deps/msvc64/boost_1_68_0/include"
, nv::NervJIT::
HEADER_ANGLED);
jit->
addHeaderSearchPath("W:/Projects/NervSeed/sources/nvCore/include"
, nv::NervJIT::
HEADER_ANGLED);
jit->
loadModuleFromFile("W:/Projects/NervSeed/temp/test6.cxx"
);
DEBUG_MSG("Retrieving root path function."
);
auto
show =
(int
(*
)())jit->
lookup("showRootPath"
);
CHECK_RET(show, 1
, "cannot retrieve showRootPath function."
);
DEBUG_MSG("Displaying root path here:"
);
int
val =
show();
DEBUG_MSG("Result value is: "
<<
val);
Comme montré dans le code ci-dessus, j’ai aussi introduit la fonction de support que j’ai juste ajoutée dans ma classe NervJIT pour fournir les définitions des macros du préprocesseur.
Bien sûr, ça ne pouvait pas marcher sans fournir les symboles nvCore pour l’étape d’édition de liens. J’ai donc aussi fourni le support pour ajouter les dépendances aux bibliothèques dynamiques :
2.
3.
4.
5.
6.
void
NervJIT::
addDynamicLib(const
char
*
filename)
{
char
prefix =
impl->
lljit->
getDataLayout().getGlobalPrefix();
DEBUG_MSG("Loading dyn lib with prefix: '"
<<
prefix<<
"'"
);
impl->
lljit->
getMainJITDylib().addGenerator(cantFail(DynamicLibrarySearchGenerator::
Load(filename, prefix)));
}
Puis, avant de charger le module test6.cxx, j’ai aussi voulu faire :
jit->
addDynamicLib("W:/Projects/NervSeed/dist/bin/msvc64/nvCore.dll"
);
Mais… bien sûr ça n’a pas marché du tout, j’obtenais toujours des erreurs lorsque j’essayais de rechercher la fonction showRootPath, par exemple :
IT session error: Symbols not
found: [ terminate, ?_Facet_Register@std@@YAXPEAV_Facet_base@1
@@Z, _invalid_parameter_noinfo_noreturn, ??_7type_info@@6
B@, ??3
@YAXPEAX_K@Z ]
Pour commencer, je n’avais réellement aucune idée d'où cela pouvait bien provenir… J’ai recherché le symbole ??3@YAXPEAX_K@Z en ligne et trouvé que c’était le plus probablement une fonction operator delete(), mais ça n’a pas beaucoup aidé.
- J’ai essayé de supprimer l’appel DynamicLibrarySearchGenerator::GetForCurrentProcess, en pensant que peut-être il y avait une sorte de conflit avec les symboles du processus.
- J’ai essayé d’ajouter beaucoup de modules Windows manuellement (j’ai utilisé DependenciesGui pour trouver quelles étaient les dépendances dans mon module vCore) et j’ai terminé avec quelque chose comme ça :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
// jit->addDynamicLib("W:/Projects/NervSeed/dist/bin/msvc64/nvCore.dll");
// jit->addDynamicLib("W:/Projects/NervSeed/dist/bin/msvc64/nvLLVM.dll");
jit->
addDynamicLib("C:/WINDOWS/system32/kernel32.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/ntdll.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/kernelbase.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/advapi32.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/msvcrt.dll"
);
// jit->addDynamicLib("C:/WINDOWS/system32/sechost.dll");
// jit->addDynamicLib("C:/WINDOWS/system32/rpcrt4.dll");
// jit->addDynamicLib("C:/WINDOWS/system32/cryptbase.dll");
// jit->addDynamicLib("C:/WINDOWS/system32/userenv.dll");
// jit->addDynamicLib("C:/WINDOWS/system32/ucrtbase.dll");
// jit->addDynamicLib("C:/WINDOWS/system32/profapi.dll");
jit->
addDynamicLib("C:/WINDOWS/system32/user32.dll"
);
// jit->addDynamicLib("C:/WINDOWS/system32/mrmcorer.dll");
jit->
addDynamicLib("C:/WINDOWS/system32/gpapi.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/ntdsapi.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/vcruntime140.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/msvcp140.dll"
);
// jit->addDynamicLib("C:/WINDOWS/system32/ucrtbase.dll");
jit->
addDynamicLib("C:/WINDOWS/system32/msvcp140_1.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/msvcp140_2.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/psapi.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/shell32.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/ole32.dll"
);
// jit->addDynamicLib("C:/WINDOWS/system32/uuid.dll");
// jit->addDynamicLib("C:/WINDOWS/system32/delayimp.dll");
// jit->addDynamicLib("C:/WINDOWS/system32/gdi32.dll");
// jit->addDynamicLib("C:/WINDOWS/system32/winspool.dll");
// jit->addDynamicLib("C:/WINDOWS/system32/oleaut32.dll");
// jit->addDynamicLib("C:/WINDOWS/system32/comdlg32.dll");
À un certain point, j’ai noté que, si j’ajoutais la bibliothèque uuid.dll, j’obtenais plutôt une plantée silencieuse qu’un message d’erreur de recherche, mais fondamentalement c’était tout.
… Et il me manquait toujours au moins trois symboles (?_Facet_Register@std@@YAXPEAV_Facet_base@1@@Z, ??_7type_info@@6B@ et ??3@YAXPEAX_K@Z). Et je ne trouvais rien d’utile en ligne à ce sujet : c’était vraiment désespérant.
IV. Retour aux fondamentaux▲
Comme d’habitude, lorsque tout part « hors de contrôle », c’est une bonne idée d’essayer de prendre du recul, de changer de perspective et d’essayer quelque chose de plus simple… J’ai donc décidé d’essayer le code JIT suivant :
2.
3.
4.
5.
6.
#include
<iostream>
extern
"C"
int
showRootPath() {
std::
cout <<
"Hello from function!"
<<
std::
endl;
return
5
;
}
Pas de dépendance à nvCore ici, mais j’obtenais encore ce même symbole manquant avec ce script. J’ai alors fait un pas de plus, en construisant une application minimale de test avec ce contenu :
2.
3.
4.
5.
6.
#include
<iostream>
int
main() {
std::
cout <<
"Hello from function!"
<<
std::
endl;
return
0
;
}
En compilant ce code avec MSVC d’un côté et avec clang++ de l’autre, les deux produisent des fichiers exécutables similaires (mais pas exactement de la même taille) et les deux exécutables n’ont qu’une dépendance au module kernel32.dll.
Il y avait toutefois quelque chose de surprenant ici. Voici la ligne de commande que j’ai utilisée pour compiler avec clang++ :
clang++
-
Wall -
std=
c++
17
W:\Projects\NervSeed\tests\test_hello_world\main.cpp -
o W:\Projects\NervSeed\dist\bin\msvc64\test_hello_world_clang.exe
Ce qui m’a surpris ici a été… que ça a fonctionné directement au sortir de la boite. (Ouais, je sais… la plupart des gens trouveraient plutôt que c’est la partie sans surprise.) J’ai commencé à m’interroger : comment l’application produite par clang++ peut-elle fonctionner directement alors que, dans mon compilateur JIT, il me faut spécifier les arguments de ligne de commande du module, y compris le chemin de recherche et d’autres paramètres d’« invocation du compilateur» pour faire compiler ces quelques lignes simples ? clang doit tout « configurer » automatiquement sous le capot !
Et c’est là qu’intervient le si pratique argument de ligne de commande -v ! De nouveau cette belle découverte a sauvé ma journée (ou bien… « presque» sauvé ma journée) :
clang -
x c++
-
v -
Wall -
std=
c++
17
W:\Projects\NervSeed\tests\test_hello_world\main.cpp -
o W:\Projects\NervSeed\dist\bin\msvc64\test_hello_world_clang.exe
clang version 11.0.0
(ssh://git@gitlab.nervtech.org:22002/nerv/NervSeed.git 8512fe463218bc327ae31fb76b8eb2e0fc894c25)
Target
:
x86_64-
pc-
windows-
msvc
Thread model: posix
InstalledDir
:
W:\Projects\NervSeed\deps\msvc64\llvm-
20200409
\bin
"W:
\\
Projects
\\
NervSeed
\\
deps
\\
msvc64
\\
llvm-20200409
\\
bin
\\
clang.exe"
-
cc1 -
triple x86_64-
pc-
windows-
msvc19.16.27030
-
emit-
obj -
mrelax-
all -
mincremental-
linker-
compatible -
disable-
free -
disable-
llvm-
verifier -
discard-
value-
names -
main-
file-
name main.cpp -
mrelocation-
model pic -
pic-
level 2
-
mthread-
model posix -
mframe-
pointer=
none -
fmath-
errno -
fno-
rounding-
math -
mconstructor-
aliases -
munwind-
tables -
target-
cpu x86-
64
-
dwarf-
column-
info -
v -
resource-
dir "W:
\\
Projects
\\
NervSeed
\\
deps
\\
msvc64
\\
llvm-20200409
\\
lib
\\
clang
\\
11.0.0"
-
internal-
isystem "W:
\\
Projects
\\
NervSeed
\\
deps
\\
msvc64
\\
llvm-20200409
\\
lib
\\
clang
\\
11.0.0
\\
include"
-
internal-
isystem "D:
\\
Apps
\\
VisualStudio2017_CE
\\
VC
\\
Tools
\\
MSVC
\\
14.16.27023
\\
include"
-
internal-
isystem "D:
\\
Apps
\\
VisualStudio2017_CE
\\
VC
\\
Tools
\\
MSVC
\\
14.16.27023
\\
atlmfc
\\
include"
-
internal-
isystem "C:
\\
Program Files (x86)
\\
Windows Kits
\\
10
\\
Include
\\
10.0.18362.0
\\
ucrt"
-
internal-
isystem "C:
\\
Program Files (x86)
\\
Windows Kits
\\
10
\\
include
\\
10.0.18362.0
\\
shared"
-
internal-
isystem "C:
\\
Program Files (x86)
\\
Windows Kits
\\
10
\\
include
\\
10.0.18362.0
\\
um"
-
internal-
isystem "C:
\\
Program Files (x86)
\\
Windows Kits
\\
10
\\
include
\\
10.0.18362.0
\\
winrt"
-
Wall -
std=
c++
17
-
fdeprecated-
macro -
fdebug-
compilation-
dir "W:
\\
Projects
\\
NervSeed
\\
deps
\\
msvc64
\\
llvm-20200409
\\
bin"
-
ferror-
limit 19
-
fmessage-
length=
120
-
fno-
use-
cxa-
atexit -
fms-
extensions -
fms-
compatibility -
fms-
compatibility-
version=
19.16.27030
-
fdelayed-
template
-
parsing -
fcxx-
exceptions -
fexceptions -
fcolor-
diagnostics -
faddrsig -
o "C:
\\
Users
\\
ultim
\\
AppData
\\
Local
\\
Temp
\\
main-03e9a9.o"
-
x c++
"W:
\\
Projects
\\
NervSeed
\\
tests
\\
test_hello_world
\\
main.cpp"
clang -
cc1 version 11.0.0
based upon LLVM 11.0.0
git default
target x86_64-
pc-
windows-
msvc
#include
"..."
search starts here:
#include
<...>
search starts here:
W
:
\Projects\NervSeed\deps\msvc64\llvm-
20200409
\lib\clang\11.0.0
\include
D
:
\Apps\VisualStudio2017_CE\VC\Tools\MSVC\14.16.27023
\include
D
:
\Apps\VisualStudio2017_CE\VC\Tools\MSVC\14.16.27023
\atlmfc\include
C
:
\Program Files (x86)\Windows Kits\10
\Include\10.0.18362.0
\ucrt
C
:
\Program Files (x86)\Windows Kits\10
\include\10.0.18362.0
\shared
C
:
\Program Files (x86)\Windows Kits\10
\include\10.0.18362.0
\um
C
:
\Program Files (x86)\Windows Kits\10
\include\10.0.18362.0
\winrt
End of search list.
"D:
\\
Apps
\\
VisualStudio2017_CE
\\
VC
\\
Tools
\\
MSVC
\\
14.16.27023
\\
bin
\\
Hostx64
\\
x64
\\
link.exe"
"-out:W:
\\
Projects
\\
NervSeed
\\
dist
\\
bin
\\
msvc64
\\
test_hello_world_clang.exe"
-
defaultlib:libcmt "-libpath:D:
\\
Apps
\\
VisualStudio2017_CE
\\
VC
\\
Tools
\\
MSVC
\\
14.16.27023
\\
lib
\\
x64"
"-libpath:D:
\\
Apps
\\
VisualStudio2017_CE
\\
VC
\\
Tools
\\
MSVC
\\
14.16.27023
\\
atlmfc
\\
lib
\\
x64"
"-libpath:C:
\\
Program Files (x86)
\\
Windows Kits
\\
10
\\
Lib
\\
10.0.18362.0
\\
ucrt
\\
x64"
"-libpath:C:
\\
Program Files (x86)
\\
Windows Kits
\\
10
\\
Lib
\\
10.0.18362.0
\\
um
\\
x64"
-
nologo "C:
\\
Users
\\
ultim
\\
AppData
\\
Local
\\
Temp
\\
main-03e9a9.o"
Donc, lorsque vous demandez à clang des sorties verbeuses, eh bien, il vous fournit des sorties verbeuses. Et tant mieux. De là, vous pouvez voir tous les paramètres/configurations/chemins d’entêtes utilisés par défaut. Bien sûr, j’ai mis à jour la nouvelle application NervJIT/test en fonction, puis j’ai configuré mon invocation du compilateur par défaut comme suit :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
compilerInstance =
std::
make_unique<
clang::
CompilerInstance>
();
auto
&
compilerInvocation =
compilerInstance->
getInvocation();
std::
stringstream ss;
// ss << "-triple=" << llvm::sys::getDefaultTargetTriple();
ss <<
"-triple=x86_64-pc-windows-msvc19.16.27030"
;
DEBUG_MSG("Using triple value: "
<<
ss.str());
std::
vector<
const
char
*>
itemcstrs;
std::
vector<
std::
string>
itemstrs;
itemstrs.push_back(ss.str());
// cf. https://clang.llvm.org/docs/MSVCCompatibility.html
// cf. https://stackoverflow.com/questions/34531071/clang-cl-on-windows-8-1-compiling-error
itemstrs.push_back("-x"
);
itemstrs.push_back("c++"
);
itemstrs.push_back("-mrelax-all"
);
itemstrs.push_back("-mincremental-linker-compatible"
);
itemstrs.push_back("-disable-free"
);
itemstrs.push_back("-discard-value-names"
);
itemstrs.push_back("-mrelocation-model"
);
itemstrs.push_back("pic"
);
itemstrs.push_back("-pic-level"
);
itemstrs.push_back("2"
);
itemstrs.push_back("-mthread-model"
);
itemstrs.push_back("posix"
);
itemstrs.push_back("-mframe-pointer=none"
);
itemstrs.push_back("-fmath-errno"
);
itemstrs.push_back("-fno-rounding-math"
);
itemstrs.push_back("-mconstructor-aliases"
);
itemstrs.push_back("-munwind-tables"
);
itemstrs.push_back("-target-cpu"
);
itemstrs.push_back("x86-64"
);
itemstrs.push_back("-dwarf-column-info"
);
// -disable-llvm-verifier -main-file-name main.cpp
itemstrs.push_back("-Wall"
);
itemstrs.push_back("-std=c++17"
);
itemstrs.push_back("-fdeprecated-macro"
);
itemstrs.push_back("-ferror-limit"
);
itemstrs.push_back("19"
);
itemstrs.push_back("-fmessage-length=120"
);
itemstrs.push_back("-fno-use-cxa-atexit"
);
// -fdebug-compilation-dir "W:\\Projects\\NervSeed\\deps\\msvc64\\llvm-20200409\\bin"
itemstrs.push_back("-fms-extensions"
);
itemstrs.push_back("-fms-compatibility"
);
itemstrs.push_back("-fms-compatibility-version=19.16.27030"
);
itemstrs.push_back("-fdelayed-template-parsing"
);
itemstrs.push_back("-fcxx-exceptions"
);
itemstrs.push_back("-fexceptions"
);
itemstrs.push_back("-fcolor-diagnostics"
);
itemstrs.push_back("-faddrsig"
);
for
(unsigned
idx =
0
; idx <
itemstrs.size(); idx++
) {
// note: if itemstrs is modified after this, itemcstrs will be full
// of invalid pointers! Could make copies, but would have to clean up then...
itemcstrs.push_back(itemstrs[idx].c_str());
}
clang::CompilerInvocation::
CreateFromArgs(compilerInvocation, llvm::
ArrayRef<
const
char
*>
(itemcstrs.data(), itemcstrs.size()), *
diagnosticsEngine.get());
Pourtant… malheureusement, même avec tous ces changements, mon script de test tout simple std::cout ne fonctionnait toujours pas… toujours les mêmes symboles manquants ! :-S
Puis j’ai porté mon attention sur cette partie :
"D:
\\
Apps
\\
VisualStudio2017_CE
\\
VC
\\
Tools
\\
MSVC
\\
14.16.27023
\\
bin
\\
Hostx64
\\
x64
\\
link.exe"
"-out:W:
\\
Projects
\\
NervSeed
\\
dist
\\
bin
\\
msvc64
\\
test_hello_world_clang.exe"
-
defaultlib:libcmt "-libpath:D:
\\
Apps
\\
VisualStudio2017_CE
\\
VC
\\
Tools
\\
MSVC
\\
14.16.27023
\\
lib
\\
x64"
"-libpath:D:
\\
Apps
\\
VisualStudio2017_CE
\\
VC
\\
Tools
\\
MSVC
\\
14.16.27023
\\
atlmfc
\\
lib
\\
x64"
"-libpath:C:
\\
Program Files (x86)
\\
Windows Kits
\\
10
\\
Lib
\\
10.0.18362.0
\\
ucrt
\\
x64"
"-libpath:C:
\\
Program Files (x86)
\\
Windows Kits
\\
10
\\
Lib
\\
10.0.18362.0
\\
um
\\
x64"
-
nologo "C:
\\
Users
\\
ultim
\\
AppData
\\
Local
\\
Temp
\\
main-03e9a9.o"
Ceci signifie que, par défaut, lorsqu’il construit un petit exécutable, clang demandera à link.exe de MSVC de lier avec la bibliothèque libcmt, qui est la bibliothèque de l’environnement d’exécution statique C. Le problème est que nous n’avons pas cette étape d’édition de liens dans notre compilation JIT : nous ne pouvons que récupérer des symboles « externes » du processus lui-même ou depuis des bibliothèques dynamiques additionnelles (au moins, aussi loin que je le comprenne). Donc, on pourrait attendre que le JIT lierait plutôt avec des symboles de l’environnement d’exécution C dynamique et je pensais que c’était réellement ce que je faisais déjà puisque j’avais :
2.
3.
4.
5.
6.
jit->
addDynamicLib("C:/WINDOWS/system32/msvcrt.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/vcruntime140.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/msvcp140.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/ucrtbase.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/msvcp140_1.dll"
);
jit->
addDynamicLib("C:/WINDOWS/system32/msvcp140_2.dll"
);
Mais il semble que quelque part ça ne devait pas suffire… Puis j’ai trouvé cette page sur StackOverflow : https://stackoverflow.com/questions/41850296/link-dynamic-c-runtime-with-clang-windows.
J’ai trouvé qu’on pouvait le faire en ajoutant les options -Wl,-nodefaultlib:libcmt -D_DLL -lmsvcrt pour remplacer la bibliothèque par défaut. Cependant ça semble assez gênant. Y a-t-il une meilleure manière de lier l’environnement d’exécution que celle-ci ?
Voyez-vous où cela nous mène ? Oui, juste ! Tout ce qui me manquait réellement depuis le début était ce préprocesseur de macro _DLL ! Bien sûr je n’étais pas réellement convaincu, mais j’ai modifié mon code de test comme ceci :
2.
3.
4.
5.
jit->
addMacroDefinition("_DLL"
);
jit->
addMacroDefinition("_CRT_SECURE_NO_WARNINGS"
);
jit->
addHeaderSearchPath("W:/Projects/NervSeed/deps/msvc64/boost_1_68_0/include"
, nv::NervJIT::
HEADER_ANGLED);
jit->
addHeaderSearchPath("W:/Projects/NervSeed/sources/nvCore/include"
, nv::NervJIT::
HEADER_ANGLED);
jit->
loadModuleFromFile("W:/Projects/NervSeed/temp/test6.cxx"
);
Et tout à coup, plus de symboles manquants, et la sortie attendue !
2.
3.
4.
5.
6.
[DEBUG]: Retrieving root path function.
[DEBUG]: Searching symbol for
showRootPath
[DEBUG]: Returning address for
showRootPath
[DEBUG]: Displaying root path here:
Hello from function!
[DEBUG]: Result value is: 5
Victoire !😀
V. Dépendance à nvCore, itération 2▲
Après déjà tant de peine, bien sûr j’étais convaincu que tout devait être en ordre, je pourrais facilement construire et exécuter mon script d’usage minimal nvCore… (Oh là là… Je ne savais pas à quel point j’étais dans l’erreur…).
Je suis alors retourné à mon script de test nvCore, toujours en cours de développement :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
#include
<iostream>
#include
<core_common.h>
#include
<NervApp.h>
#include
<sstream>
// #include <vector>
using
namespace
nv;
// int my_function(int, int);
extern
"C"
int
showRootPath() {
std::
cout <<
"Hello from function!"
<<
std::
endl;
{
auto
&
app =
NervApp::
instance();
String path =
app.getRootPath();
std::
cout <<
"Root path is: "
<<
path<<
std::
endl;
std::
ostringstream os;
os <<
"(stringstream) Root path is: "
<<
path<<
std::
endl;
std::
cout <<
os.str();
// nv::LogRecord().GetStream(LogManager::DEBUG0, "file", 0, "");
// .GetStream(LogManager::DEBUG0, __FILE__, __LINE__, "") << "Root path: "<<path;
logDEBUG("The nervApp root path is: "
<<
path);
// logDEBUG("Hello world!");
}
int
*
val =
new
int
();
*
val =
6
;
std::
cout<<
"My int value is: "
<<
(*
val)<<
std::
endl;
delete
val;
NervApp::
destroy();
MemoryManager::
destroy();
return
5
;
// return my_function(4,5);
}
Et cela a, évidemment, échoué de nouveau… toujours des symboles manquants qui ressemblaient aux précédents (?_Facet_Register@std@@YAXPEAV_Facet_base@1@@Z, _invalid_parameter_noinfo_noreturn, ??_7type_info@@6B@, ??3@YAXPEAX_K@Z]). Là, je ne comprenais vraiment plus ce qui se passait. En bidouillant dans ce script, j’ai réalisé qu’il fonctionnerait et créerait/détruirait mon instance NervApp correctement si et seulement si je n’essaie pas d’afficher quoi que ce soit sur la sortie console ou en utilisant un objet std::ostringstream ou par ma macro spéciale logDEBUG() (qui utilise aussi un objet ostringstream sous le capot).
Revenons donc à un test plus simple :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
#include
<iostream>
#include
<sstream>
#include
<vector>
int
my_func()
{
try
{
// std::ostringstream os;
// os << "stringstream test!";
// // std::cout << os.str() << std::endl;
std::
vector<
int
>
vec;
vec.push_back(1
);
vec.push_back(2
);
vec.push_back(3
);
vec.push_back(0
);
return
vec.size();
}
catch
(...) {
std::
cout <<
"An exception occured!"
<<
std::
endl;
}
return
3
;
}
extern
"C"
int
test() {
// std::vector<int> vec;
// vec.push_back(3);
std::
cout <<
"Hello from test function!"
<<
std::
endl;
// os << "stringstream test!";
return
my_func();
// return 7;
}
Comme on peut le deviner à partir du contenu du script de test juste au-dessus, j’ai aussi rapidement réalisé que je ne peux pas non plus utiliser un std::vector et, je suppose, d’autres utilitaires de la STL…
À ce stade, j’ai essayé tant de paramètres de construction/chargement de bibliothèques dynamiques/préprocesseur de macros que j’en ai perdu le compte, je ne pourrais pas rapporter toutes les erreurs que j’ai commises ici de toute façon. Abrégeons et voyons directement quels étaient, je pense, les points importants conduisant à une solution appropriée. (Alerte de divulgation : oui, vraiment, j’ai finalement trouvé une solution correcte… ou au moins, c’est ce que je crois pour le moment.)
V-A. Macros d’édition de liens correcte avec l’environnement d’exécution C▲
- Comme discuté précédemment, la définition du préprocesseur pour _DLL est requise pour que votre code lie avec l’environnement d’exécution C dynamique plutôt que statique : nous voulons donc vraiment garder cela lorsque nous compilons du code avec clang en module IR (car jusqu’ici je n’ai pas trouvé le moyen de demander à notre moteur LLJIT d’éditer les liens avec des bibliothèques statiques lorsque je charge un module). Je suis d’accord que ça sonne comme quelque chose de possible à faire… à étudier un jour peut-être.
- À un certain point, je pense aussi que la définition du préprocesseur pour _MT était aussi requise (peut-être pour demander au compilateur de lier avec « la version multithreadée de l’environnement d’exécution » de Microsoft), mais à la fin ça ne semble pas faire la moindre différence : j’ai pu supprimer la définition de cette macro et obtenir que mes « scripts C++ » (oh, que j’aime comme ça résonne ) compilent et s’exécutent correctement, je ne l’utilise donc pas pour le moment.
V-B. Architecture correcte de l’environnement d’exécution C dynamique▲
Un autre point sérieux que je ne suis pas sûr de comprendre complètement ici est sur la version de l’environnement d’exécution que vous utilisez : si vous avez construit une DLL x64 simple qui se lie à l’environnement d’exécution C dynamique, par exemple (sur un système Windows 10 x64 bien sûr), si vous vérifiez ensuite la dépendance de cette DLL par DependenciesGui ou par Dependency Walker, ils vous diront tous les deux que vous avez une dépendance à (si vous affichez le chemin complet ) :
- C:\Windows\system32\vcruntime140.dll
- C:\Windows\system32\ucrtbase.dll
- [et beaucoup d’autres dépendances indirectes, avec à un moment donné, je suppose un lien optionnel à C:\Windows\system32\msvcp140.dll]
Encore que… autant que je le comprenne, il s’agit des versions 32 bits de ces DLL ! J’ai essayé de comparer avec les DLL de l’environnement d’exécution trouvées dans mon dossier D:\Apps\VisualStudio2017_CE\VC\Redist\MSVC\14.16.27012\x64\Microsoft.VC141.CRT et les tailles des fichiers diffèrent.
Ceci expliquerait pourquoi je n’ai rien pu faire marcher avec mon module JIT aussi longtemps que j’ai utilisé ces bibliothèques :
2.
3.
4.
5.
jit->
addDynamicLib("W:/Windows/system32/msvcp140.dll"
);
jit->
addDynamicLib("W:/Windows/system32/msvcp140_2.dll"
);
jit->
addDynamicLib("W:/Windows/system32/concrt140.dll"
);
jit->
addDynamicLib("W:/Windows/system32/vcruntime140.dll"
);
jit->
addDynamicLib("W:/Windows/system32/ucrtbase.dll"
);
J’ai donc mis une copie des DLL de l’environnement d’exécution x64 dont j’avais besoin dans un dossier dédié de mon projet et j’ai commencé à utiliser ceci :
2.
3.
4.
5.
jit->
addDynamicLib("W:/Projects/NervSeed/dist/bin/msvc64/CRT/msvcp140.dll"
);
jit->
addDynamicLib("W:/Projects/NervSeed/dist/bin/msvc64/CRT/msvcp140_2.dll"
);
jit->
addDynamicLib("W:/Projects/NervSeed/dist/bin/msvc64/CRT/concrt140.dll"
);
jit->
addDynamicLib("W:/Projects/NervSeed/dist/bin/msvc64/CRT/vcruntime140.dll"
);
jit->
addDynamicLib("W:/Projects/NervSeed/dist/bin/msvc64/CRT/ucrtbase.dll"
);
J’ai trouvé toutes les DLL précédentes dans le dossier D:\Apps\VisualStudio2017_CE\VC\Redist\MSVC\14.16.27012\x64\Microsoft.VC141.CRT, excepté ucrtbase.dll, que j’ai trouvée dans C:\Windows\WinSxS\amd64_microsoft-windows-ucrt_31bf3856ad364e35_10.0.18362.387_none_016ff738ab3856ff
Cette étape a été nécessaire pour obtenir quelque chose de fonctionnel à la fin, donc vous devez être très prudent quant à la version exacte de ces DLL que vous chargez dans votre session JIT.
V-C. Ajouter manuellement quelques symboles manquants▲
Dans un monde parfait, j’espérerais que, si je fournis les bibliothèque s de l’environnement d’exécution C correctes tel que décrit ci-dessus pour ma session JIT, alors l’éditeur de liens JIT devrait pouvoir trouver tous les symboles nécessaires : tout devrait donc être prêt.
Pourtant, pour une raison que je ne comprends pas pour le moment, il semble que ce n’était pas suffisant (dans mon cas au moins) : selon que j’essaie de créer un std::vector ou un std::ostringstream ou que j’essaie d’appeler une autre méthode de la STL dans mon script, la méthode de recherche se plaint toujours de l’absence de certains symboles tels que : ??3@YAXPEAX_K@Z, ??2@YAPEAX_K@Z, ??3@YAXPEAX@Z, ??_7type_info@@6B@ .
Autant que je le sache, les symboles listés ci-dessus sont tous en relation avec l’op é rat eu r delete global et pour une part à la classe std::type_info … il y a peut-être encore quelque chose à creuser par là, mais je pense que j’ai déjà passé assez de temps sur ce point pour le moment.
• Heureusement, j’ai aussi trouvé cet article : https://stackoverflow.com/questions/54403377/problems-enabling-rtti-in-llvm-jit-ed-code : « Pour corriger l’erreur de recherche de LLVM, vous pouvez exporter ce symbole depuis votre propre DLL ou exécutable, vous pouvez aussi le faire depuis votre code C++ ainsi : »
#pragma comment(linker,
"/export:??_7type_info@@6B@"
)
Au début j’essayais d’exporter ces symboles directement depuis mon application test_nvLLVM et, ensuite, de charger aussi ces symboles depuis le processus courant de ma session JIT, mais, au moment où je testais ceci, je mélangeais aussi les environnements d’exécution C statique et dynamique et j’ai été trompé par la grande similarité des symboles (??3@YAXPEAX_K@Z ⇔ ??2@YAPEAX_K@Z ⇔ ??3@YAXPEAX@Z). Au début, je n’ai pas réussi à faire marcher ceci ; plus tard, j’ai choisi de créer un module auxiliaire dédié juste pour être sûr que je ne mélangerais pas tout à nouveau. En y réfléchissant, je pense maintenant que ça devrait aussi bien marcher si, par exemple, j’exporte les symboles manquants directement depuis mon module nvLLVM et que j’ajoute une dépendance sur lui dans ma session JIT : à tester/investiguer plus tard.
-
J’ai donc finalement construit un module partagé auxiliaire minimal que j’ai nommé llvm_helper, qui ne contient rien de nouveau :
- Ce module fait le lien avec l’environnement d’exécution C dynamique ;
- Il réexporte explicitement les symboles que LLVM ne pouvait pas trouver directement dans les bibliothèques liées ci-dessus .
Le code source de cet utilitaire est donc très simple :
2.
3.
4.
5.
6.
7.
8.
9.
// we just export the symbols we need from here:
#include
<vector>
#include
<sstream>
#pragma comment(linker,
"/export:??3@YAXPEAX_K@Z"
)
#pragma comment(linker,
"/export:??2@YAPEAX_K@Z"
)
#pragma comment(linker,
"/export:??3@YAXPEAX@Z"
)
#pragma comment(linker,
"/export:??_7type_info@@6B@"
)
Et le CMakeLists.txt de ce sous-projet est simplement :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
SET(TARGET_NAME "llvm_helper"
)
SET(TARGET_DIR "."
)
SET(CMAKE_CXX_FLAGS "/EHsc /MD -D_MT -D_DLL /std:c++17"
)
FILE(GLOB_RECURSE SOURCE_FILES "*.cpp"
)
ADD_LIBRARY (${
TARGET_NAME}
SHARED ${
SOURCE_FILES}
)
# TARGET_LINK_LIBRARIES(${TARGET_NAME})
INSTALL(TARGETS ${
TARGET_NAME}
RUNTIME DESTINATION ${
TARGET_DIR}
LIBRARY DESTINATION ${
TARGET_DIR}
)
Puis je charge aussi ce module auxiliaire dans ma session JIT :
jit->
addDynamicLib("W:/Projects/NervSeed/dist/bin/msvc64/llvm_helper.dll"
);
Finalement, avec tous ces petits changements mis ensemble, j’ai pu compiler et exécuter mon script d’intégration nvCorecorrectement ! (Ouf ! Encore une bonne chose de faite !)
Voici à quoi ressemble mon fichier source test_nvLLVM actuellement comme résultat de ces investigations :
Ce n’est pas vraiment une jolie référence bien propre que vous souhaiteriez utiliser en production, mais bon, finalement, ça fonctionne. Je vais donc nettoyer un peu, mais avant tout je vais m’assurer de mettre tout ça tel quel sur Git.
VI. Prochaines étapes▲
Avec un peu de chance je devrais pouvoir finalement passer aux liens Lua et à du « scriptage C++ » plus intéressant, ceci devrait donc être le dernier article sur cette expérience d’implémentation d’un compilateur LLVM JIT à bas niveau… pour le moment ! Car il reste bien sûr toujours beaucoup à étudier sur ce sujet (je pense par exemple au système de cache de modules de LLVM), il y a donc de fortes chances que j’y revienne à un moment donné !
Pendant ce temps , bon hacking à tout le monde😀 !
VII. Remerciements Developpez.com▲
Ce tutoriel est la traduction de JIT Compiler with LLVM - Part 4 - CRT dependency. 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.