Коротка відповідь на це питання - ні . Оскільки немає стандартного 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 дозволить вам виділити та звільнити пам'ять у цій купі. Важливо, щоб ви не використовували нормальне malloc
/ free
оскільки немає гарантії, що вони працюватимуть так, як ви очікуєте.
Випуски STL
Стандартна бібліотека C ++ має власний набір проблем ABI. Немає гарантії, що даний тип STL викладений однаково в пам’яті, а також немає гарантії того, що даний клас STL має однаковий розмір від однієї реалізації до іншої (зокрема, збірки налагоджень можуть помістити додаткову інформацію про налагодження у заданий тип STL). Тому будь-який контейнер STL повинен бути розпакований на основні типи, перш ніж його передати через межу DLL та перепакувати з іншого боку.
Ім’я mangling
Ваша 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 файл, визначивши безперебійний псевдонім функції mangled. Як приклад, скористаємось GetCCDLL
функцією, про яку я згадав трохи далі. У моїй системі для GCC та MSVC відповідно працюють наступні файли .def:
GCC:
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVC:
EXPORTS
GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1
Перебудуйте DLL, потім перегляньте функції, які він експортує. Серед них має бути ім'я без розминки. Зауважте, що ви не можете використовувати перевантажені функції таким чином : ім'я безперебійної функції є псевдонімом для однієї конкретної перевантаження функції , визначеної керованим іменем. Також зауважте, що вам потрібно буде створювати новий .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, так і будь-яких абонентів, які можуть телефонувати. Зауважте, що ми передаємо вказівник на a 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'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 працює. Ми успішно дійшли до минулих проблем STI ABI, минулих проблем C ++ ABI, минулих проблем керування, і наша MSVC DLL працює з GCC EXE.
На закінчення, якщо ви абсолютно повинні передавати об'єкти C ++ через межі DLL, ось так ви це робите. Однак, нічого з цього не гарантується для роботи з вашими налаштуваннями чи будь-ким іншим. Будь-яке з цього питання може порушитись у будь-який час і, ймовірно, порушиться за день до запланованого випуску програмного забезпечення. Цей шлях переповнений хаками, ризиками та загальною ідіотичністю, яку я, мабуть, мушу зняти. Якщо ви все-таки йдете цим маршрутом, будь ласка, протестуйте з особливою обережністю. І справді ... просто не робіть цього взагалі.