Ответ 1
Короткий ответ на этот вопрос не. Поскольку нет стандартного С++ ABI (двоичный интерфейс приложения, стандарт для вызовов, упаковка/выравнивание данных, размер шрифта и т.д.), Вам придется прыгать через множество обручей, чтобы попытаться обеспечить стандартный способ работы с объектами класса в вашей программе. Там даже не гарантируется, что он будет работать после того, как вы перепрыгнете через все эти обручи, и не будет гарантии, что решение, которое работает в одном выпуске компилятора, будет работать в следующем.
Просто создайте простой интерфейс C с помощью extern "C"
, так как C ABI четко определен и устойчив.
Если вы действительно хотите передать объекты С++ через границу DLL, это технически возможно. Вот некоторые из факторов, которые вам придется учитывать:
Упаковка/выравнивание данных
В рамках данного класса отдельные элементы данных обычно будут помещаться в память, поэтому их адреса соответствуют кратному размеру типа. Например, int
может быть выровнена с 4-байтовой границей.
Если ваша DLL скомпилирована с другим компилятором, чем ваш EXE, DLL-версия данного класса может иметь разную упаковку, чем версия EXE, поэтому, когда EXE передает объект класса DLL, DLL может быть неспособна правильно получить доступ к данному элементу данных в этом классе. DLL попытается прочитать по адресу, указанному в его собственном определении класса, а не в определении EXE, и поскольку нужный элемент данных на самом деле не хранится там, будут отображаться значения мусора.
Вы можете обойти это с помощью директивы #pragma pack
препроцессора, которая заставит компилятор применить определенную упаковку. Компилятор по-прежнему будет применять упаковку по умолчанию, если вы выберете значение пакета, большее, чем тот, который был выбран компилятором, поэтому, если вы выберете большое значение упаковки, класс все еще может имеют различную упаковку между компиляторами. Решением для этого является использование #pragma pack(1)
, что заставит компилятор выровнять элементы данных на однобайтовой границе (по существу, упаковка не будет применяться). Это не отличная идея, так как это может привести к проблемам с производительностью или даже к сбоям в определенных системах. Однако это обеспечит согласованность в том, как элементы данных вашего класса выровнены в памяти.
Переопределение членов
Если ваш класс не standard-layout, компилятор может переупорядочить свои элементы данных в памяти. Нет никакого стандарта для того, как это делается, поэтому любое перераспределение данных может вызывать несовместимость между компиляторами. Поэтому передача данных взад и вперед в DLL потребует классов стандартного макета.
Соглашение о вызовах
Существует несколько соглашений о вызовах, которые может иметь данная функция. Эти соглашения вызова определяют, как данные должны передаваться функциям: являются ли параметры, хранящиеся в регистрах или в стеке? Какой порядок аргументов вставляется в стек? Кто очищает любые аргументы, оставшиеся в стеке после завершения функции?
Важно, чтобы вы поддерживали стандартное соглашение о вызове; если вы объявите функцию как _cdecl
, по умолчанию для С++, и попытайтесь вызвать ее с помощью _stdcall
будут происходить плохие вещи. _cdecl
является стандартным вызовом для С++-функций, однако это одно дело, которое не сломается, если вы не намеренно сломаете его, указав _stdcall
в одном месте и _cdecl
в другом.
Размер Datatype
Согласно этой документации, в Windows большинство базовых типов данных имеют одинаковые размеры независимо от того, является ли ваше приложение 32-разрядным или 64-битным. Однако, поскольку размер данного типа данных применяется компилятором, а не каким-либо стандартом (все стандартные гарантии - это 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
), рекомендуется использовать фиксированные типы данных, чтобы обеспечить совместимость по размеру данных.
Проблемы с кучей
Если ваша DLL-ссылка на другую версию среды выполнения C, чем ваш EXE, два модуля будут использовать разные кучи. Это особенно вероятная проблема, учитывая, что модули компилируются с использованием разных компиляторов.
Чтобы смягчить это, вся память должна быть выделена в общую кучу и освобождена от той же кучи. К счастью, Windows предоставляет API-интерфейсы, чтобы помочь с этим: GetProcessHeap позволит вам получить доступ к кучке EXE-узла, а HeapAlloc/HeapFree позволит вам выделять и освобождать память внутри этой кучи. Важно, чтобы вы не использовали обычный malloc
/free
, так как нет гарантии, что они будут работать так, как вы ожидаете.
Проблемы с STL
Стандартная библиотека С++ имеет свой собственный набор проблем ABI. Существует нет гарантии, что данный тип STL выложен таким же образом в памяти, и нет гарантии, что данный класс STL имеет одинаковый размер от одной реализации до другой (в частности, отладочные сборки могут помещать дополнительную отладочную информацию в заданный тип STL). Поэтому любой STL-контейнер должен быть распакован в основные типы, прежде чем проходить через границу DLL и переупаковать с другой стороны.
Название mangling
Ваша DLL, по-видимому, будет экспортировать функции, которые ваш EXE захочет вызывать. Тем не менее, компиляторы С++ не имеют стандартного способа отображения имен функций. Это означает, что функция с именем GetCCDLL
может быть искажена до _Z8GetCCDLLv
в GCC и [email protected]@[email protected]@XZ
в MSVC.
Вы уже не сможете гарантировать статическое связывание с вашей DLL, поскольку DLL, созданная с помощью GCC, не будет создавать .lib файл и статически связывать DLL в MSVC. Динамическое связывание похоже на гораздо более чистый вариант, но наложение имени на вас мешает: если вы попытаетесь GetProcAddress
неправильное искаженное имя, вызов не удастся, и вы не сможете использовать свою DLL. Для этого требуется немного хакерства, и это довольно серьезная причина, по которой передача классов С++ через границу DLL - плохая идея.
Вам нужно будет создать свою DLL, а затем изучить полученный файл .def(если он будет создан, это будет зависеть от ваших параметров проекта) или использовать инструмент, например Dependency Walker, чтобы найти искаженное имя. Затем вам нужно будет написать свой собственный .def файл, определяя неподписанный псевдоним для искаженной функции. В качестве примера, позвольте использовать функцию GetCCDLL
, о которой я упоминал немного дальше. В моей системе для GCC и MSVC работают следующие .def файлы:
GCC:
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVC:
EXPORTS
[email protected]@[email protected]@XZ @1
Восстановите свою DLL, затем переустановите функции, которые она экспортирует. Среди них должно быть неперечисленное имя функции. Обратите внимание, что вы не можете использовать перегруженные функции таким образом: имя unmangled функции является псевдонимом для одной конкретной перегрузки функции, как определено измененным именем. Также обратите внимание, что вам нужно будет создать новый .def файл для вашей DLL каждый раз, когда вы измените объявления функций, так как измененные имена будут изменены. Самое главное, что в обход имени, вы нарушаете любые меры защиты, которые линкер пытается предложить вам в отношении проблем несовместимости.
Весь этот процесс проще, если вы создаете интерфейс для своей DLL, потому что у вас будет только одна функция для определения псевдонима вместо необходимости создайте псевдоним для каждой функции в вашей DLL. Тем не менее, те же предостережения по-прежнему применяются.
Передача объектов класса в функцию
Это, вероятно, самая тонкая и самая опасная проблема, связанная с передачей данных кросс-компилятора. Даже если вы справитесь со всем остальным, нет стандарта для того, как аргументы передаются функции. Это может вызвать тонкие сбои без видимых причин и простой способ их отладки. Вам необходимо передать все аргументы с помощью указателей, включая буферы для любых возвращаемых значений. Это неудобно и неудобно, и это еще одно хакерское обходное решение, которое может работать или не работать.
Объединяя все эти обходные пути и опираясь на некоторую творческую работу с шаблонами и операторами, мы можем попытаться безопасно передать объекты через границу DLL. Обратите внимание, что поддержка С++ 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
имеет немного дополнительной логики, поскольку она преобразуется в 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, использующую эти типы контейнеров. Сначала нам нужен интерфейс, поэтому у нас будет только один метод, чтобы выяснить, что нужно для 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();
Это просто создает базовый интерфейс, который могут использовать как 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
и показывает его в окне сообщений. В конце концов, это всего лишь 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 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, прошлые проблемы с С++ ABI, проблемы с прошлым изменением, а наша MSVC DLL работает с GCC EXE.
В заключение, если вы абсолютно должны передавать объекты С++ через границы DLL, так вы это делаете. Однако ничто из этого не гарантируется для работы с вашей установкой или кем-то еще. Любое из этого может быть разорвано в любое время и, вероятно, сломается за день до того, как ваше программное обеспечение будет иметь большой выпуск. Этот путь полон хаков, рисков и общего идиотизма, за которые я, вероятно, должен быть застрелен. Если вы идете по этому маршруту, проконсультируйтесь с ним с особой осторожностью. И действительно... просто не делай этого вообще.