Krótka odpowiedź na to pytanie brzmi : nie rób tego . Ponieważ nie ma standardowego C ++ ABI (binarny interfejs aplikacji, standard wywoływania konwencji, pakowania / wyrównywania danych, rozmiaru typu itp.), Będziesz musiał przejść przez wiele kółek, aby spróbować i narzucić standardowy sposób radzenia sobie z klasami obiekty w programie. Nie ma nawet gwarancji, że zadziała po przejściu przez wszystkie te obręcze, ani też nie ma gwarancji, że rozwiązanie, które działa w jednym wydaniu kompilatora, będzie działać w następnym.
Po prostu utwórz prosty interfejs w C extern "C"
, ponieważ C ABI jest dobrze zdefiniowany i stabilny.
Jeśli naprawdę chcesz przekazać obiekty C ++ przez granicę biblioteki DLL, jest to technicznie możliwe. Oto kilka czynników, które musisz wziąć pod uwagę:
Pakowanie / wyrównanie danych
W ramach danej klasy poszczególne składowe danych są zwykle specjalnie umieszczane w pamięci, aby ich adresy odpowiadały wielokrotności rozmiaru typu. Na przykład int
może być wyrównany do 4-bajtowej granicy.
Jeśli biblioteka DLL została skompilowana za pomocą innego kompilatora niż plik EXE, wersja biblioteki DLL danej klasy może mieć inne opakowanie niż wersja EXE, więc gdy plik EXE przekazuje obiekt klasy do biblioteki DLL, biblioteka DLL może nie mieć prawidłowego dostępu do danego członka danych w tej klasie. Biblioteka DLL będzie próbowała czytać z adresu określonego przez własną definicję klasy, a nie z definicji EXE, a ponieważ żądany element członkowski danych nie jest tam faktycznie przechowywany, powstałyby wartości śmieci.
Możesz obejść ten problem za pomocą #pragma pack
dyrektywy preprocesora, która zmusi kompilator do zastosowania określonego pakietu. Kompilator będzie nadal stosować domyślne pakowanie, jeśli wybierzesz wartość pakietu większą niż ta , którą wybrałby kompilator , więc jeśli wybierzesz dużą wartość pakowania, klasa może nadal mieć różne pakowanie między kompilatorami. Rozwiązaniem jest użycie #pragma pack(1)
, które zmusi kompilator do wyrównywania elementów składowych danych na granicy jednobajtowej (zasadniczo nie będzie stosowane pakowanie). To nie jest dobry pomysł, ponieważ może powodować problemy z wydajnością, a nawet awarie w niektórych systemach. Jednak to będzie zapewnienie spójności w sposób członkowie danych klasie są wyrównane w pamięci.
Zmiana kolejności członków
Jeśli Twoja klasa nie ma układu standardowego , kompilator może zmienić rozmieszczenie elementów członkowskich danych w pamięci . Nie ma standardu, jak to się robi, więc każda zmiana kolejności danych może spowodować niezgodności między kompilatorami. Dlatego przekazywanie danych w obie strony do biblioteki DLL będzie wymagało klas o standardowym układzie.
Konwencja telefoniczna
Istnieje wiele konwencji wywoływania, które może mieć dana funkcja. Te konwencje wywoływania określają sposób przekazywania danych do funkcji: czy parametry są przechowywane w rejestrach czy na stosie? W jakiej kolejności argumenty są umieszczane na stosie? Kto czyści wszystkie argumenty pozostawione na stosie po zakończeniu funkcji?
Ważne jest, aby zachować standardową konwencję wywoływania; jeśli zadeklarujesz funkcję jako _cdecl
, domyślną dla C ++ i spróbujesz wywołać ją używając _stdcall
złych rzeczy . _cdecl
jest jednak domyślną konwencją wywoływania funkcji C ++, więc jest to jedna rzecz, która nie zepsuje się, chyba że celowo ją złamiesz, podając _stdcall
w jednym miejscu a _cdecl
w innym.
Rozmiar typu danych
Zgodnie z tą dokumentacją w systemie Windows większość podstawowych typów danych ma takie same rozmiary, niezależnie od tego, czy aplikacja jest 32-bitowa, czy 64-bitowa. Jednakże, ponieważ rozmiar danego typu danych jest wymuszany przez kompilator, a nie przez żaden standard (wszystkie standardowe gwarancje są takie 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
), dobrym pomysłem jest używanie typów danych o stałym rozmiarze, aby zapewnić zgodność rozmiaru typu danych tam, gdzie to możliwe.
Problemy ze stertą
Jeśli biblioteka DLL łączy się z inną wersją środowiska wykonawczego C niż EXE, oba moduły będą używać różnych stert . Jest to szczególnie prawdopodobny problem, biorąc pod uwagę, że moduły są kompilowane za pomocą różnych kompilatorów.
Aby to złagodzić, cała pamięć będzie musiała zostać przydzielona do współużytkowanej sterty i cofnięta z tej samej sterty. Na szczęście system Windows udostępnia interfejsy API, które pomagają w tym: GetProcessHeap pozwoli ci uzyskać dostęp do sterty EXE hosta, a HeapAlloc / HeapFree pozwoli ci przydzielić i zwolnić pamięć w tej stercie. Ważne jest, aby nie używać normalnego malloc
/, free
ponieważ nie ma gwarancji, że będą działać zgodnie z oczekiwaniami.
Problemy z STL
Biblioteka standardowa C ++ ma własny zestaw problemów ABI. Nie ma gwarancji, że dany typ STL jest umieszczony w pamięci w ten sam sposób, ani nie ma gwarancji, że dana klasa STL ma ten sam rozmiar w różnych implementacjach (w szczególności kompilacje debugowania mogą umieszczać dodatkowe informacje debugowania w danego typu STL). Dlatego każdy kontener STL będzie musiał zostać rozpakowany na podstawowe typy, zanim zostanie przekazany przez granicę biblioteki DLL i przepakowany po drugiej stronie.
Imię zniekształcone
Twoja biblioteka DLL prawdopodobnie wyeksportuje funkcje, które Twój EXE będzie chciał wywołać. Jednak kompilatory C ++ nie mają standardowego sposobu manipulowania nazwami funkcji . Oznacza to, że nazwana funkcja GetCCDLL
może zostać zniekształcona _Z8GetCCDLLv
w GCC i ?GetCCDLL@@YAPAUCCDLL_v1@@XZ
MSVC.
Już nie będziesz w stanie zagwarantować statycznego łączenia z biblioteką DLL, ponieważ biblioteka DLL utworzona za pomocą GCC nie utworzy pliku .lib, a statyczne połączenie biblioteki DLL w MSVC wymaga takiego. Dynamiczne łączenie wydaje się znacznie czystszą opcją, ale GetProcAddress
zniekształcanie nazw przeszkadza : jeśli spróbujesz użyć niewłaściwej zniekształconej nazwy, połączenie zakończy się niepowodzeniem i nie będziesz mógł użyć swojej biblioteki DLL. Wymaga to trochę włamań, aby obejść ten problem i jest dość głównym powodem, dla którego przekazywanie klas C ++ przez granicę DLL jest złym pomysłem.
Będziesz musiał zbudować bibliotekę DLL, a następnie zbadać utworzony plik .def (jeśli taki został utworzony; będzie się to różnić w zależności od opcji projektu) lub użyć narzędzia takiego jak Dependency Walker, aby znaleźć zniekształconą nazwę. Następnie musisz napisać własny plik .def, definiując niezarządzany alias do zniekształconej funkcji. Jako przykład użyjmy GetCCDLL
funkcji, o której wspomniałem nieco dalej. W moim systemie następujące pliki .def działają odpowiednio dla GCC i MSVC:
GCC:
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVC:
EXPORTS
GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1
Odbuduj bibliotekę DLL, a następnie ponownie sprawdź funkcje, które eksportuje. Wśród nich powinna znajdować się niezarządzona nazwa funkcji. Zauważ, że nie możesz używać przeciążonych funkcji w ten sposób : nazwa niezarządzonej funkcji jest aliasem dla jednego określonego przeciążenia funkcji, zdefiniowanego przez zniekształconą nazwę. Zauważ również, że będziesz musiał utworzyć nowy plik .def dla swojej biblioteki DLL za każdym razem, gdy zmieniasz deklaracje funkcji, ponieważ zniekształcone nazwy ulegną zmianie. Co najważniejsze, pomijając zniekształcanie nazwy, zastępujesz wszelkie zabezpieczenia, które konsolidator próbuje ci zaoferować w związku z problemami z niekompatybilnością.
Cały proces jest prostszy, jeśli utworzysz interfejs dla swojej biblioteki DLL, ponieważ będziesz mieć tylko jedną funkcję do zdefiniowania aliasu, zamiast tworzyć alias dla każdej funkcji w bibliotece DLL. Jednak nadal obowiązują te same zastrzeżenia.
Przekazywanie obiektów klas do funkcji
Jest to prawdopodobnie najbardziej subtelny i najniebezpieczniejszy z problemów, które nękają przekazywanie danych między kompilatorami. Nawet jeśli zajmujesz się wszystkim innym, nie ma standardu przekazywania argumentów do funkcji . Może to powodować subtelne awarie bez wyraźnego powodu i bez łatwego sposobu ich debugowania . Będziesz musiał przekazać wszystkie argumenty za pośrednictwem wskaźników, w tym buforów dla wszelkich zwracanych wartości. Jest to niezgrabne i niewygodne, a jednocześnie jest kolejnym hackowym obejściem, które może działać lub nie.
Łącząc wszystkie te obejścia i opierając się na pewnej twórczej pracy z szablonami i operatorami , możemy próbować bezpiecznie przekazywać obiekty przez granicę biblioteki DLL. Zauważ, że obsługa C ++ 11 jest obowiązkowa, podobnie jak obsługa #pragma pack
i jego wariantów; MSVC 2013 oferuje tę obsługę, podobnie jak najnowsze wersje GCC i 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)
pod
Klasa specjalizuje się dla każdego podstawowego typu danych, dzięki czemu int
zostanie automatycznie zawinięte int32_t
, uint
zostaną zawinięte uint32_t
itp Wszystko to odbywa się za kulisami, dzięki przeciążony =
i ()
operatorów. Pominąłem pozostałe specjalizacje typu podstawowego, ponieważ są one prawie całkowicie takie same, z wyjątkiem podstawowych typów danych ( bool
specjalizacja ma trochę dodatkowej logiki, ponieważ jest konwertowana na a, int8_t
a następnie int8_t
jest porównywana z 0, aby przekonwertować z powrotem na bool
, ale to jest dość trywialne).
Możemy również owijać typy STL w ten sposób, chociaż wymaga to trochę dodatkowej pracy:
#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)
Teraz możemy utworzyć bibliotekę DLL, która korzysta z tych typów podów. Najpierw potrzebujemy interfejsu, więc będziemy mieć tylko jedną metodę, aby dowiedzieć się, jak to zrobić.
//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();
To po prostu tworzy podstawowy interfejs, z którego mogą korzystać zarówno biblioteka DLL, jak i wszyscy wywołujący. Zauważ, że przekazujemy wskaźnik do a pod
, a nie do pod
siebie. Teraz musimy to zaimplementować po stronie 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;
}
A teraz zaimplementujmy ShowMessage
funkcję:
#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);
}
Nic nadzwyczajnego: to po prostu kopiuje przekazany pod
plik do normalnego wstring
i wyświetla go w skrzynce wiadomości. W końcu to tylko POC , a nie pełna biblioteka narzędzi.
Teraz możemy zbudować bibliotekę DLL. Nie zapomnij o specjalnych plikach .def do obejścia zniekształcania nazw konsolidatora. (Uwaga: struktura CCDLL, którą faktycznie zbudowałem i uruchomiłem, miała więcej funkcji niż ta, którą tutaj przedstawiam. Pliki .def mogą nie działać zgodnie z oczekiwaniami).
Teraz plik EXE wywoła 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;
}
A oto wyniki. Nasza biblioteka DLL działa. Z powodzeniem dotarliśmy do poprzednich problemów z STL ABI, poprzednich problemów z C ++ ABI, poprzednich problemów z manglingiem, a nasza biblioteka MSVC DLL działa z GCC EXE.
Podsumowując, jeśli absolutnie musisz przekazywać obiekty C ++ przez granice bibliotek DLL, tak to się robi. Jednak nic z tego nie gwarantuje, że będzie działać z twoją lub kimkolwiek innym. Wszystko to może się zepsuć w dowolnym momencie i prawdopodobnie zepsuje się dzień przed zaplanowaną wersją główną oprogramowania. Ta ścieżka jest pełna hacków, zagrożeń i ogólnego idiotyzmu, za które prawdopodobnie powinienem zostać zastrzelony. Jeśli wybierzesz tę trasę, wykonaj test z najwyższą ostrożnością. I naprawdę ... po prostu w ogóle tego nie rób.