Dans un projet en C++, quand on souhaite organiser le code dans différentes bibliothèques, on a le choix entre utiliser des bibliothèques statiques et dynamiques. Les bibliothèques statiques sont facilement intégrables dans un projet toutefois si on souhaite la mettre à jour, il faut recompiler tout le projet qui le consomme. Les bibliothèques dynamiques sont, quant à elles, moins facilement intégrables mais pour les mettre à jour, il suffit de les remplacer sans forcément avoir à recompiler le projet qui les utilise. C’est cet avantage qui rends en partie les bibliothèques dynamiques intéressantes en terme de déploiement.
Avec la technologie .NET, quand on souhaite intégrer une assembly managée à un projet, il suffit de l’ajouter en tant que référence de projet. On peut ainsi, d’une part, utiliser des objets de cette assembly et compiler son projet. D’autre part, en rendant disponible l’assembly au CLR, on peut de nouveau l’utiliser à l’exécution.
Les DLL natives sont moins facilement flexibles et nécessitent quelques aménagements de façon à être utilisées avec la même flexibilité que les assemblies .NET ou presque.
Le but de cet article est de montrer quelques techniques d’implémentation pour rendre les DLL natives intégrables dans un projet C++.
Dans un 1er temps, on définira quelques termes. Avant de référencer directement une DLL C++, on va introduire le sujet en référençant une bibliothèque statique. On va ensuite convertir cette bibliothèque statique en bibliothèque dynamique et indiquer quelques méthodes pour la référencer.
Quelques définitions et explications en préambule
Bibliothèque statique vs bibliothèque dynamique
Bibliothèque dynamique
Bibliothèque statique
Les étapes de la compilation
Pre-processing
Compilation
Edition de liens
Exposer les symboles d’une bibliothèque
Création d’une dépendance vers une bibliothèque statique
Installation Visual Studio
Création de la bibliothèque statique NativeCallee
Création de l’exécutable NativeCaller
Lancer l’exécution
Convertir la dépendance statique en dépendance dynamique
Référencer une DLL
Exposer des objets avec __declspec(dllexport)
Voir les fonctions exposées
Amélioration de l’implémentation
Pourquoi éviter les déclarations d’export inutiles ?
Pourquoi CallerRedirection contient les objets exposés par NativeCallee ?
Nouvelle implémentation pour éviter les déclarations d’export inutiles
Utilisation d’un fichier DEF pour exposer des objets
Exporter des fonctions sous forme d’un linkage C
Quelques définitions et explications en préambule
Dans cette partie, on va apporter quelques précisions sur les bibliothèques en C++ et sur les étapes de la compilation. Si vous êtes familier avec ces notions, passer directement à la partie suivante.
Bibliothèque statique vs bibliothèque dynamique
On distingue 2 types de bibliothèques dans lesquelles on peut compiler du code C++: les bibliothèques dynamiques et les bibliothèques statiques.
Bibliothèque dynamique
Les bibliothèques dynamiques (i.e. dynamic library) se rapprochent des assemblies .NET car elles ont quelques caractéristiques en commun:
- Il s’agit de fichiers séparés qui ne seront pas inclus dans le fichier exécutable ou dans une bibliothèque l’utilisant comme dépendance.
- L’extension de ces bibliothèques est
.dll
pour Dynamic Link Library.
Le gros intérêt de ces bibliothèques est d’être partageables entre plusieurs exécutables. Ainsi, à l’exécution, quand un appel est effectué vers une fonction se trouvant dans une bibliothèque dynamique, elle est chargée en mémoire. Si un autre exécutable utilise la même bibliothèque, le chargement en mémoire n’est effectué qu’une seule fois ce qui permet d’économiser de l’espace mémoire.
L’inconvénient majeur des bibliothèques dynamiques est qu’elles doivent être accessibles au moment de l’exécution. Comme ce sont des fichiers séparés, si l’un d’entre eux n’est pas accessible au moment de l’exécution, il se produira une erreur et l’exécutable va interrompre son exécution.
Bibliothèque statique
Les bibliothèques statiques (i.e. static library) sont des fichiers dont l’extension est .lib
. Si un exécutable ou une autre bibliothèque a une dépendance vers une bibliothèque statique, le code utilisé sera directement incorporé dans le fichier de l’exécutable ou de la bibliothèque.
L’intérêt de ces bibliothèques est d’être certain d’inclure les bonnes dépendances dans l’exécutable final. Au moment de l’exécution, il n’y a pas de risques que le fichier de la bibliothèque ne soit pas accessible puisque le code utilisé est inclus dans l’exécutable lui-même. De même, il n’y a pas de risque d’utiliser une mauvaise version.
A l’inverse, pour mettre à jour ce type de bibliothèque, il faut recompiler l’exécutable ou la bibliothèque l’utilisant. D’autre part, si plusieurs exécutables utilisent la même bibliothèque, le code exécuté sera chargé en mémoire autant de fois qu’un exécutable l’utilise puisqu’il fait partie directement de l’exécutable.
Les étapes de la compilation
En C++, la compilation s’effectue en 3 étapes: le pré-processing, la compilation à proprement parlé et l’édition de liens (i.e. linking).
Pre-processing
Cette étape correspond à un prétraitement des fichiers source. Dans un premier temps, les instructions #include
seront remplacées par le contenu des fichiers .h
auxquelles elles font référence. Toutes les déclarations seront, ainsi, indiquées dans les fichiers .cpp
.
Au cours de cette étape, d’autres traitements seront effectués:
- Certaines parties de code seront utilisées suivant les instructions correspondant aux directives préprocesseurs comme
#if
,#ifdefn
,#ifndef
, etc… (cf. documentation Microsoft). - Le remplacement de macros se feront avec l’instruction
#define
.
A la suite de cette étape, le code ne contiendra plus de références vers les fichiers .h
et les directives préprocesseurs seront remplacées par leur signification.
Compilation
La compilation consiste à générer des fichiers objet avec l’extension .obj
à partir des fichiers de code source .cpp. Les fichiers .obj
contiennent des instructions en langage machine correspondant au code compilé toutefois ces fichiers ne peuvent pas être exécutés directement. Chaque instruction dans les fichiers .cpp
est compilée en langage machine et en symboles. Ces symboles peuvent, par exemple, désignés des objets. Les symboles sont référencés avec leur nom.
Les fichiers .obj
peuvent contenir des références vers des symboles déclarés (grâce aux références vers les fichiers .h
) mais qui ne possèdent pas de définition. Le code compilé peut, ainsi, faire référence à ces symboles sans que le compilateur ne remarque l’absence de définition car les références utilisent le nom des symboles. Ces noms peuvent ne correspondre à aucun code machine exécutable.
Edition de liens
Cette étape va permettre de rassembler tous les fichiers .obj
pour produire un exécutable (fichier .exe), une bibliothèque statique (fichier .lib
) ou une bibliothèques dynamiques (fichier .dll
). En fonction des bibliothèques statiques indiquées en paramètre du linker, il va être capable de trouver une définition aux noms de symboles utilisés par le compilateur. Ainsi les noms des symboles sont remplacés par des adresses exactes.
Il est donc important de préciser en paramètre d’entrée du linker quelles sont les bibliothèques pouvant contenir les définitions des symboles utilisés.
Exposer les symboles d’une bibliothèque
Le fichier d’une bibliothèque statique est une archive des fichiers .obj
obtenus après la compilation. Lors de l’édition de liens, le contenu des bibliothèques statiques est parcouru pour chercher la définition des symboles. Quand cette définition est trouvée, le code correspondant est récupéré et placé dans le fichier résultant.
Les bibliothèques dynamiques sur les plateformes Windows n’exposent pas de symboles. Le linker n’est pas capable d’identifier le code se trouvant directement dans une DLL à partir du nom des symboles. Pour être capable d’identifier le code se trouvant dans une DLL, il faut que celle-ci expose les symboles correspondant.
Pour exposer les symboles à partir d’une DLL Windows, il faut utiliser dans le code l’instruction __declspec(dllexport)
suivi du code à exposer. Cette instruction va permettre de créer une bibliothèque statique d’import qui va contenir le code permettant de charger la DLL dynamique contenant le code exposé et de l’exécuter. Il faut référencer cette bibliothèque statique d’import dans les paramètres d’entrée du linker.
Lors de l’étape d’édition de liens, pour un symbole donné, le linker va récupérer le code se trouvant dans la bibliothèque statique d’import et l’inclure dans le fichier résultant. A l’exécution, le code se trouvant dans la DLL sera, ainsi, appelé sans que le linker ne connaisse directement l’adresse du code dans la DLL.
Dans la 2e partie de cette article, on indiquera comment utiliser l’instruction __declspec(dllexport)
pour générer une bibliothèque statique d’import.
Création d’une dépendance vers une bibliothèque statique
Avant de rentrer directement dans la génération d’une bibliothèque statique d’import, on va simplement créer 2 projets:
- Un projet appelé
NativeCallee
permettant de générer une bibliothèque statique contenant du code à appeler. - Un projet appelé
NativeCaller
permettant de générer un exécutable. Le code de ce projet permet d’appeler du code se trouvant dans la bibliothèque statique générée par le premier projetNativeCallee
.
Pour résumer, les appels se font de cette façon: NativeCaller
⇒ NativeCallee
.
Le code de cet article se trouve dans le repository GitHub github.com/msoft/cpp_dll_reference.
Installation Visual Studio
Avant de commencer, il faut s’assurer que le support C++ est bien installé dans Visual Studio. En exécutant l’installateur de Visual Studio 2017, il faut que “Développement Desktop en C++” soit coché:
Création de la bibliothèque statique NativeCallee
On va créer un projet C++ vide en cliquant sur les éléments suivant dans Visual Studio 2017:
- Créer un projet C++ vide en cliquant sur “Nouveau” ⇒ “Projet” ⇒ Dans “Visual C++”, cliquer sur “Projet vide” (“Empty project“).
Il faut nommer le projetNativeCallee
. - Ajouter un classe en effectuant un clique droit sur le projet puis cliquer sur “Ajouter…” et enfin cliquer sur “Classe”.
Il faut nommer la classe Callee. - Implémentation de
Callee.h
: insérer ce code dans le fichierCallee.h
:#ifndef CALLEE_H #define CALLEE_H #pragma once class Callee { private: wchar_t *textToDisplay; public: Callee(wchar_t *textToDisplay); ~Callee(); void DisplayText(); }; #endif /* CALLEE_H */
- Implémentaiton de
Callee.cpp
: insérer ce code dans le fichierCallee.cpp
:#include "Callee.h" #include <wchar.h> Callee::Callee(wchar_t *textToDisplay) { this->textToDisplay = textToDisplay; } Callee::~Callee() { } void Callee::DisplayText() { wprintf(this->textToDisplay, "%s\n"); }
Ce code permet simplement de créer une classe avec un texte à afficher dans la variable membre
textToDisplay
. La fonctionDisplayText()
permet d’afficher le texte. - Configurer les propriétés du projet
NativeCallee
:
Effectuer un clique droit sur le projetNativeCallee
puis cliquer sur Propriétés:
Dans la partie Général, sélectionner les éléments suivants:
- “Type de configuration” (“Configuration type“): “Bibliothèque statique (.lib)” (“Static Library“).
- “Jeu de caractères” (“Character set“): “Utiliser le jeu de caractères Unicode” (“Use Unicode Character set“).
- “Prise en charge du Common Language Runtime” (“Common Language Runtime Support“): “Pas de prise en charge du Common Language” (“No Common Language Runtime Support“).
Ne pas oublier de valider en cliquant sur “Appliquer”.
Dans la fenêtre de présentation des paramètres du projet, si on change les valeurs des paramètres “Configuration” et/ou “Plateforme” (“Platform“), on peut voir que les paramètres du projet reviennent à leur valeur par défaut:
Ainsi, toutes les valeurs sélectionnées sont spécifiques à une configuration et à une plateforme donnée. Ce qui signifie que si on change la configuration et/ou la plateforme cible, il faut ressaisir les paramètres du projet.
Si on lance la compilation du projet NativeCallee
en cliquant sur “Générer…” (“Build…“), on obtient les fichiers suivants:
- Dans le répertoire
NativeCallee\NativeCallee\Debug
, le fichierCallee.obj
correspondant au fichier objet de la classeCallee
. - Dans le répertoire
Debug
, le fichierNativeCallee.lib
correspondant à la bibliothèque statique générée.
Création de l’exécutable NativeCaller
On va, maintenant, créer un projet C++ permettant de générer une application:
- Créer une application Console en C++ en effectuant un clique droit sur la solution ⇒ cliquer sur “Ajouter” ⇒ “Nouveau projet” ⇒ Dans la partie “Visual C++”, sélectionner “Application Console Windows” (“Windows Console Application“).
Il faut nommer le projetNativeCaller
. - Ajouter la référence vers la bibliothèque statique
NativeCallee
: accéder aux propriétés du projetNativeCaller
en effectuant un clique droit sur le projetNativeCaller
puis en cliquant sur “Propriétés”.Indiquer les paramètres suivant:
- Dans la partie C/C++ ⇒ Général:
Indiquer:
“Autres répertoires Include” (“Additional Include Directories“):$(MSBuildProjectDirectory)\..\NativeCallee
Ce paramètre permet d’accéder au fichierNativeCallee.h
. - Dans la partie “Editeur de liens” (“Linker“) ⇒ “entrée” (“Input“):
Indiquer:
“Dépendances supplémentaires” (“Additional dependencies“):$(MSBuildProjectDirectory)\..\Debug\NativeCallee.lib
Ce paramètre permet d’indiquer la bibliothèque statiqueNativeCallee.lib
.
Ne pas oublier de changer les paramètres pour toutes les configurations.
- Dans la partie C/C++ ⇒ Général:
- Dans le fichier
NativeCaller.cpp
(contenant le main de l’application), il faut indiquer l’implémentation suivante:#include "pch.h" #include "Redirect.h" #include <iostream> int main() { std::wstring textToDisplay(L"Text to display"); Callee *callee = new Callee(const_cast<wchar_t*>(textToDisplay.c_str())); callee->DisplayText(); delete callee; std::cin.get(); return 0; }
Ce code permet d’instancier la classe
Callee
avec une chaine de caractères et d’appeler une méthode permettant d’afficher cette chaine.
Lancer l’exécution
Avec de lancer l’exécution, il faut définir le projet de démarrage:
- Effectuer un clique droit sur le projet
NativeCaller
(correspondant à l’exécutable) - Cliquer sur “Définir comme projet de démarrage” (“Set as Startup project“).
Lancer l’exécution en appuyant sur [F5].
Le résultat devrait être de la forme:
Calling other lib: Text to display
Ce code permet d’appeler du code se trouvant dans une bibliothèque statique. Le code s’exécute normallement.
Convertir la dépendance statique en dépendance dynamique
Dans cette partie, on va convertir la bibliothèque statique en bibliothèque dynamique de façon à mettre en évidence l’erreur lors de l’édition de liens.
Référencer une DLL
Tout d’abord, on convertit le projet NativeCallee
en bibliothèque dynamique:
- Dans les propriétés du projet
NativeCallee
: effectuer un clique droit sur le projetNativeCallee
puis cliquer sur “Propriétés”. - Dans la partie “Général”, changer le paramètre suivant:
“Type de configuration” (“Configuration Type“): “Bibliothèque dynamique (.dll)” (“Dynamic Library“) - Valider en cliquant sur OK.
Si on essaie de compiler, on obtient une erreur provenant du linker indiquant qu’il n’arrive pas à ouvrir le fichier NativeCallee.lib
.
Si on supprime la référence vers ce fichier dans les propriétés du projet NativeCaller
dans “Editeur de liens” (“Linker“) ⇒ “entrée” (“Input“) ⇒ “Dépendances supplémentaires” (“Additional dependencies“), on obtiendra une erreur indiquant que la résolution de symboles n’a pas pu aboutir.
Exposer des objets avec __declspec(dllexport)
Comme indiqué plus haut, il faut exposer les symboles de la bibliothèque dynamique NativeCallee.dll
de façon à générer une bibliothèque statique d’import. Cette bibliothèque statique d’import pourra être utilisée par NativeCaller
.
Pour générer la bibliothèque statique d’import, il faut modifier l’implémentation de Callee.h
en rajoutant les directives __declspec(dllexport)
devant les éléments à exposer:
#ifndef CALLEE_H
#define CALLEE_H
#pragma once
class Callee
{
private:
wchar_t *textToDisplay;
public:
__declspec(dllexport) Callee(wchar_t *textToDisplay);
__declspec(dllexport) ~Callee();
__declspec(dllexport) void DisplayText();
};
#endif /* CALLEE_H */
Si on compile le projet NativeCallee
, on remarque dans le répertoire Debug
, les fichiers:
NativeCallee.dll
correspondant à la bibliothèque dynamiqueNativeCallee
.NativeCallee.lib
correspondant à la bibliothèque statique d’import de la DLLNativeCallee.dll
.
Si on compile toute la solution et qu’on lance l’exécution, on constate que l’exécution est la même que précédemment.
Voir les fonctions exposées
Quelques méthodes permettent de voir les fonctions exposées par le DLL NativeCallee.dll
:
- Avec DependencyWalker: “DependancyWalker” est un utilitaire qui permet de voir toutes les dépendances d’une assembly, on peut ainsi vérifier que les dépendances existent et sont de la bonne version ou architecture.
“DependancyWalker” peut être téléchargé sur www.dependencywalker.com/.
Si on ouvre la DLL
NativeCallee.dll
avec “DependencyWalker”:
On remarque les fonctions exportées correspondant aux objets qui sont précédés par
__declspec(dllexport)
dans la classeCallee
:??0Callee@@QAE@PA_W@Z ??1Callee@@QAE@XZ ?DisplayText@Callee@@QAEXXZ
Les noms de fonctions sont déformés Les fonctions exposées correspondent à du code C++. Pour les exposer dans la DLL sont prendre en compte l’encapsulation dans une classe, leur nom est déformé (i.e. mangled).
- Avec dumpbin: dumpbin est un utilitaire livré avec Visual Studio C++, le chemin de l’exécutable est du type:
C:\Program Files\Microsoft Visual Studio [version VS]\VC\bin
ou
C:\Program Files (x86)\Microsoft Visual Studio [version VS]\VC\bin
On peut y accéder directement à partir de la ligne de commandes Visual Studio (ou “Developer Command Prompt for VS2017“).
On peut utiliser dumpbin pour afficher les symboles se trouvant dans un fichier
.obj
. Ainsi pour afficher les symboles se trouvant dans le fichier objet de la classeCallee
, il faut exécuter la commande suivante:dumpbin /symbols NativeCallee\Debug\Callee.obj
Amélioration de l’implémentation
Le code précédant fonctionne et permet d’exposer des objets dans une DLL toutefois il peut générer des déclarations d’export inutiles dans le cas où le code est appelé par une autre DLL.
Pourquoi éviter les déclarations d’export inutiles ?
Pour illustrer, on va créer une DLL intermédiaire entre NativeCaller
et NativeCallee
. Cette DLL sera appelée par NativeCaller
et elle appellera NativeCallee
. On appellera la DLL intermédiaire CallerRedirection
.
Pour résumer, les appels se feront de cette façon: NativeCaller
⇒ CallerRedirection
⇒ NativeCallee
.
Pour créer la DLL CallerRedirection.dll
, on crée un nouveau projet:
- Créer un projet C++ vide en cliquant sur “Nouveau” ⇒ “Projet” ⇒ Dans “Visual C++”, cliquer sur “Projet vide” (“Empty project“).
Il faut nommer le projetCallerRedirection
. - Ajouter un classe en effectuant un clique droit sur le projet puis cliquer sur “Ajouter…” et enfin cliquer sur “Classe”.
Il faut nommer la classeRedirect
. - Implémentation de
Redirect.h
: insérer ce code dans le fichierRedirect.h
:#ifndef REDIRECT_H #define REDIRECT_H #pragma once #include "Callee.h" class Redirect { private: Callee* callee; public: __declspec(dllexport) Redirect(wchar_t *textToDisplay); __declspec(dllexport) ~Redirect(); __declspec(dllexport) void RedirectCall(); };
- Implémentation de
Redirect.cpp
: insérer le code suivant dans le fichierRedirect.cpp
:#include "Redirect.h" Redirect::Redirect(wchar_t *textToDisplay) { this->callee = new Callee(textToDisplay); } Redirect::~Redirect() { delete this->callee; } void Redirect::RedirectCall() { this->callee->DisplayText(); }
Ce code permet d’instancier
Callee
avec une chaine de caractères et il permet d’appelerCallee::DisplayText()
pour afficher la chaine de caractères. - Ajouter la référence vers la bibliothèque statique
NativeCallee
: accéder aux propriétés du projetCallerRedirection
en effectuant un clique droit sur le projetCallerRedirection
puis en cliquant sur “Propriétés”.Indiquer les paramètres suivant:
- Dans la partie “C/C++”” ⇒ “Général”, indiquer:
“Autres répertoires Include“:$(MSBuildProjectDirectory)\..\NativeCallee
Ce paramètre permet d’accéder au fichierNativeCallee.h
. - Dans la partie “Editeur de liens” (“Linker“) ⇒ “entrée” (“Input“), indiquer:
“Dépendances supplémentaires” (“Additional dependencies“):$(MSBuildProjectDirectory)\..\Debug\NativeCallee.lib
Ce paramètre permet d’indiquer la bibliothèque statiqueNativeCallee.lib
.
- Dans la partie “C/C++”” ⇒ “Général”, indiquer:
- On modifie les propriétés du projet
NativeCaller
pour qu’il appelleCallerRedirection
au lieu d’appelerNativeCallee
.Dans les propriétés du projet
NativeCaller
(effectuer un clique droit sur le projetNativeCaller
puis en cliquant sur “Propriétés”):Indiquer les paramètres suivant:
- Dans la partie C/C++ ⇒ Général:
Indiquer:
“Autres répertoires Include” (“Include directories“):$(MSBuildProjectDirectory)\..\NativeCallee;$(MSBuildProjectDirectory)\..\CallerRedirection
Ce paramètre permet d’accéder aux fichiersNativeCallee.h
etRedirect.h
. - Dans la partie “Editeur de liens” (“Linker“) ⇒ “entrée” (“Input“):
Indiquer:
“Dépendances supplémentaires” (“Additional dependencies“):$(MSBuildProjectDirectory)\..\Debug\CallerRedirection.lib
Ce paramètre permet d’indiquer la bibliothèque statique d’import deCallerRedirection.lib
.
- Dans la partie C/C++ ⇒ Général:
- On modifie l’implémentation de
NativeCaller
dans le fichierNativeCaller.cpp
:#include "pch.h" #include "Redirect.h" #include <iostream> int main() { std::wstring textToDisplay(L"Text to display"); Redirect *callRedirection = new Redirect(const_cast<wchar_t*>(textToDisplay.c_str())); std::cout << "Calling other lib:\n"; callRedirection->RedirectCall(); delete callRedirection; std::cin.get(); return 0; }
Ce code permet d’appeler
CallerRedirection
au lien d’appeler directementNativeCallee
. - Pour que la compilation puisse réussir, il faut préciser l’ordre de compilation des projets. Ainsi on indique les dépendances pour que la compilation des projets se fasse dans le bon ordre:
- On effectue un clique droit sur la solution puis on clique sur “Dépendances du projet” (“Project dependencies“).
- En sélectionnant
NativeCaller
, il faut cocherCallerRedirection
etNativeCaller
. - En sélectionnant
CallerRedirection
, il faut cocherNativeCallee
.
L’ordre de compilation doit être le suivant:
Si on compile la solution et si on exécute, le résultat devrait être semblable à ce qu’on a obtenu précédemment.
Si on affiche les symboles exportés de CallerRedirection
en exécutant la commande suivante:
dumpbin /symbols CallerRedirection/Debug/Redirect.obj
On peut voir parmi les objets exportées:
03C 00000000 UNDEF notype () External | ??0Callee@@QAE@PA_W@Z (public: __thiscall Callee::Callee(wchar_t *)) 03D 00000000 UNDEF notype () External | ??1Callee@@QAE@XZ (public: __thiscall Callee::~Callee(void)) 03E 00000000 UNDEF notype () External | ?DisplayText@Callee@@QAEXXZ (public: void __thiscall Callee::DisplayText(void)) 03F 00000000 SECTF notype () External | ?__autoclassinit2@Callee@@QAEXI@Z (public: void __thiscall Callee::__autoclassinit2(unsigned int)) 040 00000000 SECTB notype () External | ??_GCallee@@QAEPAXI@Z (public: void * __thiscall Callee::`scalar deleting destructor'(unsigned int)) 041 00000000 SECT5 notype () External | ??0Redirect@@QAE@PA_W@Z (public: __thiscall Redirect::Redirect(wchar_t *)) 042 00000000 SECT8 notype () External | ??1Redirect@@QAE@XZ (public: __thiscall Redirect::~Redirect(void)) 043 00000000 SECTD notype () External | ?RedirectCall@Redirect@@QAEXXZ (public: void __thiscall Redirect::RedirectCall(void))
On peut voir des fonctions exposées provenant de NativeCallee
.
Pourquoi CallerRedirection contient les objets exposés par NativeCallee ?
CallerRedirection
contient les objets exposés par NativeCallee
à cause de l’instruction:
#include "Callee.h"
Cette instruction se trouve dans Redirect.h
.
Pour rappel et comme on l’a expliqué plus haut dans les étapes de compilation, lors de l’étape de pré-processing, le code se trouvant dans les fichiers .h
est directement inclus dans le code dans les fichiers .cpp
. Ainsi le contenu du fichier Callee.h
est placé dans le fichier Redirect.h
qui lui-même est placé dans le fichier Redirect.cpp
.
Ainsi le fichier objet Redirect.obj
contient aussi les directives d’export de NativeCallee
:
__declspec(dllexport) Callee(wchar_t *textToDisplay);
__declspec(dllexport) ~Callee();
__declspec(dllexport) void DisplayText();
Ces objets sont exposés par CallerRedirection
pourtant CallerRedirection
ne les expose pas directement.
Nouvelle implémentation pour éviter les déclarations d’export inutiles
On améliore l’implémentation en utilisant des macros et des directives préprocesseurs. Cette implémentation permet d’éviter des déclarations d’export soient faites inutilement dans une DLL alors qu’elle ne définit pas directement les objets exportés.
Pour éviter ça:
- On modifie l’implémentation dans
Callee.h
et on rajoute des directives pré-processeur:#ifndef CALLEE_H #define CALLEE_H #pragma once #ifdef NATIVECALLEE_EXPORTS #define NATIVECALLEE_API __declspec(dllexport) #else #define NATIVECALLEE_API __declspec(dllimport) #endif class Callee { private: wchar_t *textToDisplay; public: NATIVECALLEE_API Callee(wchar_t *textToDisplay); NATIVECALLEE_API ~Callee(); NATIVECALLEE_API void DisplayText(); }; #endif /* CALLEE_H */
- Dans les propriétés du projet
NativeCallee
Dans la partie “C/C++”” ⇒ “Préprocesseur”, on indique la valeur suivante pour le paramètre “Définitions de préprocesseur” (“Preprocessor definitions“):NATIVECALLEE_EXPORTS
La présence de cette valeur lors de l’étape de pré-processing permet d’affecter une valeur différente à la macro NATIVECALLEE_API
. Ainsi:
- Dans le projet
NativeCallee
, quand l’étape de pré-processing s’exécute, la valeurNATIVECALLEE_EXPORTS
est définie et c’est la ligne suivante qui va s’exécuter:#define NATIVECALLEE_API __declspec(dllexport)
Par suite, les objets dans la classe
Callee
seront déclarés de la façon suivante:__declspec(dllexport) Callee(wchar_t *textToDisplay); __declspec(dllexport) ~Callee(); __declspec(dllexport) void DisplayText();
Ces objets seront donc bien exposés par la DLL
NativeCallee.dll
comme précédemment. - Dans le projet
CallerRedirection
, quand l’étape de pré-processing s’exécute, la valeur NATIVECALLEE_EXPORTS n’est pas définie et c’est la ligne suivant qui va s’exécuter:#define NATIVECALLEE_API __declspec(dllimport)
Par suite, les objets dans la classe
Callee
seront déclarés de la façon suivante:__declspec(dllimport) Callee(wchar_t *textToDisplay); __declspec(dllimport) ~Callee(); __declspec(dllimport) void DisplayText();
Ces objets seront, ainsi importés et non pas exposés par la DLL
CallerRedirection.dll
.
Si on recompile le code, ainsi obtenu, et si on observe les symboles se trouvant dans le fichier objet CallerRedirection/Debug/Redirect.obj
, on obtient:
03C 00000000 UNDEF notype External | __imp_??0Callee@@QAE@PA_W@Z (__declspec(dllimport) public: __thiscall Callee::Callee(wchar_t *)) 03D 00000000 UNDEF notype External | __imp_??1Callee@@QAE@XZ (__declspec(dllimport) public: __thiscall Callee::~Callee(void)) 03E 00000000 UNDEF notype External | __imp_?DisplayText@Callee@@QAEXXZ (__declspec(dllimport) public: void __thiscall Callee::DisplayText(void)) 03F 00000000 SECTF notype () External | ?__autoclassinit2@Callee@@QAEXI@Z (public: void __thiscall Callee::__autoclassinit2(unsigned int)) 040 00000000 SECTB notype () External | ??_GCallee@@QAEPAXI@Z (public: void * __thiscall Callee::`scalar deleting destructor'(unsigned int)) 041 00000000 SECT5 notype () External | ??0Redirect@@QAE@PA_W@Z (public: __thiscall Redirect::Redirect(wchar_t *)) 042 00000000 SECT8 notype () External | ??1Redirect@@QAE@XZ (public: __thiscall Redirect::~Redirect(void)) 043 00000000 SECTD notype () External | ?RedirectCall@Redirect@@QAEXXZ (public: void __thiscall Redirect::RedirectCall(void))
On remarque que les fonctions de NativeCallee
sont importées et non plus exportées.
Utilisation d’un fichier DEF pour exposer des objets
Au lieu d’utiliser l’instruction __declspec(dllexport)
dans le code, il est possible d’utiliser un fichier dans lequel on indique les objets à exposer. La difficulté à utiliser ce fichier est qu’il faut indiquer le nom de la fonction tel qu’il apparaît dans les symboles du fichier .obj
.
Par exemple si on modifie le projet CallerRedirection
:
- Dans l’implémentation de
Redirect.h
, on supprime toutes les directives__declspec(dllexport)
:#ifndef REDIRECT_H #define REDIRECT_H #pragma once #include "Callee.h" class Redirect { private: Callee* callee; public: Redirect(wchar_t *textToDisplay); ~Redirect(); void RedirectCall(); }; #endif /* REDIRECT_H */
- On recompile seulement le projet
CallerRedirection
en effectuant un clique droit sur le projet puis en cliquant sur “Regénérer” (“Rebuild“). - On génère les symboles en exécutant la commande suivante avec la ligne de commandes de Visual Studio:
dumpbin /symbols CallerRedirection\Debug\Redirect.obj
Pour faciliter la récupération des noms de fonctions on peut rediriger la sortie dans un fichier texte:
dumpbin /symbols CallerRedirection\Debug\Redirect.obj > symbols.txt
Les noms des fonctions à exposer apparaissent de cette façon:
041 00000000 SECT5 notype () External | ??0Redirect@@QAE@PA_W@Z (public: __thiscall Redirect::Redirect(wchar_t *)) 042 00000000 SECT8 notype () External | ??1Redirect@@QAE@XZ (public: __thiscall Redirect::~Redirect(void)) 043 00000000 SECTD notype () External | ?RedirectCall@Redirect@@QAEXXZ (public: void __thiscall Redirect::RedirectCall(void))
- On crée un fichier nommé
CallerRedirection.def
dans le répertoireCallerRedirection
avec le contenu suivant:LIBRARY CALLERREDIRECTION EXPORTS ??0Redirect@@QAE@PA_W@Z @1 ??1Redirect@@QAE@XZ @2 ?RedirectCall@Redirect@@QAEXXZ @3
On indique chaque fonction à exposer suivie d’un espace et d’une numérotation du type
@<index de la fonction>
- Dans les propriétés du projet
CallerRedirection
:Dans la partie “Editeurs de liens” (“Linker“) ⇒ “entrée” (“Input“) ⇒ Pour le paramètre “Fichier de définition de module” (“Module Definition File“), on indique le nom:
CallerRedirection.def
On relance la compilation, toute la solution devrait compiler normalement comme précédemment. Les fonctions sont donc bien exposées.
Exporter des fonctions sous forme d’un linkage C
Les méthodes précédentes permettent d’exposer des objets C++. Ces exports sont compatibles à condition d’être appelés par du code C++. Il est possible d’effectuer des déclarations d’export en utilisant des exports compatibles C en utilisant la directive extern "C"
.
Pour l’utiliser, il suffit de préfixer la déclaration de la fonction à exposer avec extern "C"
.
Par exemple si on modifie le projet NativeCallee
:
- On modifie le fichier
Callee.h
avant la déclaration de la classe Callee:#ifndef CALLEE_H #define CALLEE_H #pragma once #ifdef NATIVECALLEE_EXPORTS #define NATIVECALLEE_API __declspec(dllexport) #else #define NATIVECALLEE_API __declspec(dllimport) #endif extern "C" NATIVECALLEE_API void DisplayTextWithCallee(const wchar_t *textToDisplay); class Callee { private: wchar_t *textToDisplay; public: NATIVECALLEE_API Callee(wchar_t *textToDisplay); NATIVECALLEE_API ~Callee(); NATIVECALLEE_API void DisplayText(); }; #endif /* CALLEE_H */
On déclare une nouvelle méthode à exposer
DisplayTextWithCallee()
. - On implémente la fonction dans
Callee.cpp
:void DisplayTextWithCallee(const wchar_t *textToDisplay) { Callee *callee = new Callee(const_cast<wchar_t*>(textToDisplay)); callee->DisplayText(); delete callee; }
Cette méthode permet d’instancier une classe
Callee
avec une chaine de caractères comme pour les exemples précédents. Ensuite, on affiche le contenu de la chaine de caractères. - On modifie l’implémentation dans le projet
CallerRedirection
pour appeler la méthodeDisplayTextWithCallee()
.D’abord on modifie l’implémentation de
Redirect.h
:#ifndef REDIRECT_H #define REDIRECT_H #pragma once #include "Callee.h" class Redirect { private: wchar_t *textToDisplay; public: __declspec(dllexport) Redirect(wchar_t *textToDisplay); __declspec(dllexport) ~Redirect(); __declspec(dllexport) void RedirectCall(); }; #endif /* REDIRECT_H */
- On modifie ensuite l’implémentation de
Redirect.cpp
:#include "Redirect.h" Redirect::Redirect(wchar_t *textToDisplay) { this->textToDisplay = textToDisplay; } Redirect::~Redirect() { } void Redirect::RedirectCall() { DisplayTextWithCallee(this->textToDisplay); }
Si on observe la façon dont la fonction est exportée avec “DependencyWalker” dans la DLL CallerRedirection.dll
, on constate que le nom n’est pas déformé comme c’est le cas pour les fonctions provenant d’objets C++:
Si on exécute la commande suivante avec la ligne de commandes Visual Studio:
dumpbin /symbols NativeCallee\Debug\Callee.obj
On obtient les symboles suivants:
058 00000000 UNDEF notype () External | ??2@YAPAXI@Z (void * __cdecl operator new(unsigned int)) 059 00000000 UNDEF notype () External | ??3@YAXPAXI@Z (void __cdecl operator delete(void *,unsigned int)) 05A 00000000 SECT10 notype () External | _DisplayTextWithCallee 05B 00000000 SECT5 notype () External | ??0Callee@@QAE@PA_W@Z (public: __thiscall Callee::Callee(wchar_t *)) 05C 00000000 SECT7 notype () External | ??1Callee@@QAE@XZ (public: __thiscall Callee::~Callee(void)) 05D 00000000 SECTC notype () External | ?DisplayText@Callee@@QAEXXZ (public: void __thiscall Callee::DisplayText(void))
On constate que le nom de fonction exportée sous forme d’un linkage C n’est pas déformé.
Le code de cet article se trouve dans le repository GitHub github.com/msoft/cpp_dll_reference.
Conclusion
Le but de cet article était d’apporter quelques indications pour exposer des fonctions se trouvant dans une DLL de façon à pouvoir les consommer à partir d’un autre projet C++ lors de la compilation et par une autre bibliothèque au runtime. J’espère qu’il aura pu rendre toutes ces problématiques plus claires.
Cet article sert d’introduction pour illustrer la consommation de bibliothèque native à partir de code managé. Sujet qui sera traité dans des articles ultérieurs.
- What is the effect of extern “C” in C++?: https://stackoverflow.com/questions/1041866/what-is-the-effect-of-extern-c-in-c
- what does __declspec(dllimport) really mean?:
https://stackoverflow.com/questions/8863193/what-does-declspecdllimport-really-mean - .def files C/C++ DLLs: https://stackoverflow.com/questions/366228/def-files-c-c-dlls
- Exporting DLL C++ Class , question about .def file: https://stackoverflow.com/questions/186232/exporting-dll-c-class-question-about-def-file
- Exporting from a DLL Using DEF Files: https://docs.microsoft.com/fr-fr/cpp/build/exporting-from-a-dll-using-def-files?view=vs-2019
- Preprocessor directives: http://www.cplusplus.com/doc/tutorial/preprocessor/
- How C++ Works: Understanding Compilation: https://www.toptal.com/c-plus-plus/c-plus-plus-understanding-compilation
- Internal and External Linkage in C++: http://www.goldsborough.me/c/c++/linker/2016/03/30/19-34-25-internal_and_external_linkage_in_c++/
- Libraries: https://ptolemy.berkeley.edu/ptolemyclassic/almagest/docs/user/html/sharedlib.doc1.html
- Exporting Symbols: http://www.lurklurk.org/linkers/linkers.html#winexport
- Link an executable to a DLL: https://docs.microsoft.com/en-us/cpp/build/linking-an-executable-to-a-dll?view=vs-2019
- #define Directive (C/C++): https://docs.microsoft.com/fr-fr/cpp/preprocessor/hash-define-directive-c-cpp?view=vs-2019