Die kurze Antwort auf diese Frage lautet nicht . Da es kein Standard-C ++ - ABI gibt (Anwendungsbinärschnittstelle, Standard für Aufrufkonventionen, Datenpacken / -ausrichtung, Typgröße usw.), müssen Sie durch viele Rahmen springen, um eine Standardmethode für den Umgang mit Klassen durchzusetzen Objekte in Ihrem Programm. Es gibt nicht einmal eine Garantie, dass es funktioniert, nachdem Sie durch all diese Reifen gesprungen sind, noch gibt es eine Garantie, dass eine Lösung, die in einer Compiler-Version funktioniert, in der nächsten funktioniert.
Erstellen nur eine einfache C - Schnittstelle extern "C"
, da die C ABI ist gut definiert und stabil.
Wenn Sie C ++ - Objekte wirklich, wirklich über eine DLL-Grenze übergeben möchten, ist dies technisch möglich. Hier sind einige der Faktoren, die Sie berücksichtigen müssen:
Packen / Ausrichten von Daten
Innerhalb einer bestimmten Klasse werden einzelne Datenelemente normalerweise speziell im Speicher abgelegt, sodass ihre Adressen einem Vielfachen der Größe des Typs entsprechen. Beispielsweise int
könnte a an einer 4-Byte-Grenze ausgerichtet sein.
Wenn Ihre DLL mit einem anderen Compiler als Ihre EXE-Datei kompiliert wird, hat die DLL-Version einer bestimmten Klasse möglicherweise ein anderes Paket als die EXE-Version. Wenn die EXE das Klassenobjekt an die DLL übergibt, kann die DLL möglicherweise nicht ordnungsgemäß auf eine zugreifen gegebenes Datenelement innerhalb dieser Klasse. Die DLL würde versuchen, von der Adresse zu lesen, die durch ihre eigene Definition der Klasse angegeben ist, nicht durch die Definition der EXE, und da das gewünschte Datenelement dort nicht tatsächlich gespeichert ist, würden sich Müllwerte ergeben.
Sie können dies mit der #pragma pack
Präprozessor-Direktive umgehen, die den Compiler zwingt, bestimmte Packungen anzuwenden. Der Compiler wendet weiterhin die Standardverpackung an, wenn Sie einen Packwert auswählen, der größer ist als der vom Compiler gewählte . Wenn Sie also einen großen Packwert auswählen, kann eine Klasse zwischen den Compilern immer noch unterschiedliche Packungen aufweisen. Die Lösung hierfür ist die Verwendung #pragma pack(1)
, die den Compiler zwingt, Datenelemente an einer Ein-Byte-Grenze auszurichten (im Wesentlichen wird kein Packen angewendet). Dies ist keine gute Idee, da dies auf bestimmten Systemen zu Leistungsproblemen oder sogar zum Absturz führen kann. Dadurch wird jedoch die Konsistenz bei der Ausrichtung der Datenelemente Ihrer Klasse im Speicher sichergestellt.
Neuordnung der Mitglieder
Wenn Ihre Klasse kein Standardlayout hat , kann der Compiler seine Datenelemente im Speicher neu anordnen . Es gibt keinen Standard dafür, daher kann jede Neuordnung von Daten zu Inkompatibilitäten zwischen Compilern führen. Für die Weitergabe von Daten an eine DLL sind daher Standardlayoutklassen erforderlich.
Aufruf Konvention
Es gibt mehrere Aufrufkonventionen, die eine bestimmte Funktion haben kann. Diese Aufrufkonventionen legen fest, wie Daten an Funktionen übergeben werden sollen: Werden Parameter in Registern oder auf dem Stapel gespeichert? In welcher Reihenfolge werden Argumente auf den Stapel geschoben? Wer bereinigt alle auf dem Stapel verbleibenden Argumente, nachdem die Funktion beendet wurde?
Es ist wichtig, dass Sie eine Standard-Anrufkonvention einhalten. Wenn Sie eine Funktion als deklarieren _cdecl
, wird die Standardeinstellung für C ++ und der Versuch, sie mit _stdcall
schlechten Dingen aufzurufen, passieren . _cdecl
ist jedoch die Standardaufrufkonvention für C ++ - Funktionen. Dies ist also eine Sache, die nur dann unterbrochen wird, wenn Sie sie absichtlich durch Angabe einer _stdcall
an einer Stelle und einer _cdecl
an einer anderen Stelle brechen .
Datentypgröße
Laut dieser Dokumentation haben die meisten grundlegenden Datentypen unter Windows die gleichen Größen, unabhängig davon, ob Ihre App 32-Bit oder 64-Bit ist. Da die Größe eines bestimmten Datentyps jedoch vom Compiler und nicht von einem Standard erzwungen wird (alle Standardgarantien sind dies 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
), empfiehlt es sich, Datentypen mit fester Größe zu verwenden, um die Kompatibilität der Datentypgröße nach Möglichkeit sicherzustellen.
Haufenprobleme
Wenn Ihre DLL eine Verbindung zu einer anderen Version der C-Laufzeit als Ihre EXE-Datei herstellt, verwenden die beiden Module unterschiedliche Heaps . Dies ist ein besonders wahrscheinliches Problem, da die Module mit verschiedenen Compilern kompiliert werden.
Um dies zu vermeiden, muss der gesamte Speicher einem gemeinsam genutzten Heap zugewiesen und von demselben Heap freigegeben werden. Glücklicherweise bietet Windows APIs, die Ihnen dabei helfen: Mit GetProcessHeap können Sie auf den Heap der Host-EXE zugreifen, und mit HeapAlloc / HeapFree können Sie Speicher innerhalb dieses Heaps zuweisen und freigeben . Es ist wichtig, dass Sie nicht normal verwenden malloc
/ free
da es keine Garantie gibt, dass sie so funktionieren, wie Sie es erwarten.
STL-Probleme
Die C ++ - Standardbibliothek hat ihre eigenen ABI-Probleme. Es gibt keine Garantie dafür, dass ein bestimmter STL-Typ im Speicher auf dieselbe Weise angeordnet ist, und es gibt auch keine Garantie dafür, dass eine bestimmte STL-Klasse von einer Implementierung zur anderen dieselbe Größe hat (insbesondere können Debug-Builds zusätzliche Debug-Informationen in a einfügen gegebener STL-Typ). Daher muss jeder STL-Container in grundlegende Typen entpackt werden, bevor er über die DLL-Grenze geleitet und auf der anderen Seite neu verpackt wird.
Name Mangling
Ihre DLL exportiert vermutlich Funktionen, die Ihre EXE aufrufen möchte. C ++ - Compiler verfügen jedoch nicht über eine Standardmethode zum Verwalten von Funktionsnamen . Dies bedeutet, dass eine benannte Funktion in GCC und in MSVC GetCCDLL
möglicherweise beschädigt wird._Z8GetCCDLLv
?GetCCDLL@@YAPAUCCDLL_v1@@XZ
Sie können bereits keine statische Verknüpfung mit Ihrer DLL garantieren, da eine mit GCC erstellte DLL keine LIB-Datei erstellt und für die statische Verknüpfung einer DLL in MSVC eine erforderlich ist. Die dynamische Verknüpfung scheint eine viel sauberere Option zu sein, aber das Mangeln von Namen steht Ihnen im Weg: Wenn Sie versuchen, GetProcAddress
den falschen Namen zu verwenden, schlägt der Aufruf fehl und Sie können Ihre DLL nicht verwenden. Dies erfordert ein wenig Hacking, um herumzukommen, und ist ein ziemlich wichtiger Grund, warum das Übergeben von C ++ - Klassen über eine DLL-Grenze eine schlechte Idee ist.
Sie müssen Ihre DLL erstellen und dann die erstellte .def-Datei untersuchen (falls eine erstellt wird; dies hängt von Ihren Projektoptionen ab) oder ein Tool wie Dependency Walker verwenden, um den verstümmelten Namen zu finden. Anschließend müssen Sie Ihre eigene .def-Datei schreiben und einen nicht verschränkten Alias für die entstellte Funktion definieren. Verwenden wir als Beispiel die GetCCDLL
Funktion, die ich etwas weiter oben erwähnt habe. Auf meinem System funktionieren die folgenden .def-Dateien für GCC bzw. MSVC:
GCC:
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVC:
EXPORTS
GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1
Erstellen Sie Ihre DLL neu und überprüfen Sie die exportierten Funktionen erneut. Ein nicht verwickelter Funktionsname sollte darunter sein. Beachten Sie, dass Sie überladene Funktionen nicht auf diese Weise verwenden können : Der Name der nicht verschränkten Funktion ist ein Alias für eine bestimmte Funktionsüberladung, wie durch den entstellten Namen definiert. Beachten Sie außerdem, dass Sie bei jeder Änderung der Funktionsdeklarationen eine neue .def-Datei für Ihre DLL erstellen müssen, da sich die entstellten Namen ändern. Am wichtigsten ist, dass Sie durch Umgehen des Namens Mangling alle Schutzmaßnahmen außer Kraft setzen, die der Linker Ihnen in Bezug auf Inkompatibilitätsprobleme bieten möchte.
Dieser gesamte Vorgang ist einfacher, wenn Sie eine Schnittstelle für Ihre DLL erstellen , da Sie nur eine Funktion zum Definieren eines Alias haben, anstatt für jede Funktion in Ihrer DLL einen Alias erstellen zu müssen. Es gelten jedoch weiterhin die gleichen Einschränkungen.
Klassenobjekte an eine Funktion übergeben
Dies ist wahrscheinlich das subtilste und gefährlichste Problem, das die Übergabe von Cross-Compiler-Daten plagt. Selbst wenn Sie alles andere erledigen, gibt es keinen Standard dafür, wie Argumente an eine Funktion übergeben werden . Dies kann zu subtilen Abstürzen ohne ersichtlichen Grund und ohne einfache Möglichkeit zum Debuggen führen . Sie müssen alle Argumente über Zeiger übergeben, einschließlich Puffer für alle Rückgabewerte. Dies ist ungeschickt und unpraktisch und eine weitere hackige Problemumgehung, die möglicherweise funktioniert oder nicht.
Wenn wir all diese Problemumgehungen zusammenstellen und auf kreativer Arbeit mit Vorlagen und Operatoren aufbauen , können wir versuchen, Objekte sicher über eine DLL-Grenze zu übergeben. Beachten Sie, dass die Unterstützung von C ++ 11 obligatorisch ist, ebenso wie die Unterstützung für #pragma pack
und seine Varianten. MSVC 2013 bietet diese Unterstützung ebenso wie neuere Versionen von GCC und 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)
Die pod
Klasse ist auf jeden grundlegenden Datentyp spezialisiert, sodass dieser int
automatisch umbrochen int32_t
, uint
umbrochen uint32_t
usw. wird. Dies alles geschieht dank der Überlastung =
und der ()
Operatoren hinter den Kulissen . Ich habe den Rest der grundlegenden Typspezialisierungen weggelassen, da sie bis auf die zugrunde liegenden Datentypen fast identisch sind (die bool
Spezialisierung hat ein wenig zusätzliche Logik, da sie in a konvertiert wird int8_t
und dann int8_t
mit 0 verglichen wird, um sie wieder zu konvertieren bool
, aber das ist ziemlich trivial).
Wir können STL-Typen auch auf diese Weise verpacken, obwohl dies ein wenig zusätzliche Arbeit erfordert:
#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)
Jetzt können wir eine DLL erstellen, die diese Pod-Typen verwendet. Zuerst brauchen wir eine Schnittstelle, damit wir nur eine Methode haben, um das Mangeln herauszufinden.
//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();
Dadurch wird lediglich eine grundlegende Schnittstelle erstellt, die sowohl die DLL als auch alle Anrufer verwenden können. Beachten Sie, dass wir einen Zeiger auf a übergeben pod
, nicht auf a pod
selbst. Jetzt müssen wir das auf der DLL-Seite implementieren:
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;
}
Und jetzt implementieren wir die ShowMessage
Funktion:
#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);
}
Nichts Besonderes: Dies kopiert nur das übergebene pod
in ein normales wstring
und zeigt es in einer Messagebox. Immerhin ist dies nur ein POC , keine vollständige Dienstprogrammbibliothek.
Jetzt können wir die DLL erstellen. Vergessen Sie nicht die speziellen .def-Dateien, um die Namensverknüpfung des Linkers zu umgehen. (Hinweis: Die CCDLL-Struktur, die ich tatsächlich erstellt und ausgeführt habe, hatte mehr Funktionen als die hier vorgestellte. Die .def-Dateien funktionieren möglicherweise nicht wie erwartet.)
Nun soll eine EXE die DLL aufrufen:
//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;
}
Und hier sind die Ergebnisse. Unsere DLL funktioniert. Wir haben erfolgreich frühere STL-ABI-Probleme, frühere C ++ ABI-Probleme und frühere Mangling-Probleme erreicht, und unsere MSVC-DLL arbeitet mit einer GCC-EXE.
Zum Schluss, wenn Sie unbedingt müssen C ++ Objekte über DLL Grenzen passieren, das ist , wie Sie es tun. Es ist jedoch garantiert, dass nichts davon mit Ihrem Setup oder dem eines anderen funktioniert. All dies kann jederzeit unterbrochen werden und wird wahrscheinlich am Tag vor der geplanten Veröffentlichung einer größeren Version Ihrer Software unterbrochen. Dieser Weg ist voller Hacks, Risiken und allgemeiner Idiotie, für die ich wahrscheinlich erschossen werden sollte. Wenn Sie diesen Weg gehen, testen Sie bitte mit äußerster Vorsicht. Und wirklich ... mach das einfach überhaupt nicht.