Краткий ответ на этот вопрос - нет . Поскольку нет стандартного C ++ ABI (двоичный интерфейс приложения, стандарт для соглашений о вызовах, упаковки / выравнивания данных, размера типа и т. Д.), Вам придется перепрыгнуть через множество обручей, чтобы попытаться применить стандартный способ работы с классом. объекты в вашей программе. Нет даже гарантии, что он будет работать после того, как вы прыгнете через все эти обручи, и нет гарантии, что решение, работающее в одной версии компилятора, будет работать в следующей.
Просто создайте простой C - интерфейс с использованием extern "C"
, так как C ABI является четко определенной и стабильной.
Если вы действительно, действительно хотите передать объекты C ++ через границу DLL, это технически возможно. Вот некоторые из факторов, которые вам необходимо учитывать:
Упаковка / выравнивание данных
Внутри данного класса отдельные члены данных обычно специально помещаются в память, чтобы их адреса соответствовали кратному размеру типа. Например, int
можно выровнять по 4-байтовой границе.
Если ваша DLL скомпилирована с помощью другого компилятора, чем ваш EXE, версия DLL данного класса может иметь другую упаковку, чем версия EXE, поэтому, когда EXE передает объект класса в DLL, DLL может быть не в состоянии должным образом получить доступ к данный член данных в этом классе. DLL будет пытаться читать с адреса, указанного в ее собственном определении класса, а не в определении EXE, и, поскольку требуемый элемент данных на самом деле там не хранится, это приведет к мусорным значениям.
Вы можете обойти это с помощью #pragma pack
директивы препроцессора, которая заставит компилятор применить определенную упаковку. Компилятор по-прежнему будет применять упаковку по умолчанию, если вы выберете значение пакета больше, чем то, которое выбрал бы компилятор , поэтому, если вы выберете большое значение упаковки, класс по-прежнему может иметь различную упаковку между компиляторами. Решением для этого является использование #pragma pack(1)
, которое заставит компилятор выровнять элементы данных по однобайтовой границе (по сути, упаковка не будет применяться). Это не лучшая идея, так как это может вызвать проблемы с производительностью или даже сбои в некоторых системах. Тем не менее, она будет обеспечивать согласованность в том , как члены данных вашего класса выравниваются в памяти.
Изменение порядка участников
Если ваш класс не является стандартным , компилятор может переупорядочить его элементы данных в памяти . Не существует стандарта того, как это делается, поэтому любое изменение порядка данных может вызвать несовместимость между компиляторами. Следовательно, для передачи данных в DLL и обратно потребуются классы стандартного макета.
Соглашение о вызове
У данной функции может быть несколько соглашений о вызовах . Эти соглашения о вызовах определяют, как данные должны передаваться в функции: параметры хранятся в регистрах или в стеке? В каком порядке аргументы помещаются в стек? Кто очищает все аргументы, оставшиеся в стеке после завершения функции?
Важно поддерживать стандартное соглашение о вызовах; если вы объявите функцию как _cdecl
значение по умолчанию для C ++ и попытаетесь вызвать ее с использованием _stdcall
, произойдут неприятности . _cdecl
однако это соглашение о вызовах по умолчанию для функций C ++, поэтому это одна вещь, которая не сломается, если вы намеренно не нарушите ее, указав _stdcall
в одном месте, а _cdecl
в другом.
Размер типа данных
Согласно этой документации , в Windows большинство основных типов данных имеют одинаковые размеры независимо от того, является ли ваше приложение 32-битным или 64-битным. Однако, поскольку размер данного типа данных определяется компилятором, а не каким-либо стандартом (все стандартные гарантии заключаются в том 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
), рекомендуется использовать типы данных фиксированного размера, чтобы гарантировать совместимость размеров данных там, где это возможно.
Проблемы с кучей
Если ваша DLL связана с другой версией среды выполнения C, чем ваш EXE, два модуля будут использовать разные кучи . Это особенно вероятная проблема, учитывая, что модули компилируются разными компиляторами.
Чтобы смягчить это, вся память должна быть выделена в общую кучу и освобождена из той же кучи. К счастью, Windows предоставляет API, чтобы помочь с этим: GetProcessHeap позволит вам получить доступ к куче EXE хоста, а HeapAlloc / HeapFree позволит вам выделить и освободить память в этой куче. Важно, чтобы вы не использовали normal malloc
/, free
поскольку нет гарантии, что они будут работать так, как вы ожидаете.
Проблемы с STL
Стандартная библиотека C ++ имеет собственный набор проблем с ABI. Нет гарантии, что данный тип STL размещен в памяти одинаковым образом, и нет гарантии, что данный класс STL имеет одинаковый размер от одной реализации к другой (в частности, отладочные сборки могут помещать дополнительную отладочную информацию в данный тип STL). Следовательно, любой контейнер STL должен быть распакован на основные типы перед передачей через границу DLL и переупаковкой на другой стороне.
Изменение имени
Ваша DLL предположительно будет экспортировать функции, которые ваш EXE захочет вызывать. Однако компиляторы C ++ не имеют стандартного способа изменять имена функций . Это означает, GetCCDLL
что указанная функция может быть искажена _Z8GetCCDLLv
в GCC и ?GetCCDLL@@YAPAUCCDLL_v1@@XZ
MSVC.
Вы уже не сможете гарантировать статическое связывание с вашей DLL, поскольку DLL, созданная с помощью GCC, не будет создавать файл .lib, а для статического связывания DLL в MSVC он требуется. Динамическое связывание кажется намного более чистым вариантом, но искажение имени мешает вам: если вы попытаетесь GetProcAddress
указать неправильное искаженное имя, вызов завершится неудачно, и вы не сможете использовать свою DLL. Это требует небольшого взлома, и это довольно важная причина, по которой передача классов C ++ через границу DLL - плохая идея.
Вам нужно будет создать свою DLL, а затем изучить созданный файл .def (если он создан; это будет зависеть от параметров вашего проекта) или использовать такой инструмент, как Dependency Walker, чтобы найти искаженное имя. Затем вам нужно будет написать свой собственный файл .def, определяющий несвязанный псевдоним для искаженной функции. В качестве примера давайте воспользуемся GetCCDLL
функцией, о которой я упоминал чуть позже. В моей системе следующие файлы .def работают для GCC и MSVC соответственно:
GCC:
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVC:
EXPORTS
GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1
Перестройте свою DLL, а затем еще раз проверьте экспортируемые функции. Среди них должно быть имя функции без запутывания. Обратите внимание , что вы не можете использовать перегруженные функции этого пути : имя unmangled функции является псевдонимом для одной конкретной перегрузки функции , как это определенно искаженным именем. Также обратите внимание, что вам нужно будет создавать новый файл .def для вашей DLL каждый раз, когда вы меняете объявления функций, поскольку измененные имена будут меняться. Что наиболее важно, обходя искажение имен, вы отменяете любые меры защиты, которые компоновщик пытается предложить вам в отношении проблем несовместимости.
Весь этот процесс будет проще, если вы создадите интерфейс для своей DLL, так как у вас будет только одна функция, для которой нужно определить псевдоним, вместо того, чтобы создавать псевдоним для каждой функции в вашей DLL. Однако все же действуют те же предостережения.
Передача объектов класса в функцию
Это, вероятно, самая тонкая и самая опасная проблема, мешающая передаче данных кросс-компилятора. Даже если вы обрабатываете все остальное, не существует стандарта того, как аргументы передаются функции . Это может вызвать незначительные сбои без видимой причины и непростого способа их отладки . Вам нужно будет передать все аргументы через указатели, включая буферы для любых возвращаемых значений. Это неуклюже и неудобно, и это еще один хакерский обходной путь, который может работать, а может и не работать.
Собрав воедино все эти обходные пути и опираясь на творческую работу с шаблонами и операторами , мы можем попытаться безопасно передавать объекты через границу DLL. Обратите внимание, что поддержка C ++ 11 является обязательной, как и поддержка #pragma pack
и его варианты; MSVC 2013 предлагает эту поддержку, как и последние версии GCC и 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
Класс специализирован для всех основных типов данных, так что int
автоматически завернуть к int32_t
, uint
будет обернуто в uint32_t
и т.д. Это все происходит за кулисами, благодаря перегруженным =
и ()
операторам. Я пропустил остальные специализации базовых типов, поскольку они почти полностью идентичны, за исключением базовых типов данных ( bool
специализация имеет немного дополнительной логики, поскольку она преобразуется в a, int8_t
а затем int8_t
сравнивается с 0 для преобразования обратно в bool
, но это довольно тривиально).
Мы также можем обернуть типы STL таким образом, хотя это требует небольшой дополнительной работы:
#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)
Теперь мы можем создать DLL, которая использует эти типы модулей. Во-первых, нам нужен интерфейс, поэтому у нас будет только один метод, для которого нужно разобраться.
//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();
Это просто создает базовый интерфейс, который может использовать как DLL, так и любой вызывающий объект. Обратите внимание, что мы передаем указатель на объект pod
, а не на pod
сам объект . Теперь нам нужно реализовать это на стороне 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;
}
А теперь реализуем ShowMessage
функцию:
#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);
}
Ничто не слишком фантазии: это только копии переданная pod
в нормальную wstring
и показывает , что в MessageBox. В конце концов, это просто POC , а не полная служебная библиотека.
Теперь мы можем создать DLL. Не забудьте о специальных файлах .def, чтобы обойти искажение имени компоновщика. (Примечание: структура CCDLL, которую я фактически построил и запустил, имеет больше функций, чем та, которую я представляю здесь. Файлы .def могут работать не так, как ожидалось.)
Теперь EXE для вызова 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;
}
И вот результаты. Наша DLL работает. Мы успешно справились с прошлыми проблемами STL ABI, прошлыми проблемами C ++ ABI, прошлыми проблемами искажения, и наша DLL MSVC работает с GCC EXE.
В заключение, если вам абсолютно необходимо передавать объекты C ++ через границы DLL, вы это делаете именно так. Однако ничто из этого не гарантирует работы ни с вашей, ни с чьей-либо системой. Любое из этого может сломаться в любое время и, вероятно, сломается за день до того, как запланирован основной выпуск вашего программного обеспечения. Этот путь полон взломов, рисков и общего идиотизма, за который, наверное, стоит пристрелить. Если вы все же пойдете по этому пути, пожалуйста, проверяйте с особой осторожностью. И действительно ... просто не делай этого вообще.