¿Cómo paso de forma segura objetos, especialmente objetos STL, hacia y desde una DLL?

106

¿Cómo paso objetos de clase, especialmente objetos STL, hacia y desde una DLL de C ++?

Mi aplicación tiene que interactuar con complementos de terceros en forma de archivos DLL, y no puedo controlar con qué compilador están construidos estos complementos. Soy consciente de que no existe una ABI garantizada para los objetos STL y me preocupa causar inestabilidad en mi aplicación.

cf se para con Monica
fuente
4
Si está hablando de la biblioteca estándar de C ++, probablemente debería llamarla así. STL puede significar diferentes cosas según el contexto. (Ver también stackoverflow.com/questions/5205491/… )
Micha Wiedenmann

Respuestas:

156

La respuesta corta a esta pregunta es no . Debido a que no existe una ABI de C ++ estándar (interfaz binaria de aplicación, un estándar para convenciones de llamadas, empaquetado / alineación de datos, tamaño de letra, etc.), tendrá que pasar por muchos obstáculos para intentar hacer cumplir una forma estándar de tratar con la clase. objetos en su programa. Ni siquiera hay garantía de que funcione después de pasar por todos esos aros, ni hay garantía de que una solución que funcione en una versión del compilador funcione en la siguiente.

Simplemente cree una interfaz C simple usando extern "C", ya que C ABI está bien definida y es estable.


Si realmente desea pasar objetos C ++ a través de un límite de DLL, es técnicamente posible. Estos son algunos de los factores que deberá tener en cuenta:

Empaquetado / alineación de datos

Dentro de una clase dada, los miembros de datos individuales generalmente se colocarán especialmente en la memoria para que sus direcciones correspondan a un múltiplo del tamaño del tipo. Por ejemplo, un intpodría estar alineado con un límite de 4 bytes.

Si su DLL se compila con un compilador diferente al de su EXE, la versión de DLL de una clase determinada puede tener un empaque diferente al de la versión de EXE, por lo que cuando el EXE pasa el objeto de clase a la DLL, la DLL podría no poder acceder correctamente a una miembro de datos dado dentro de esa clase. La DLL intentaría leer desde la dirección especificada por su propia definición de la clase, no la definición del EXE, y dado que el miembro de datos deseado no está realmente almacenado allí, resultarían valores basura.

Puede solucionar este problema utilizando la #pragma packdirectiva de preprocesador, que obligará al compilador a aplicar un empaquetado específico. El compilador seguirá aplicando el empaquetado predeterminado si selecciona un valor de paquete mayor que el que habría elegido el compilador , por lo que si elige un valor de empaquetado grande, una clase puede tener un empaquetado diferente entre compiladores. La solución para esto es usar #pragma pack(1), lo que obligará al compilador a alinear los miembros de datos en un límite de un byte (esencialmente, no se aplicará ningún paquete). Esta no es una gran idea, ya que puede causar problemas de rendimiento o incluso fallas en ciertos sistemas. Sin embargo, será garantizar la coherencia en la forma en que los miembros de datos de su clase están alineados en la memoria.

Reordenación de miembros

Si su clase no es de diseño estándar , el compilador puede reorganizar sus miembros de datos en la memoria . No existe un estándar sobre cómo se hace esto, por lo que cualquier reordenamiento de datos puede causar incompatibilidades entre compiladores. Pasar datos de un lado a otro a una DLL requerirá clases de diseño estándar, por lo tanto.

Convención de llamadas

Hay varias convenciones de llamada que puede tener una función determinada. Estas convenciones de llamada especifican cómo se pasarán los datos a las funciones: ¿se almacenan los parámetros en registros o en la pila? ¿En qué orden se introducen los argumentos en la pila? ¿Quién limpia los argumentos que quedan en la pila después de que finaliza la función?

Es importante que mantenga una convención de llamadas estándar; si declaras una función como _cdecl, la predeterminada para C ++, e intentas llamarla usando algo _stdcall malo, sucederán cosas . _cdecles la convención de llamada predeterminada para las funciones de C ++, sin embargo, esto es algo que no se romperá a menos que lo rompa deliberadamente especificando un _stdcallen un lugar y un _cdeclen otro.

Tamaño del tipo de datos

Según esta documentación , en Windows, la mayoría de los tipos de datos fundamentales tienen los mismos tamaños, independientemente de si su aplicación es de 32 bits o de 64 bits. Sin embargo, dado que el compilador impone el tamaño de un tipo de datos dado, no cualquier estándar (todas las garantías estándar son eso 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)), es una buena idea usar tipos de datos de tamaño fijo para garantizar la compatibilidad del tamaño del tipo de datos siempre que sea posible.

Problemas de montón

Si su DLL se vincula a una versión diferente del tiempo de ejecución de C que su EXE, los dos módulos usarán montones diferentes . Este es un problema especialmente probable dado que los módulos se compilan con diferentes compiladores.

Para mitigar esto, toda la memoria deberá asignarse a un montón compartido y desasignarse del mismo montón. Afortunadamente, Windows proporciona API para ayudar con esto: GetProcessHeap le permitirá acceder al montón de EXE del host, y HeapAlloc / HeapFree le permitirá asignar y liberar memoria dentro de este montón. Es importante que no use normal malloc/ freeya que no hay garantía de que funcionen como espera.

Problemas de STL

La biblioteca estándar de C ++ tiene su propio conjunto de problemas de ABI. No hay garantía de que un tipo STL determinado se disponga de la misma manera en la memoria, ni existe garantía de que una clase STL determinada tenga el mismo tamaño de una implementación a otra (en particular, las compilaciones de depuración pueden poner información de depuración adicional en una dado el tipo de STL). Por lo tanto, cualquier contenedor STL deberá descomprimirse en tipos fundamentales antes de pasar a través del límite de DLL y volver a empaquetar en el otro lado.

Destrozar nombre

Su DLL probablemente exportará funciones a las que su EXE querrá llamar. Sin embargo, los compiladores de C ++ no tienen una forma estándar de alterar los nombres de las funciones . Esto significa que una función nombrada GetCCDLLpodría modificarse _Z8GetCCDLLven GCC y ?GetCCDLL@@YAPAUCCDLL_v1@@XZen MSVC.

Ya no podrá garantizar la vinculación estática a su DLL, ya que una DLL producida con GCC no producirá un archivo .lib y vincular estáticamente una DLL en MSVC requiere uno. La vinculación dinámica parece una opción mucho más limpia, pero la alteración de nombres se interpone en su camino: si intenta GetProcAddressutilizar el nombre alterado incorrecto, la llamada fallará y no podrá utilizar su DLL. Esto requiere un poco de piratería para moverse, y es una razón bastante importante por la que pasar clases de C ++ a través de un límite de DLL es una mala idea.

Deberá crear su DLL, luego examinar el archivo .def producido (si se produce uno; esto variará según las opciones de su proyecto) o usar una herramienta como Dependency Walker para encontrar el nombre destrozado. Luego, deberá escribir su propio archivo .def, definiendo un alias sin alterar para la función alterada. Como ejemplo, usemos la GetCCDLLfunción que mencioné un poco más arriba. En mi sistema, los siguientes archivos .def funcionan para GCC y MSVC, respectivamente:

GCC:

EXPORTS
    GetCCDLL=_Z8GetCCDLLv @1

MSVC:

EXPORTS
    GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1

Reconstruya su DLL, luego vuelva a examinar las funciones que exporta. Entre ellos debe figurar un nombre de función sin alterar. Tenga en cuenta que no puede usar funciones sobrecargadas de esta manera : el nombre de la función sin alterar es un alias para una sobrecarga de función específica definida por el nombre alterado. También tenga en cuenta que deberá crear un nuevo archivo .def para su DLL cada vez que cambie las declaraciones de función, ya que los nombres alterados cambiarán. Lo más importante es que al omitir la manipulación de nombres, está anulando cualquier protección que el vinculador esté tratando de ofrecerle con respecto a problemas de incompatibilidad.

Todo este proceso es más simple si crea una interfaz para que la siga su DLL, ya que solo tendrá una función para definir un alias en lugar de tener que crear un alias para cada función en su DLL. Sin embargo, se siguen aplicando las mismas advertencias.

Pasar objetos de clase a una función

Este es probablemente el más sutil y peligroso de los problemas que afectan al paso de datos entre compiladores. Incluso si maneja todo lo demás, no existe un estándar sobre cómo se pasan los argumentos a una función . Esto puede provocar fallos sutiles sin motivo aparente y sin una forma sencilla de depurarlos . Deberá pasar todos los argumentos a través de punteros, incluidos los búferes para los valores devueltos. Esto es torpe e inconveniente, y es otra solución hacky que puede funcionar o no.


Al reunir todas estas soluciones y aprovechar un poco de trabajo creativo con plantillas y operadores , podemos intentar pasar objetos de forma segura a través de un límite de DLL. Tenga en cuenta que la compatibilidad con C ++ 11 es obligatoria, al igual que la compatibilidad con #pragma packsus variantes; MSVC 2013 ofrece este soporte, al igual que las versiones recientes de GCC y 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)

La podclase está especializada para cada tipo de datos básico, por lo que intautomáticamente se ajustará a int32_t, se uintajustará a uint32_t, etc. Todo esto ocurre detrás de escena, gracias a los operadores =y sobrecargados (). He omitido el resto de las especializaciones de tipos básicos, ya que son casi completamente iguales excepto por los tipos de datos subyacentes (la boolespecialización tiene un poco de lógica adicional, ya que se convierte en ay int8_tluego int8_tse compara con 0 para volver a convertir a bool, pero esto es bastante trivial).

También podemos envolver los tipos STL de esta manera, aunque requiere un poco de trabajo adicional:

#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)

Ahora podemos crear una DLL que haga uso de estos tipos de pod. Primero necesitamos una interfaz, por lo que solo tendremos un método para resolver el problema.

//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();

Esto solo crea una interfaz básica que pueden usar tanto la DLL como cualquier persona que llama. Tenga en cuenta que estamos pasando un puntero a a pod, no a podsí mismo. Ahora necesitamos implementar eso en el lado de 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;
}

Y ahora implementemos la ShowMessagefunción:

#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);
}

Nada demasiado sofisticado: esto simplemente copia el paso poda un normal wstringy lo muestra en un cuadro de mensaje. Después de todo, esto es solo un POC , no una biblioteca de utilidades completa.

Ahora podemos construir la DLL. No olvide los archivos .def especiales para evitar la alteración del nombre del vinculador. (Nota: la estructura CCDLL que realmente construí y ejecuté tenía más funciones que la que presento aquí. Es posible que los archivos .def no funcionen como se esperaba).

Ahora para que un EXE llame a la 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;
}

Y aquí están los resultados. Nuestro DLL funciona. Hemos superado con éxito problemas de ABI de STL anteriores, problemas de ABI de C ++ anteriores, problemas de manipulación anteriores y nuestra DLL de MSVC está funcionando con un EXE de GCC.

La imagen que muestra el resultado después.


En conclusión, si absolutamente debe pasar objetos C ++ a través de los límites de DLL, así es como lo hace. Sin embargo, nada de esto está garantizado para funcionar con su configuración o con la de cualquier otra persona. Todo esto puede fallar en cualquier momento y probablemente lo hará el día antes de que su software esté programado para tener una versión principal. Este camino está lleno de trucos, riesgos e idioteces generales por los que probablemente deberían dispararme. Si sigue esta ruta, pruebe con extrema precaución. Y realmente ... simplemente no hagas esto en absoluto.

cf se para con Monica
fuente
1
¡Hmm, no está mal! Obtuvo una colección bastante buena de argumentos en contra del uso de tipos c ++ estándar para interactuar con una DLL de Windows y los etiquetó en consecuencia. Estas restricciones ABI particulares no se aplicarán a otras cadenas de herramientas que no sean MSVC. Esto incluso debería mencionarse ...
πάντα ῥεῖ
12
@DavidHeffernan Correcto. Pero este es el resultado de varias semanas de investigación para mí, así que pensé que valdría la pena documentar lo que he aprendido para que otros no tengan que hacer la misma investigación y esos mismos intentos de hackear una solución que funcione. Tanto más cuanto que esta parece ser una pregunta casi común por aquí.
cf se encuentra con Monica
@ πάνταῥεῖ Estas restricciones ABI particulares no se aplicarán a otras cadenas de herramientas que no sean MSVC. Esto debería incluso mencionarse ... No estoy seguro de haberlo entendido correctamente. ¿Está indicando que estos problemas de ABI son exclusivos de MSVC y, por ejemplo, un archivo DLL creado con clang funcionará correctamente con un EXE creado con GCC? Estoy un poco confundido, ya que eso parece contradictorio con toda mi investigación ...
cf se encuentra con Monica
@computerfreaker No, estoy diciendo que PE y ELF están usando diferentes formatos ABI ...
πάντα ῥεῖ
3
@computerfreaker La mayoría de los principales compiladores de C ++ (GCC, Clang, ICC, EDG, etc.) siguen la ABI de Itanium C ++. MSVC no lo hace. Así que sí, estos problemas de ABI son en gran parte específicos de MSVC, aunque no exclusivamente; incluso los compiladores de C en plataformas Unix (¡e incluso diferentes versiones del mismo compilador!) Sufren de una interoperabilidad menos que perfecta. Por lo general, están lo suficientemente cerca, sin embargo, que no me sorprendería en absoluto descubrir que se puede vincular con éxito una DLL construida por Clang con un ejecutable construido por GCC.
Stuart Olsen
17

@computerfreaker ha escrito una gran explicación de por qué la falta de ABI impide pasar objetos C ++ a través de los límites de DLL en el caso general, incluso cuando las definiciones de tipo están bajo el control del usuario y se usa exactamente la misma secuencia de tokens en ambos programas. (Hay dos casos que funcionan: clases de diseño estándar e interfaces puras)

Para los tipos de objetos definidos en el estándar C ++ (incluidos los adaptados de la biblioteca de plantillas estándar), la situación es mucho, mucho peor. Los tokens que definen estos tipos NO son los mismos en varios compiladores, ya que el estándar C ++ no proporciona una definición de tipo completa, solo requisitos mínimos. Además, la búsqueda de nombres de los identificadores que aparecen en estas definiciones de tipo no resuelve lo mismo. Incluso en sistemas donde hay una ABI de C ++, intentar compartir estos tipos a través de los límites de los módulos da como resultado un comportamiento indefinido masivo debido a violaciones de la regla de una definición.

Esto es algo con lo que los programadores de Linux no estaban acostumbrados a tratar, porque libstdc ++ de g ++ era un estándar de facto y prácticamente todos los programas lo usaban, satisfaciendo así el ODR. libc ++ de clang rompió esa suposición, y luego C ++ 11 llegó con cambios obligatorios en casi todos los tipos de bibliotecas estándar.

Simplemente no comparta tipos de bibliotecas estándar entre módulos. Es un comportamiento indefinido.

Ben Voigt
fuente
16

Algunas de las respuestas aquí hacen que pasar clases de C ++ suene realmente aterrador, pero me gustaría compartir un punto de vista alternativo. El método C ++ virtual puro mencionado en algunas de las otras respuestas en realidad resulta ser más limpio de lo que piensas. Construí un sistema completo de complementos en torno al concepto y ha funcionado muy bien durante años. Tengo una clase "PluginManager" que carga dinámicamente las dlls desde un directorio especificado usando LoadLib () y GetProcAddress () (y los equivalentes de Linux, por lo que el ejecutable lo hace multiplataforma).

Lo crea o no, este método es indulgente incluso si hace algunas cosas extravagantes como agregar una nueva función al final de su interfaz virtual pura e intenta cargar dlls compilados en la interfaz sin esa nueva función; se cargarán bien. Por supuesto ... tendrá que verificar un número de versión para asegurarse de que su ejecutable solo llame a la nueva función para las nuevas DLL que implementan la función. Pero la buena noticia es: ¡funciona! Entonces, de alguna manera, tiene un método burdo para evolucionar su interfaz con el tiempo.

Otra cosa interesante acerca de las interfaces virtuales puras: ¡puedes heredar tantas interfaces como quieras y nunca te encontrarás con el problema del diamante!

Yo diría que la mayor desventaja de este enfoque es que debes tener mucho cuidado con los tipos que pasas como parámetros. Sin clases u objetos STL sin envolverlos primero con interfaces virtuales puras. Sin estructuras (sin pasar por el pragma pack vudú). Solo tipos primitivos y punteros a otras interfaces. Además, no puede sobrecargar funciones, lo cual es un inconveniente, pero no un obstáculo.

La buena noticia es que con un puñado de líneas de código puede crear clases e interfaces genéricas reutilizables para envolver cadenas STL, vectores y otras clases de contenedor. Alternativamente, puede agregar funciones a su interfaz como GetCount () y GetVal (n) para permitir que las personas recorran las listas.

Las personas que crean complementos para nosotros lo encuentran bastante fácil. No tienen que ser expertos en el límite de ABI ni nada; simplemente heredan las interfaces que les interesan, codifican las funciones que admiten y devuelven falso para las que no.

La tecnología que hace que todo esto funcione no se basa en ningún estándar que yo sepa. Por lo que sé, Microsoft decidió hacer sus tablas virtuales de esa manera para que pudieran hacer COM, y otros redactores de compiladores decidieron seguir su ejemplo. Esto incluye GCC, Intel, Borland y la mayoría de los demás compiladores de C ++ importantes. Si está planeando usar un compilador incrustado oscuro, entonces este enfoque probablemente no funcione para usted. Teóricamente, cualquier empresa compiladora podría cambiar sus tablas virtuales en cualquier momento y romper cosas, pero considerando la enorme cantidad de código escrito a lo largo de los años que depende de esta tecnología, me sorprendería mucho si alguno de los principales jugadores decidiera romper el rango.

Entonces, la moraleja de la historia es ... Con la excepción de algunas circunstancias extremas, necesita una persona a cargo de las interfaces que pueda asegurarse de que el límite ABI se mantenga limpio con los tipos primitivos y evite la sobrecarga. Si está de acuerdo con esa estipulación, entonces no tendría miedo de compartir interfaces a clases en DLL / SO entre compiladores. Compartir clases directamente == problemas, pero compartir interfaces virtuales puras no es tan malo.

Ph0t0n
fuente
Ese es un buen punto ... Debería haber dicho "No tengas miedo de compartir interfaces con las clases". Editaré mi respuesta.
Ph0t0n
2
¡Oye, esa es una gran respuesta, gracias! En mi opinión, lo que lo haría aún mejor serían algunos enlaces a lecturas adicionales que muestren algunos ejemplos de las cosas que está mencionando (o incluso algún código), por ejemplo, para envolver las clases STL, etc. De lo contrario, estoy leyendo esta respuesta, pero estoy un poco perdido sobre cómo se verían realmente estas cosas y cómo buscarlas.
Ela782
8

No puede pasar objetos STL de manera segura a través de los límites de DLL, a menos que todos los módulos (.EXE y .DLL) estén construidos con la misma versión del compilador de C ++ y la misma configuración y sabores del CRT, lo cual es muy restrictivo y claramente no es su caso.

Si desea exponer una interfaz orientada a objetos desde su DLL, debe exponer interfaces puras de C ++ (que es similar a lo que hace COM). Considere leer este artículo interesante sobre CodeProject:

Cómo: Exportar clases de C ++ desde una DLL

También puede considerar la posibilidad de exponer una interfaz C pura en el límite de la DLL y luego crear un contenedor C ++ en el sitio de la persona que llama.
Esto es similar a lo que sucede en Win32: el código de implementación de Win32 es casi C ++, pero muchas API de Win32 exponen una interfaz C pura (también hay API que exponen interfaces COM). Luego, ATL / WTL y MFC envuelven estas interfaces C puras con clases y objetos C ++.

Señor C64
fuente