Как безопасно передавать объекты, особенно объекты STL, в и из библиотеки DLL?

106

Как передать объекты класса, особенно объекты STL, в C ++ DLL и обратно?

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

CF стоит с Моникой
источник
4
Если вы говорите о стандартной библиотеке C ++, то вам, вероятно, следует ее так называть. STL может означать разные вещи в зависимости от контекста. (См. Также stackoverflow.com/questions/5205491/… )
Миша Виденманн,

Ответы:

157

Краткий ответ на этот вопрос - нет . Поскольку нет стандартного 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@@XZMSVC.

Вы уже не сможете гарантировать статическое связывание с вашей 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, вы это делаете именно так. Однако ничто из этого не гарантирует работы ни с вашей, ни с чьей-либо системой. Любое из этого может сломаться в любое время и, вероятно, сломается за день до того, как запланирован основной выпуск вашего программного обеспечения. Этот путь полон взломов, рисков и общего идиотизма, за который, наверное, стоит пристрелить. Если вы все же пойдете по этому пути, пожалуйста, проверяйте с особой осторожностью. И действительно ... просто не делай этого вообще.

CF стоит с Моникой
источник
1
Хм, неплохо! Вы выделили довольно хороший набор аргументов против использования стандартных типов C ++ для взаимодействия с Windows DLL и пометили их соответствующим образом. Эти конкретные ограничения ABI не применяются для других инструментальных средств, кроме MSVC. Об этом нужно даже упомянуть ...
πάντα ῥεῖ 01
12
@DavidHeffernan Верно. Но для меня это результат нескольких недель исследований, поэтому я подумал, что было бы целесообразно задокументировать то, что я узнал, чтобы другим не нужно было проводить такое же исследование и те же попытки совместно взломать рабочее решение. Тем более, что здесь это, кажется, частый вопрос.
cf стоит с Моникой
@ πάνταῥεῖ Эти конкретные ограничения ABI не применяются для других инструментальных цепочек, кроме MSVC. Об этом надо даже упомянуть ... Не уверен, что правильно понимаю. Вы указываете, что эти проблемы с ABI являются исключительными для MSVC, и, скажем, DLL, созданная с помощью clang, будет успешно работать с EXE, созданным с помощью GCC? Я немного сбит с толку, так как это кажется противоречащим всем моим исследованиям ...
ср. С Моникой
@computerfreaker Нет, я говорю, что PE и ELF используют разные форматы ABI ...
πάντα ῥεῖ 01
3
@computerfreaker Большинство основных компиляторов C ++ (GCC, Clang, ICC, EDG и т. д.) соответствуют Itanium C ++ ABI. MSVC этого не делает. Так что да, эти проблемы с ABI в значительной степени специфичны для MSVC, хотя и не исключительно - даже компиляторы C на платформах Unix (и даже разные версии одного и того же компилятора!) Страдают от несовершенной совместимости. Однако они обычно достаточно близки, поэтому я не удивлюсь, обнаружив, что вы можете успешно связать DLL, созданную с помощью Clang, с исполняемым файлом, созданным с помощью GCC.
Стюарт Олсен
17

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

Для типов объектов, определенных в стандарте C ++ (включая адаптированные из стандартной библиотеки шаблонов), ситуация намного хуже. Токены, определяющие эти типы, НЕ являются одинаковыми для разных компиляторов, поскольку стандарт C ++ не предоставляет полного определения типа, а только минимальные требования. Кроме того, поиск имен идентификаторов, которые появляются в этих определениях типов, не разрешает то же самое. Даже в системах, где есть C ++ ABI, попытка совместного использования таких типов через границы модуля приводит к массовому неопределенному поведению из-за нарушений правила одного определения.

Это то, с чем программисты Linux не привыкли иметь дело, потому что libstdc ++ g ++ был стандартом де-факто, и практически все программы его использовали, таким образом удовлетворяя требованиям ODR. libc ++ clang сломал это предположение, а затем пришел C ++ 11 с обязательными изменениями почти для всех типов стандартных библиотек.

Просто не делитесь типами стандартных библиотек между модулями. Это неопределенное поведение.

Бен Фойгт
источник
16

Некоторые ответы здесь делают прохождение классов C ++ действительно пугающим, но я хотел бы поделиться альтернативной точкой зрения. Чистый виртуальный метод C ++, упомянутый в некоторых других ответах, на самом деле оказывается чище, чем вы думаете. Я построил целую систему плагинов вокруг этой концепции, и она работает очень хорошо в течение многих лет. У меня есть класс «PluginManager», который динамически загружает библиотеки DLL из указанного каталога с помощью LoadLib () и GetProcAddress () (и эквивалентов Linux, так что исполняемый файл делает его кроссплатформенным).

Вы не поверите, но этот метод простителен, даже если вы делаете какие-то дурацкие вещи, например, добавляете новую функцию в конец своего чистого виртуального интерфейса и пытаетесь загрузить библиотеки DLL, скомпилированные для интерфейса без этой новой функции - они загрузятся нормально. Конечно ... вам нужно будет проверить номер версии, чтобы убедиться, что ваш исполняемый файл вызывает новую функцию только для новых dll, которые реализуют эту функцию. Но есть и хорошие новости: это работает! Таким образом, у вас есть грубый метод развития интерфейса с течением времени.

Еще одна интересная особенность чистых виртуальных интерфейсов - вы можете унаследовать столько интерфейсов, сколько захотите, и вы никогда не столкнетесь с проблемой ромба!

Я бы сказал, что самым большим недостатком этого подхода является то, что вы должны быть очень осторожны с тем, какие типы вы передаете в качестве параметров. Никаких классов или объектов STL без предварительного обертывания их чистыми виртуальными интерфейсами. Никаких структур (без прохождения прагмы пак вуду). Просто примитивные типы и указатели на другие интерфейсы. Кроме того, нельзя перегружать функции, что является неудобством, но не препятствием.

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

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

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

Итак, мораль этой истории такова ... За исключением нескольких крайних обстоятельств, вам нужен один человек, отвечающий за интерфейсы, который может следить за тем, чтобы граница ABI оставалась чистой с примитивными типами и избегала перегрузки. Если вы согласны с этим условием, то я не побоюсь делиться интерфейсами с классами в библиотеках DLL / SO между компиляторами. Совместное использование классов напрямую == проблема, но совместное использование чистых виртуальных интерфейсов не так уж и плохо.

Ph0t0n
источник
Это хороший момент ... Я должен был сказать: «Не бойтесь делиться интерфейсами с классами». Отредактирую свой ответ.
Ph0t0n
2
Эй, это отличный ответ, спасибо! На мой взгляд, это могло бы сделать его еще лучше, это были бы ссылки на дальнейшее чтение, в которых показаны некоторые примеры упомянутых вами вещей (или даже некоторый код) - например, для упаковки классов STL и т. Д. В противном случае я читаю этот ответ, но тогда я немного не понимаю, как эти вещи будут выглядеть на самом деле и как их искать.
Ela782 09
8

Вы не можете безопасно передавать объекты STL через границы DLL, если все модули (.EXE и .DLL) не построены с одной и той же версией компилятора C ++ и теми же настройками и разновидностями CRT, что сильно ограничивает и явно не ваш случай.

Если вы хотите предоставить объектно-ориентированный интерфейс из своей библиотеки DLL, вы должны предоставить чистые интерфейсы C ++ (что аналогично тому, что делает COM). Прочтите эту интересную статью о CodeProject:

HowTo: экспорт классов C ++ из библиотеки DLL

Вы также можете рассмотреть возможность предоставления чистого интерфейса C на границе DLL, а затем создания оболочки C ++ на сайте вызывающей стороны.
Это похоже на то, что происходит в Win32: код реализации Win32 - это почти C ++, но многие API Win32 предоставляют чистый интерфейс C (есть также API, которые предоставляют интерфейсы COM). Затем ATL / WTL и MFC оборачивают эти чистые интерфейсы C классами и объектами C ++.

Мистер C64
источник