La risposta breve a questa domanda è non farlo . Poiché non esiste uno standard C ++ ABI (interfaccia binaria dell'applicazione, uno standard per convenzioni di chiamata, impacchettamento / allineamento dei dati, dimensione del tipo, ecc.), Dovrai saltare molti ostacoli per cercare di applicare un modo standard di trattare con la classe oggetti nel programma. Non c'è nemmeno una garanzia che funzionerà dopo aver saltato tutti quei cerchi, né c'è una garanzia che una soluzione che funziona in una versione del compilatore funzionerà nella successiva.
Basta creare una semplice interfaccia C utilizzando extern "C"
, poiché l'ABI C è ben definito e stabile.
Se davvero, davvero vuole passare oggetti C ++ attraverso la frontiera di DLL, è tecnicamente possibile. Ecco alcuni dei fattori che dovrai tenere in considerazione:
Impacchettamento / allineamento dei dati
All'interno di una data classe, i singoli membri di dati vengono solitamente inseriti in memoria in modo speciale in modo che i loro indirizzi corrispondano a un multiplo della dimensione del tipo. Ad esempio, un fileint
potrebbe essere allineato a un limite di 4 byte.
Se la DLL è compilata con un compilatore diverso dal tuo EXE, la versione della DLL di una determinata classe potrebbe avere un impacchettamento diverso rispetto alla versione dell'EXE, quindi quando l'EXE passa l'oggetto della classe alla DLL, la DLL potrebbe non essere in grado di accedere correttamente a un dato membro di dati all'interno di quella classe. La DLL tenterà di leggere dall'indirizzo specificato dalla propria definizione della classe, non dalla definizione dell'EXE, e poiché il membro di dati desiderato non è effettivamente memorizzato lì, risulterebbero valori in Garbage Collector.
È possibile aggirare questo problema utilizzando la #pragma pack
direttiva del preprocessore, che costringerà il compilatore ad applicare un impacchettamento specifico. Il compilatore continuerà ad applicare il pacchetto predefinito se si seleziona un valore di pacchetto più grande di quello che il compilatore avrebbe scelto , quindi se si sceglie un valore di imballaggio grande, una classe può ancora avere un imballaggio diverso tra i compilatori. La soluzione è usare #pragma pack(1)
, che costringerà il compilatore ad allineare i membri dei dati su un limite di un byte (essenzialmente, non verrà applicato alcun impacchettamento). Questa non è una grande idea, in quanto può causare problemi di prestazioni o addirittura arresti anomali su determinati sistemi. Tuttavia, lo farà garantire la coerenza nel modo dati membro della vostra classe sono allineati in memoria.
Riordino dei membri
Se la tua classe non è di layout standard , il compilatore può riorganizzare i suoi membri di dati in memoria . Non esiste uno standard per come eseguire questa operazione, quindi qualsiasi riorganizzazione dei dati può causare incompatibilità tra i compilatori. Il passaggio di dati avanti e indietro a una DLL richiederà pertanto classi di layout standard.
Chiamata convenzione
Esistono più convenzioni di chiamata una determinata funzione può avere. Queste convenzioni di chiamata specificano come i dati devono essere passati alle funzioni: i parametri sono memorizzati nei registri o nello stack? In che ordine vengono inseriti gli argomenti nella pila? Chi ripulisce gli argomenti rimasti in pila al termine della funzione?
È importante mantenere una convenzione di chiamata standard; se dichiari una funzione come _cdecl
, il valore predefinito per C ++ e provi a chiamarlo usando _stdcall
cose cattive accadrà . _cdecl
è la convenzione di chiamata predefinita per le funzioni C ++, tuttavia, questa è una cosa che non si interromperà a meno che tu non la interrompa deliberatamente specificando un _stdcall
in un punto e un _cdecl
in un altro.
Dimensione del tipo di dati
Secondo questa documentazione , su Windows, la maggior parte dei tipi di dati fondamentali ha le stesse dimensioni indipendentemente dal fatto che l'app sia a 32 o 64 bit. Tuttavia, poiché la dimensione di un dato tipo di dati è imposta dal compilatore, non da alcuno standard (tutte le garanzie standard lo sono 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
), è una buona idea usare tipi di dati a dimensione fissa per garantire la compatibilità della dimensione del tipo di dati ove possibile.
Problemi con l'heap
Se la DLL si collega a una versione diversa del runtime C rispetto all'EXE, i due moduli utilizzeranno heap diversi . Questo è un problema particolarmente probabile dato che i moduli vengono compilati con diversi compilatori.
Per mitigare questo problema, tutta la memoria dovrà essere allocata in un heap condiviso e deallocata dallo stesso heap. Fortunatamente, Windows fornisce API per aiutare con questo: GetProcessHeap ti consentirà di accedere all'heap EXE dell'host e HeapAlloc / HeapFree ti consentirà di allocare e liberare memoria all'interno di questo heap. È importante non utilizzare normali malloc
/ free
poiché non vi è alcuna garanzia che funzioneranno come previsto.
Problemi STL
La libreria standard C ++ ha una propria serie di problemi ABI. Non vi è alcuna garanzia che un dato tipo di STL sia disposto allo stesso modo in memoria, né vi è garanzia che una data classe STL abbia la stessa dimensione da un'implementazione all'altra (in particolare, le build di debug possono inserire informazioni di debug aggiuntive in un dato tipo STL). Pertanto, qualsiasi contenitore STL dovrà essere decompresso in tipi fondamentali prima di essere passato attraverso il limite della DLL e reimballato sull'altro lato.
Nome mutilazione
La tua DLL presumibilmente esporterà le funzioni che il tuo EXE vorrà chiamare. Tuttavia, i compilatori C ++ non hanno un modo standard di modificare i nomi delle funzioni . Ciò significa che una funzione denominata GetCCDLL
potrebbe essere alterata _Z8GetCCDLLv
in GCC e ?GetCCDLL@@YAPAUCCDLL_v1@@XZ
in MSVC.
Non sarai già in grado di garantire il collegamento statico alla tua DLL, poiché una DLL prodotta con GCC non produrrà un file .lib e il collegamento statico di una DLL in MSVC ne richiede uno. Il collegamento dinamico sembra un'opzione molto più pulita, ma la manipolazione del nome ti ostacola: se provi con GetProcAddress
il nome alterato sbagliato, la chiamata fallirà e non sarai in grado di usare la tua DLL. Ciò richiede un po 'di pirateria informatica per aggirare, ed è una ragione abbastanza importante per cui passare le classi C ++ attraverso un confine DLL è una cattiva idea.
Dovrai creare la tua DLL, quindi esaminare il file .def prodotto (se ne viene prodotto uno; questo varierà in base alle opzioni del tuo progetto) o utilizzare uno strumento come Dependency Walker per trovare il nome alterato. Quindi, è necessario scrivere il proprio file DEF, che definisce un alias unmangled alla funzione maciullato. Ad esempio, usiamo la GetCCDLL
funzione che ho menzionato un po 'più in alto. Sul mio sistema, i seguenti file .def funzionano rispettivamente per GCC e MSVC:
GCC:
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVC:
EXPORTS
GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1
Ricostruisci la tua DLL, quindi riesamina le funzioni che esporta. Un nome di funzione non confuso dovrebbe essere tra questi. Notare che non è possibile utilizzare le funzioni sovraccaricate in questo modo : il nome della funzione unmangled è un alias per uno specifico overload della funzione come definito dal nome alterato. Si noti inoltre che sarà necessario creare un nuovo file .def per la DLL ogni volta che si modificano le dichiarazioni di funzione, poiché i nomi alterati cambieranno. Ancora più importante, aggirando il nome alterando, stai ignorando qualsiasi protezione che il linker sta cercando di offrirti per quanto riguarda i problemi di incompatibilità.
L'intero processo è più semplice se crei un'interfaccia da seguire per la tua DLL, poiché avrai solo una funzione per la quale definire un alias invece di dover creare un alias per ogni funzione nella tua DLL. Tuttavia, si applicano ancora gli stessi avvertimenti.
Passaggio di oggetti di classe a una funzione
Questo è probabilmente il più sottile e pericoloso dei problemi che affliggono il passaggio di dati tra compilatori. Anche se gestisci tutto il resto, non esiste uno standard per il modo in cui gli argomenti vengono passati a una funzione . Ciò può causare piccoli arresti anomali senza motivo apparente e nessun modo semplice per eseguire il debug . Dovrai passare tutti gli argomenti tramite puntatori, inclusi i buffer per qualsiasi valore restituito. Questo è goffo e scomodo, ed è ancora un'altra soluzione alternativa hacky che può o non può funzionare.
Mettendo insieme tutte queste soluzioni alternative e basandosi su un lavoro creativo con modelli e operatori , possiamo tentare di passare in modo sicuro gli oggetti attraverso un limite DLL. Si noti che il supporto per C ++ 11 è obbligatorio, così come il supporto per #pragma pack
e le sue varianti; MSVC 2013 offre questo supporto, così come le versioni recenti di GCC e 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 è specializzata per ogni tipo di dati di base, quindi int
verrà automaticamente inserito in int32_t
,uint
verrà eseguito il wrapping uint32_t
, ecc. Tutto ciò avviene dietro le quinte, grazie agli operatori =
e sovraccaricati ()
. Ho omesso il resto delle specializzazioni di tipo di base poiché sono quasi completamente le stesse tranne per i tipi di dati sottostanti (la bool
specializzazione ha un po 'di logica in più, poiché viene convertita in a int8_t
e quindi int8_t
viene confrontata con 0 per riconvertirla bool
, ma questo è abbastanza banale).
Possiamo anche avvolgere i tipi STL in questo modo, sebbene richieda un po 'di lavoro extra:
#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)
Ora possiamo creare una DLL che utilizza questi tipi di pod. Per prima cosa abbiamo bisogno di un'interfaccia, quindi avremo solo un metodo per capire il mangling.
//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();
Questo crea solo un'interfaccia di base utilizzabile sia dalla DLL che da qualsiasi chiamante. Nota che stiamo passando un puntatore a un filepod
, non a pod
se stesso. Ora dobbiamo implementarlo sul lato 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;
}
E ora implementiamo il ShowMessage
funzione:
#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);
}
Niente di troppo stravagante: questo copia solo il passato pod
in un normale wstring
e lo mostra in una casella di messaggio. Dopo tutto, questo è solo un POC , non una libreria di utilità completa.
Ora possiamo costruire la DLL. Non dimenticare gli speciali file .def per aggirare la modifica del nome del linker. (Nota: la struttura CCDLL che ho effettivamente costruito ed eseguito aveva più funzioni di quella che presento qui. I file .def potrebbero non funzionare come previsto.)
Ora per un EXE per chiamare 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;
}
Ed ecco i risultati. La nostra DLL funziona. Abbiamo raggiunto con successo i problemi ABI STL precedenti, i problemi ABI C ++ precedenti, i problemi di mangling passati e la nostra DLL MSVC funziona con un EXE GCC.
In conclusione, se è assolutamente necessario passare oggetti C ++ attraverso i confini DLL, questo è come lo fai. Tuttavia, niente di tutto ciò è garantito per funzionare con la tua configurazione o quella di qualcun altro. Tutto ciò potrebbe interrompersi in qualsiasi momento e probabilmente si interromperà il giorno prima che il tuo software abbia una versione principale. Questo percorso è pieno di hack, rischi e idiozia generale per cui probabilmente dovrei essere ucciso. Se segui questa strada, prova con estrema cautela. E davvero ... non farlo affatto.