La réponse courte à cette question est non . Parce qu'il n'y a pas d' ABI C ++ standard (interface binaire d'application, un standard pour les conventions d'appel, l'empaquetage / alignement des données, la taille du type, etc.), vous devrez sauter à travers de nombreux obstacles pour essayer d'appliquer une manière standard de gérer la classe objets dans votre programme. Il n'y a même pas de garantie que cela fonctionnera après avoir franchi tous ces obstacles, ni de garantie qu'une solution qui fonctionne dans une version du compilateur fonctionnera dans la suivante.
Créez simplement une interface C simple en utilisant extern "C"
, car l'ABI C est bien défini et stable.
Si vous voulez vraiment, vraiment passer des objets C ++ à travers une limite DLL, c'est techniquement possible. Voici quelques-uns des facteurs dont vous devrez tenir compte:
Emballage / alignement des données
Dans une classe donnée, les membres de données individuels seront généralement placés spécialement en mémoire afin que leurs adresses correspondent à un multiple de la taille du type. Par exemple, un int
peut être aligné sur une limite de 4 octets.
Si votre DLL est compilée avec un compilateur différent de votre EXE, la version de la DLL d'une classe donnée peut avoir un emballage différent de la version de l'EXE, donc lorsque l'EXE passe l'objet de classe à la DLL, la DLL peut être incapable d'accéder correctement à un donnée membre de cette classe. La DLL essaierait de lire à partir de l'adresse spécifiée par sa propre définition de la classe, et non par la définition de l'EXE, et puisque le membre de données souhaité n'y est pas réellement stocké, des valeurs inutiles en résulteraient.
Vous pouvez contourner ce #pragma pack
problème à l'aide de la directive préprocesseur, qui forcera le compilateur à appliquer une compression spécifique. Le compilateur appliquera toujours l'empaquetage par défaut si vous sélectionnez une valeur de paquet plus grande que celle que le compilateur aurait choisie , donc si vous choisissez une valeur d'emballage élevée, une classe peut toujours avoir un empaquetage différent entre les compilateurs. La solution pour cela est d'utiliser #pragma pack(1)
, ce qui forcera le compilateur à aligner les données membres sur une limite d'un octet (essentiellement, aucun compactage ne sera appliqué). Ce n'est pas une bonne idée, car cela peut entraîner des problèmes de performances ou même des plantages sur certains systèmes. Cependant, il va assurer la cohérence de la façon dont les membres de données de votre classe sont alignés en mémoire.
Réorganisation des membres
Si votre classe n'est pas à disposition standard , le compilateur peut réorganiser ses données membres en mémoire . Il n'y a pas de norme sur la façon dont cela est fait, donc toute réorganisation des données peut provoquer des incompatibilités entre les compilateurs. La transmission de données dans les deux sens à une DLL nécessitera donc des classes de mise en page standard.
Convention d'appel
Une fonction donnée peut avoir plusieurs conventions d'appel . Ces conventions d'appel spécifient comment les données doivent être passées aux fonctions: les paramètres sont-ils stockés dans des registres ou sur la pile? Dans quel ordre les arguments sont-ils poussés sur la pile? Qui nettoie les arguments laissés sur la pile une fois la fonction terminée?
Il est important que vous mainteniez une convention d'appel standard; si vous déclarez une fonction comme _cdecl
, la valeur par défaut pour C ++, et essayez de l'appeler en utilisant de _stdcall
mauvaises choses se produira . _cdecl
est la convention d'appel par défaut pour les fonctions C ++, cependant, c'est une chose qui ne cassera pas à moins que vous ne la rompiez délibérément en spécifiant un _stdcall
à un endroit et un _cdecl
à un autre.
Taille du type de données
Selon cette documentation , sous Windows, la plupart des types de données fondamentaux ont les mêmes tailles, que votre application soit 32 bits ou 64 bits. Cependant, étant donné que la taille d'un type de données donné est imposée par le compilateur, et non par aucune norme (toutes les garanties standard sont que 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
), c'est une bonne idée d'utiliser des types de données de taille fixe pour assurer la compatibilité de la taille des types de données lorsque cela est possible.
Problèmes de tas
Si votre DLL est liée à une version différente du runtime C que votre EXE, les deux modules utiliseront des tas différents . C'est un problème particulièrement probable étant donné que les modules sont compilés avec différents compilateurs.
Pour atténuer cela, toute la mémoire devra être allouée dans un tas partagé et désallouée à partir du même tas. Heureusement, Windows fournit des API pour vous aider: GetProcessHeap vous permettra d'accéder au tas de fichiers EXE de l'hôte, et HeapAlloc / HeapFree vous permettra d'allouer et de libérer de la mémoire dans ce tas. Il est important que vous n'utilisiez pas normal malloc
/ free
car il n'y a aucune garantie qu'ils fonctionneront comme vous le souhaitez.
Problèmes de STL
La bibliothèque standard C ++ a son propre ensemble de problèmes ABI. Il n'y a aucune garantie qu'un type STL donné est disposé de la même manière en mémoire, ni aucune garantie qu'une classe STL donnée a la même taille d'une implémentation à une autre (en particulier, les versions de débogage peuvent mettre des informations de débogage supplémentaires dans un donné le type de STL). Par conséquent, tout conteneur STL devra être décompressé en types fondamentaux avant d'être passé à travers la limite DLL et reconditionné de l'autre côté.
Nom mutilation
Votre DLL exportera vraisemblablement les fonctions que votre EXE voudra appeler. Cependant, les compilateurs C ++ n'ont pas de méthode standard pour modifier les noms de fonctions . Cela signifie qu'une fonction nommée GetCCDLL
peut être transformée _Z8GetCCDLLv
en GCC et ?GetCCDLL@@YAPAUCCDLL_v1@@XZ
MSVC.
Vous ne pourrez déjà pas garantir la liaison statique vers votre DLL, car une DLL produite avec GCC ne produira pas de fichier .lib et la liaison statique d'une DLL dans MSVC en nécessite un. La liaison dynamique semble être une option beaucoup plus propre, mais la modification des noms vous gêne: si vous essayez GetProcAddress
d'utiliser le mauvais nom mutilé, l'appel échouera et vous ne pourrez pas utiliser votre DLL. Cela nécessite un peu de piratage pour se déplacer, et c'est une raison assez importante pour laquelle le passage de classes C ++ à travers une limite DLL est une mauvaise idée.
Vous devrez créer votre DLL, puis examiner le fichier .def produit (le cas échéant; cela variera en fonction des options de votre projet) ou utiliser un outil comme Dependency Walker pour trouver le nom mutilé. Ensuite, vous devrez écrire votre propre fichier .def, définissant un alias démêlé pour la fonction mutilée. À titre d'exemple, utilisons la GetCCDLL
fonction que j'ai mentionnée un peu plus haut. Sur mon système, les fichiers .def suivants fonctionnent respectivement pour GCC et MSVC:
GCC:
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVC:
EXPORTS
GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1
Reconstruisez votre DLL, puis réexaminez les fonctions qu'elle exporte. Un nom de fonction démêlé devrait en faire partie. Notez que vous ne pouvez pas utiliser les fonctions surchargées de cette manière : le nom de la fonction non mélangée est un alias pour une surcharge de fonction spécifique telle que définie par le nom mutilé. Notez également que vous devrez créer un nouveau fichier .def pour votre DLL chaque fois que vous modifiez les déclarations de fonction, car les noms mutilés changeront. Plus important encore, en contournant le nom de la déformation, vous remplacez toutes les protections que l'éditeur de liens essaie de vous offrir en ce qui concerne les problèmes d'incompatibilité.
Tout ce processus est plus simple si vous créez une interface à suivre pour votre DLL, car vous n'aurez qu'une seule fonction pour définir un alias au lieu de devoir créer un alias pour chaque fonction de votre DLL. Cependant, les mêmes mises en garde s'appliquent toujours.
Passer des objets de classe à une fonction
C'est probablement le plus subtil et le plus dangereux des problèmes qui affectent le passage des données entre les compilateurs. Même si vous gérez tout le reste, il n'y a pas de norme sur la façon dont les arguments sont passés à une fonction . Cela peut provoquer des plantages subtils sans raison apparente et sans moyen facile de les déboguer . Vous devrez passer tous les arguments via des pointeurs, y compris les tampons pour toutes les valeurs de retour. C'est maladroit et peu pratique, et c'est encore une autre solution de contournement hacky qui peut ou non fonctionner.
En rassemblant toutes ces solutions de contournement et en nous appuyant sur un travail créatif avec des modèles et des opérateurs , nous pouvons tenter de transmettre en toute sécurité des objets à travers une limite de DLL. Notez que la prise en charge de C ++ 11 est obligatoire, de même que la prise en charge #pragma pack
et ses variantes; MSVC 2013 offre ce support, tout comme les versions récentes de GCC et clang.
//POD_base.h: defines a template base class that wraps and unwraps data types for safe passing across compiler boundaries
//define malloc/free replacements to make use of Windows heap APIs
namespace pod_helpers
{
void* pod_malloc(size_t size)
{
HANDLE heapHandle = GetProcessHeap();
HANDLE storageHandle = nullptr;
if (heapHandle == nullptr)
{
return nullptr;
}
storageHandle = HeapAlloc(heapHandle, 0, size);
return storageHandle;
}
void pod_free(void* ptr)
{
HANDLE heapHandle = GetProcessHeap();
if (heapHandle == nullptr)
{
return;
}
if (ptr == nullptr)
{
return;
}
HeapFree(heapHandle, 0, ptr);
}
}
//define a template base class. We'll specialize this class for each datatype we want to pass across compiler boundaries.
#pragma pack(push, 1)
// All members are protected, because the class *must* be specialized
// for each type
template<typename T>
class pod
{
protected:
pod();
pod(const T& value);
pod(const pod& copy);
~pod();
pod<T>& operator=(pod<T> value);
operator T() const;
T get() const;
void swap(pod<T>& first, pod<T>& second);
};
#pragma pack(pop)
//POD_basic_types.h: holds pod specializations for basic datatypes.
#pragma pack(push, 1)
template<>
class pod<unsigned int>
{
//these are a couple of convenience typedefs that make the class easier to specialize and understand, since the behind-the-scenes logic is almost entirely the same except for the underlying datatypes in each specialization.
typedef int original_type;
typedef std::int32_t safe_type;
public:
pod() : data(nullptr) {}
pod(const original_type& value)
{
set_from(value);
}
pod(const pod<original_type>& copyVal)
{
original_type copyData = copyVal.get();
set_from(copyData);
}
~pod()
{
release();
}
pod<original_type>& operator=(pod<original_type> value)
{
swap(*this, value);
return *this;
}
operator original_type() const
{
return get();
}
protected:
safe_type* data;
original_type get() const
{
original_type result;
result = static_cast<original_type>(*data);
return result;
}
void set_from(const original_type& value)
{
data = reinterpret_cast<safe_type*>(pod_helpers::pod_malloc(sizeof(safe_type))); //note the pod_malloc call here - we want our memory buffer to go in the process heap, not the possibly-isolated DLL heap.
if (data == nullptr)
{
return;
}
new(data) safe_type (value);
}
void release()
{
if (data)
{
pod_helpers::pod_free(data); //pod_free to go with the pod_malloc.
data = nullptr;
}
}
void swap(pod<original_type>& first, pod<original_type>& second)
{
using std::swap;
swap(first.data, second.data);
}
};
#pragma pack(pop)
La pod
classe est spécialisée pour chaque type de données de base, de sorte qu'il int
sera automatiquement enveloppé int32_t
, uint
sera enveloppé uint32_t
, etc. Tout cela se produit dans les coulisses, grâce aux opérateurs surchargés =
et ()
. J'ai omis le reste des spécialisations de type de base car elles sont presque entièrement les mêmes, à l'exception des types de données sous-jacents (la bool
spécialisation a un peu de logique supplémentaire, car elle est convertie en a int8_t
, puis int8_t
est comparée à 0 pour la reconvertir en bool
, mais c'est assez trivial).
Nous pouvons également envelopper les types STL de cette manière, même si cela nécessite un peu de travail supplémentaire:
#pragma pack(push, 1)
template<typename charT>
class pod<std::basic_string<charT>> //double template ftw. We're specializing pod for std::basic_string, but we're making this specialization able to be specialized for different types; this way we can support all the basic_string types without needing to create four specializations of pod.
{
//more comfort typedefs
typedef std::basic_string<charT> original_type;
typedef charT safe_type;
public:
pod() : data(nullptr) {}
pod(const original_type& value)
{
set_from(value);
}
pod(const charT* charValue)
{
original_type temp(charValue);
set_from(temp);
}
pod(const pod<original_type>& copyVal)
{
original_type copyData = copyVal.get();
set_from(copyData);
}
~pod()
{
release();
}
pod<original_type>& operator=(pod<original_type> value)
{
swap(*this, value);
return *this;
}
operator original_type() const
{
return get();
}
protected:
//this is almost the same as a basic type specialization, but we have to keep track of the number of elements being stored within the basic_string as well as the elements themselves.
safe_type* data;
typename original_type::size_type dataSize;
original_type get() const
{
original_type result;
result.reserve(dataSize);
std::copy(data, data + dataSize, std::back_inserter(result));
return result;
}
void set_from(const original_type& value)
{
dataSize = value.size();
data = reinterpret_cast<safe_type*>(pod_helpers::pod_malloc(sizeof(safe_type) * dataSize));
if (data == nullptr)
{
return;
}
//figure out where the data to copy starts and stops, then loop through the basic_string and copy each element to our buffer.
safe_type* dataIterPtr = data;
safe_type* dataEndPtr = data + dataSize;
typename original_type::const_iterator iter = value.begin();
for (; dataIterPtr != dataEndPtr;)
{
new(dataIterPtr++) safe_type(*iter++);
}
}
void release()
{
if (data)
{
pod_helpers::pod_free(data);
data = nullptr;
dataSize = 0;
}
}
void swap(pod<original_type>& first, pod<original_type>& second)
{
using std::swap;
swap(first.data, second.data);
swap(first.dataSize, second.dataSize);
}
};
#pragma pack(pop)
Nous pouvons maintenant créer une DLL qui utilise ces types de pod. Tout d'abord, nous avons besoin d'une interface, nous n'aurons donc qu'une seule méthode pour déterminer la manipulation.
//CCDLL.h: defines a DLL interface for a pod-based DLL
struct CCDLL_v1
{
virtual void ShowMessage(const pod<std::wstring>* message) = 0;
};
CCDLL_v1* GetCCDLL();
Cela crée simplement une interface de base que la DLL et tous les appelants peuvent utiliser. Notez que nous passons un pointeur vers a pod
, pas vers pod
lui - même. Nous devons maintenant implémenter cela du côté DLL:
struct CCDLL_v1_implementation: CCDLL_v1
{
virtual void ShowMessage(const pod<std::wstring>* message) override;
};
CCDLL_v1* GetCCDLL()
{
static CCDLL_v1_implementation* CCDLL = nullptr;
if (!CCDLL)
{
CCDLL = new CCDLL_v1_implementation;
}
return CCDLL;
}
Et maintenant, implémentons la ShowMessage
fonction:
#include "CCDLL_implementation.h"
void CCDLL_v1_implementation::ShowMessage(const pod<std::wstring>* message)
{
std::wstring workingMessage = *message;
MessageBox(NULL, workingMessage.c_str(), TEXT("This is a cross-compiler message"), MB_OK);
}
Rien d'extraordinaire: cela copie simplement le passé pod
dans une normale wstring
et l'affiche dans une boîte de message. Après tout, ce n'est qu'un POC , pas une bibliothèque d'utilitaires complète.
Nous pouvons maintenant créer la DLL. N'oubliez pas les fichiers .def spéciaux pour contourner le changement de nom de l'éditeur de liens. (Remarque: la structure CCDLL que j'ai réellement construite et exécutée avait plus de fonctions que celle que je présente ici. Les fichiers .def peuvent ne pas fonctionner comme prévu.)
Maintenant, pour un EXE pour appeler la DLL:
//main.cpp
#include "../CCDLL/CCDLL.h"
typedef CCDLL_v1*(__cdecl* fnGetCCDLL)();
static fnGetCCDLL Ptr_GetCCDLL = NULL;
int main()
{
HMODULE ccdll = LoadLibrary(TEXT("D:\\Programming\\C++\\CCDLL\\Debug_VS\\CCDLL.dll")); //I built the DLL with Visual Studio and the EXE with GCC. Your paths may vary.
Ptr_GetCCDLL = (fnGetCCDLL)GetProcAddress(ccdll, (LPCSTR)"GetCCDLL");
CCDLL_v1* CCDLL_lib;
CCDLL_lib = Ptr_GetCCDLL(); //This calls the DLL's GetCCDLL method, which is an alias to the mangled function. By dynamically loading the DLL like this, we're completely bypassing the name mangling, exactly as expected.
pod<std::wstring> message = TEXT("Hello world!");
CCDLL_lib->ShowMessage(&message);
FreeLibrary(ccdll); //unload the library when we're done with it
return 0;
}
Et voici les résultats. Notre DLL fonctionne. Nous avons réussi à atteindre les problèmes STL ABI passés, les problèmes ABI C ++ passés, les problèmes antérieurs de déformation, et notre DLL MSVC fonctionne avec un EXE GCC.
En conclusion, si vous devez absolument transmettre des objets C ++ au-delà des limites de la DLL, voici comment procéder. Cependant, rien de tout cela n'est garanti pour fonctionner avec votre configuration ou celle de quelqu'un d'autre. Tout ceci peut être interrompu à tout moment, et probablement le jour avant que votre logiciel ne soit prévu pour une version majeure. Ce chemin est plein de hacks, de risques et d'idioties générales pour lesquelles je devrais probablement me faire tirer dessus. Si vous suivez cette voie, veuillez tester avec une extrême prudence. Et vraiment ... ne faites pas ça du tout.