Як я безпечно передаю об'єкти, особливо STL-об’єкти, до DLL і з нього?


106

Як я передаю об'єкти класу, особливо об'єкти STL, до DLL C ++?

Моя програма повинна взаємодіяти з сторонніми плагінами у вигляді файлів DLL, і я не можу контролювати, з якого компілятора ці плагіни побудовані. Я знаю, що для об'єктів STL немає гарантованого ABI, і я стурбований причиною нестабільності в моєму додатку.


4
Якщо ви говорите про стандартну бібліотеку C ++, то, ймовірно, ви повинні це так назвати. STL може означати різні речі залежно від контексту. (Див. Також stackoverflow.com/questions/5205491/… )
Micha Wiedenmann

Відповіді:


156

Коротка відповідь на це питання - ні . Оскільки немає стандартного 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, ось так ви це робите. Однак, нічого з цього не гарантується для роботи з вашими налаштуваннями чи будь-ким іншим. Будь-яке з цього питання може порушитись у будь-який час і, ймовірно, порушиться за день до запланованого випуску програмного забезпечення. Цей шлях переповнений хаками, ризиками та загальною ідіотичністю, яку я, мабуть, мушу зняти. Якщо ви все-таки йдете цим маршрутом, будь ласка, протестуйте з особливою обережністю. І справді ... просто не робіть цього взагалі.


1
Хм, непогано! Ви зібрали досить гарну колекцію аргументів проти використання стандартних типів c ++ для взаємодії з DLL для Windows і відповідно позначили їх. Ці обмеження ABI не застосовуються для інших ланцюжків інструментів, ніж MSVC. Про це варто навіть згадати ...
πάντα ῥεῖ

12
@DavidHeffernan Правильно. Але це результат декількох тижнів досліджень для мене, тому я вважав, що варто задокументувати те, що я дізнався, щоб іншим не потрібно було робити те саме дослідження і ті самі спроби злому робочого рішення. Тим більше, що це, мабуть, є настільки поширеним питанням.
cf стоїть з Монікою

@ πάνταῥεῖ Ці конкретні обмеження ABI не застосовуються для інших ланцюжків інструментів, ніж MSVC. Це варто навіть згадати ... Я не впевнений, що я правильно це розумію. Чи вказуєте ви, що ці проблеми ABI виключні для MSVC, і, скажімо, DLL, побудований з клангом, буде успішно працювати з EXE, побудованим з GCC? Я трохи розгублений, оскільки це здається суперечливим у всіх моїх дослідженнях ...
cf стоїть з Монікою

@computerfreaker Ні, я кажу, що PE та ELF використовують різні формати ABI ...
πάντα ῥεῖ

3
@computerfreaker Більшість основних компіляторів на C ++ (GCC, Clang, ICC, EDG тощо) слідують за ItaIі C ++ ABI. MSVC не робить. Так, так, ці проблеми ABI значною мірою характерні для MSVC, хоча і не виключно - навіть компілятори C на платформах Unix (і навіть різні версії одного компілятора!) Страждають від менш досконалої сумісності. Вони, як правило, досить близькі, але я не був би здивований, коли виявив, що ти можеш успішно зв’язати DLL-вбудовану DLL із виконаним GCC-файлом.
Стюарт Олсен

17

@computerfreaker написав чудове пояснення того, чому відсутність ABI перешкоджає проходженню об'єктів C ++ через межі DLL в загальному випадку, навіть коли визначення типів знаходяться під контролем користувача і в обох програмах використовується така сама послідовність лексем. (Працюють два випадки: класи стандартного компонування та чисті інтерфейси)

Для типів об'єктів, визначених у стандарті C ++ (включаючи типи, адаптовані з бібліотеки стандартних шаблонів), ситуація набагато гірша. Маркери, що визначають ці типи, НЕ однакові для кількох компіляторів, оскільки стандарт C ++ не передбачає повного визначення типу, лише мінімальні вимоги. Крім того, пошук імен ідентифікаторів, що з'являються у цих визначеннях, не вирішує те саме. Навіть у системах, де є C ++ ABI, спроба поділитися такими типами через межі модулів призводить до масового невизначеного поведінки через порушення Правила одного визначення.

Це те, до чого програмісти Linux не звикли мати справу, тому що libstdc ++ g ++ був де-факто стандартом і практично всі програми використовували його, таким чином задовольняючи ODR. clab's libc ++ порушив це припущення, і тоді C ++ 11 прийшов разом із обов’язковими змінами майже для всіх стандартних типів бібліотеки.

Просто не поділяйте стандартні типи бібліотек між модулями. Це невизначена поведінка.


16

Деякі з відповідей тут роблять проходження класів C ++ справді жахливим, але я хотів би поділитися альтернативною точкою зору. Чистий віртуальний метод C ++, згаданий в деяких інших відповідях, насправді виявляється чистішим, ніж ви могли подумати. Я створив цілу систему плагінів навколо концепції, і вона працює дуже добре роками. У мене є клас "PluginManager", який динамічно завантажує dlls з визначеного каталогу, використовуючи LoadLib () та GetProcAddress () (і еквіваленти Linux, тому виконуваний файл, щоб зробити його крос-платформою).

Вірите чи ні, цей спосіб прощає, навіть якщо ви робите якісь дурні речі, такі як додавання нової функції в кінці вашого чистого віртуального інтерфейсу і намагаєтеся завантажити файли, складені проти інтерфейсу, без цієї нової функції - вони завантажуватимуться чудово. Звичайно ... вам доведеться перевірити номер версії, щоб переконатися, що ваш виконуваний файл викликає лише нову функцію для нових dll, які реалізують цю функцію. Але гарна новина: це працює! Таким чином, у вас є сильний метод для розвитку вашого інтерфейсу з часом.

Ще одна цікава річ про чисті віртуальні інтерфейси - ви можете успадкувати стільки інтерфейсів, скільки хочете, і ніколи не зіткнетеся з проблемою алмазів!

Я б сказав, що найбільшим недоліком цього підходу є те, що ви повинні бути дуже обережними щодо того, які типи передаєте як параметри. Ні класів, ні STL-об'єктів, не спочатку обертаючи їх чистими віртуальними інтерфейсами. Жодних структур (не проходячись через вуду з прагмою). Просто примітивні типи та покажчики на інші інтерфейси. Крім того, ви не можете перевантажувати функції, що є незручністю, але не є пробкою.

Хороша новина полягає в тому, що за допомогою декількох рядків коду ви можете створювати багаторазові загальні класи та інтерфейси, щоб обгортати рядки, вектори та інші контейнерні класи STL. Крім того, ви можете додати такі функції у свій інтерфейс, як GetCount () та GetVal (n), щоб люди перебирали списки.

Люди, що створюють плагіни для нас, знаходять це досить просто. Вони не повинні бути експертами з межі ABI або будь-чого іншого - вони просто успадковують цікаві їм інтерфейси, кодують функції, які вони підтримують, і повертають помилкові для тих, яких вони не мають.

Наскільки я знаю, технологія, яка змушує цю роботу не базуватись на будь-якому стандарті. З того, що я збираю, Microsoft вирішила зробити свої віртуальні таблиці таким чином, щоб вони могли зробити COM, а інші автори компіляторів вирішили наслідувати його. Сюди входять GCC, Intel, Borland та більшість інших основних компіляторів C ++. Якщо ви плануєте використовувати неясний вбудований компілятор, такий підхід, ймовірно, не буде працювати для вас. Теоретично будь-яка компанія-компілятор могла в будь-який час змінити свої віртуальні таблиці і розбити речі, але враховуючи величезну кількість кодів, написаних роками, що залежить від цієї технології, я був би дуже здивований, якби хтось із основних гравців вирішив посісти рейтинг.

Тож мораль історії полягає в тому, що ... За винятком кількох екстремальних обставин, вам потрібна одна людина, відповідальна за інтерфейси, які зможуть переконатися, що межа ABI залишається в чистоті від примітивних типів і уникає перевантажень. Якщо ви добре з цим умовою, я б не побоявся поділитися інтерфейсами для класів у DLL / SO між компіляторами. Спільний доступ до класів == проблема, але обмін чистими віртуальними інтерфейсами не так вже й поганий.


Це хороший момент ... Я повинен був сказати "Не бійтеся ділитися інтерфейсами на класи". Я відредагую свою відповідь.
Ph0t0n

2
Гей, це чудова відповідь, дякую! На мій погляд, це було б ще кращим: це деякі посилання на подальше читання, які показують приклади речей, про які ви згадуєте (або навіть якийсь код) - наприклад, для обгортання класів STL тощо. Інакше я читаю ця відповідь, але тоді я трохи розгубився, як виглядатимуть ці речі і як їх шукати.
Ela782

8

Ви не можете безпечно передавати STL-об’єкти через межі DLL, якщо всі модулі (.EXE та .DLL) не побудовані з однаковою версією компілятора C ++ та тими ж налаштуваннями та ароматами CRT, що дуже обмежує, і, очевидно, це не ваш випадок.

Якщо ви хочете відкрити об'єктно-орієнтований інтерфейс з DLL, вам слід відкрити чисті інтерфейси C ++ (що схоже на те, що робить COM). Розгляньте цю цікаву статтю про CodeProject:

HowTo: Експорт класів C ++ з DLL

Ви також можете розглянути можливість відкриття чистого інтерфейсу С на кордоні DLL, а потім побудувати обгортку C ++ на сайті виклику.
Це схоже на те, що відбувається в Win32: Код реалізації Win32 майже є C ++, але багато API Win32 розкривають чистий інтерфейс С (є також API, що розкривають інтерфейси COM). Потім ATL / WTL і MFC обертають ці чисті інтерфейси C класами та об'єктами C ++.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.