Como faço para passar objetos com segurança, especialmente objetos STL, para e de uma DLL?

106

Como faço para passar objetos de classe, especialmente objetos STL, para e de uma DLL C ++?

Meu aplicativo precisa interagir com plug-ins de terceiros na forma de arquivos DLL e não posso controlar com qual compilador esses plug-ins são criados. Estou ciente de que não há ABI garantida para objetos STL e estou preocupado em causar instabilidade em meu aplicativo.

cf fica com a Monica
fonte
4
Se você está falando sobre a Biblioteca Padrão C ++, então provavelmente deveria chamá-la assim. STL pode significar coisas diferentes dependendo do contexto. (Consulte também stackoverflow.com/questions/5205491/… )
Micha Wiedenmann

Respostas:

156

A resposta curta a esta pergunta é não . Como não há ABI C ++ padrão (interface binária do aplicativo, um padrão para convenções de chamada, empacotamento / alinhamento de dados, tamanho do tipo, etc.), você terá que passar por muitos obstáculos para tentar impor uma maneira padrão de lidar com a classe objetos em seu programa. Não há nem mesmo uma garantia de que funcionará depois de passar por todos esses obstáculos, nem há garantia de que uma solução que funciona em uma versão do compilador funcionará na próxima.

Basta criar uma interface C simples usando extern "C", já que a C ABI é bem definida e estável.


Se você realmente, realmente quer passar objetos C ++ em um limite DLL, é tecnicamente possível. Aqui estão alguns dos fatores que você deve levar em consideração:

Empacotamento / alinhamento de dados

Dentro de uma determinada classe, os membros de dados individuais geralmente serão colocados de maneira especial na memória para que seus endereços correspondam a um múltiplo do tamanho do tipo. Por exemplo, um intpode ser alinhado a um limite de 4 bytes.

Se sua DLL for compilada com um compilador diferente do EXE, a versão da DLL de uma determinada classe pode ter uma embalagem diferente da versão do EXE, portanto, quando o EXE passa o objeto de classe para a DLL, a DLL pode não ser capaz de acessar corretamente um dado membro de dados dentro dessa classe. A DLL tentaria ler a partir do endereço especificado por sua própria definição da classe, não pela definição do EXE, e como o membro de dados desejado não está realmente armazenado lá, resultariam em valores de lixo.

Você pode contornar isso usando a #pragma packdiretiva do pré - processador, que forçará o compilador a aplicar pacotes específicos. O compilador ainda aplicará o empacotamento padrão se você selecionar um valor de empacotamento maior do que aquele que o compilador teria escolhido , portanto, se você escolher um valor de empacotamento grande, uma classe ainda pode ter empacotamento diferente entre os compiladores. A solução para isso é usar o #pragma pack(1), que forçará o compilador a alinhar os membros de dados em um limite de um byte (essencialmente, nenhum empacotamento será aplicado). Esta não é uma boa ideia, pois pode causar problemas de desempenho ou até mesmo travar em determinados sistemas. No entanto, ele irá assegurar a coerência na maneira como os membros de dados do seu classe estão alinhados na memória.

Reordenação de membros

Se sua classe não for de layout padrão , o compilador pode reorganizar seus membros de dados na memória . Não existe um padrão de como isso é feito, portanto, qualquer reorganização de dados pode causar incompatibilidades entre os compiladores. Passar dados de um lado para outro para uma DLL exigirá classes de layout padrão, portanto.

Convenção de chamada

Existem várias convenções de chamada que uma determinada função pode ter. Essas convenções de chamada especificam como os dados devem ser passados ​​para as funções: os parâmetros são armazenados em registradores ou na pilha? Em que ordem os argumentos são colocados na pilha? Quem limpa os argumentos deixados na pilha após o término da função?

É importante que você mantenha uma convenção de chamada padrão; se você declarar uma função como _cdecl, o padrão para C ++, e tentar chamá-la usando _stdcall coisas ruins acontecerão . _cdeclé a convenção de chamada padrão para funções C ++, entretanto, isso é algo que não quebrará, a menos que você deliberadamente o quebre especificando um _stdcallem um lugar e um _cdeclem outro.

Tamanho do tipo de dados

De acordo com esta documentação , no Windows, a maioria dos tipos de dados fundamentais têm os mesmos tamanhos, independentemente de seu aplicativo ser de 32 ou 64 bits. No entanto, como o tamanho de um determinado tipo de dados é imposto pelo compilador, não por qualquer padrão (todas as garantias padrão são 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)), é uma boa ideia usar tipos de dados de tamanho fixo para garantir a compatibilidade do tamanho do tipo de dados sempre que possível.

Problemas de heap

Se sua DLL se vincular a uma versão diferente do tempo de execução C do que seu EXE, os dois módulos usarão heaps diferentes . Este é um problema especialmente provável, visto que os módulos estão sendo compilados com diferentes compiladores.

Para atenuar isso, toda a memória terá que ser alocada em um heap compartilhado e desalocado do mesmo heap. Felizmente, o Windows fornece APIs para ajudar com isso: GetProcessHeap permitirá que você acesse o heap do EXE do host, e HeapAlloc / HeapFree permitirá que você aloque e libere memória dentro desse heap. É importante que você não use o normal malloc/ freepois não há garantia de que funcionará da maneira que você espera.

Problemas de STL

A biblioteca padrão C ++ tem seu próprio conjunto de questões ABI. Não há garantia de que um determinado tipo de STL seja disposto da mesma maneira na memória, nem há garantia de que uma determinada classe de STL tenha o mesmo tamanho de uma implementação para outra (em particular, compilações de depuração podem colocar informações de depuração extras em um determinado tipo de STL). Portanto, qualquer contêiner STL terá que ser descompactado em tipos fundamentais antes de ser passado pelo limite da DLL e reembalado do outro lado.

Nome mutilado

Sua DLL provavelmente exportará funções que seu EXE desejará chamar. No entanto, os compiladores C ++ não possuem uma maneira padrão de alterar os nomes das funções . Isso significa que uma função nomeada GetCCDLLpode ser mutilada _Z8GetCCDLLvno GCC e ?GetCCDLL@@YAPAUCCDLL_v1@@XZno MSVC.

Você já não poderá garantir a vinculação estática à sua DLL, pois uma DLL produzida com GCC não produzirá um arquivo .lib e a vinculação estática de uma DLL no MSVC requer um. Vincular dinamicamente parece uma opção muito mais limpa, mas a mutilação de nomes atrapalha: se você tentar usar GetProcAddresso nome mutilado errado, a chamada falhará e você não conseguirá usar sua DLL. Isso requer um pouco de hackeamento para ser contornado e é um dos principais motivos pelos quais passar classes C ++ através de um limite de DLL é uma má ideia.

Você precisará construir sua DLL e, em seguida, examinar o arquivo .def produzido (se houver; isso irá variar com base nas opções do projeto) ou usar uma ferramenta como Dependency Walker para encontrar o nome mutilado. Em seguida, você precisará escrever seu próprio arquivo .def, definindo um alias não mutilado para a função mutilada. Como exemplo, vamos usar a GetCCDLLfunção que mencionei um pouco mais adiante. No meu sistema, os seguintes arquivos .def funcionam para GCC e MSVC, respectivamente:

GCC:

EXPORTS
    GetCCDLL=_Z8GetCCDLLv @1

MSVC:

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

Reconstrua sua DLL e examine novamente as funções que ela exporta. Um nome de função não fragmentado deve estar entre eles. Observe que você não pode usar funções sobrecarregadas desta forma : o nome da função não fragmentada é um alias para uma sobrecarga de função específica, conforme definido pelo nome mutilado. Observe também que você precisará criar um novo arquivo .def para sua DLL toda vez que alterar as declarações de função, pois os nomes mutilados serão alterados. Mais importante ainda, ao ignorar a mutilação de nome, você está substituindo todas as proteções que o vinculador está tentando oferecer em relação a problemas de incompatibilidade.

Todo esse processo é mais simples se você criar uma interface para sua DLL seguir, já que você terá apenas uma função para definir um alias, em vez de precisar criar um alias para cada função em sua DLL. No entanto, as mesmas advertências ainda se aplicam.

Passar objetos de classe para uma função

Este é provavelmente o mais sutil e mais perigoso dos problemas que afetam a passagem de dados entre compiladores. Mesmo que você lide com todo o resto, não há um padrão de como os argumentos são passados ​​para uma função . Isso pode causar travamentos sutis sem motivo aparente e sem uma maneira fácil de depurá-los . Você precisará passar todos os argumentos por meio de ponteiros, incluindo buffers para quaisquer valores de retorno. Isso é desajeitado e inconveniente e é mais uma solução alternativa de hacky que pode ou não funcionar.


Juntando todas essas soluções alternativas e desenvolvendo algum trabalho criativo com modelos e operadores , podemos tentar passar objetos com segurança através de um limite de DLL. Observe que o suporte a C ++ 11 é obrigatório, assim como o suporte para #pragma packe suas variantes; O MSVC 2013 oferece esse suporte, assim como as versões recentes do GCC e do 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)

A podclasse é especializada para todos os tipos de dados básicos, de forma que intserão automaticamente agrupados int32_t, uintserão agrupados uint32_tetc. Isso tudo ocorre nos bastidores, graças aos operadores =e sobrecarregados (). Omiti o resto das especializações de tipo básico, uma vez que são quase inteiramente iguais, exceto pelos tipos de dados subjacentes (a boolespecialização tem um pouco de lógica extra, uma vez que é convertida em a int8_te, em seguida, int8_té comparada a 0 para converter de volta para bool, mas isso é bastante trivial).

Também podemos agrupar tipos STL dessa maneira, embora exija um pouco de trabalho extra:

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

Agora podemos criar uma DLL que faz uso desses tipos de pod. Primeiro, precisamos de uma interface, portanto, teremos apenas um método para descobrir a mutilação.

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

Isso apenas cria uma interface básica que a DLL e os chamadores podem usar. Observe que estamos passando um ponteiro para a pod, não para podele mesmo. Agora precisamos implementar isso no lado da 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;
}

E agora vamos implementar a ShowMessagefunção:

#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 muito sofisticado: isso apenas copia o passado podem um normal wstringe o mostra em uma caixa de mensagem. Afinal, este é apenas um POC , não uma biblioteca de utilitários completa.

Agora podemos construir a DLL. Não se esqueça dos arquivos .def especiais para contornar a alteração do nome do vinculador. (Observação: a estrutura CCDLL que realmente criei e executei tinha mais funções do que a que apresento aqui. Os arquivos .def podem não funcionar como esperado.)

Agora, para um EXE para chamar a 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;
}

E aqui estão os resultados. Nosso DLL funciona. Alcançamos com sucesso problemas anteriores de STL ABI, anteriores problemas C ++ ABI, anteriores problemas de mutilação e nossa DLL MSVC está funcionando com um EXE GCC.

A imagem que mostra o resultado depois.


Em conclusão, se você absolutamente deve passar objetos C ++ através dos limites de DLL, é assim que você faz. No entanto, nada disso funcionará com a sua configuração ou com a de qualquer outra pessoa. Tudo isso pode falhar a qualquer momento e provavelmente será interrompido no dia anterior ao agendamento de um lançamento principal do software. Este caminho está cheio de hacks, riscos e idiotices gerais pelos quais eu provavelmente deveria ser atirado. Se você seguir esse caminho, teste com extrema cautela. E realmente ... simplesmente não faça isso.

cf fica com a Monica
fonte
1
Hmm, nada mal! Você puxou uma boa coleção de argumentos contra o uso de tipos c ++ padrão para interagir com uma DLL do Windows e marcou de acordo. Essas restrições específicas de ABI não se aplicam a outros conjuntos de ferramentas além do MSVC. Isso deveria ser mencionado ...
πάντα ῥεῖ
12
@DavidHeffernan Certo. Mas isso é o resultado de várias semanas de pesquisa para mim, então pensei que valeria a pena documentar o que aprendi para que outros não precisem fazer a mesma pesquisa e as mesmas tentativas de hackear uma solução que funcione. Ainda mais porque esta parece ser uma questão semicomum por aqui.
cf está com Monica de
@ πάνταῥεῖ Estas restrições específicas de ABI não se aplicam a outras cadeias de ferramentas além do MSVC. Isso deveria ser mencionado ... Não tenho certeza se entendi isso corretamente. Você está indicando que esses problemas de ABI são exclusivos do MSVC e, digamos, uma DLL criada com o clang funcionará com êxito com um EXE criado com o GCC? Estou um pouco confuso, já que isso parece contraditório com todas as minhas pesquisas ...
cf está com Monica em
@computerfreaker Não, estou dizendo que PE e ELF estão usando formatos ABI diferentes ...
πάντα ῥεῖ
3
@computerfreaker A maioria dos principais compiladores C ++ (GCC, Clang, ICC, EDG, etc.) seguem o Itanium C ++ ABI. MSVC não. Então, sim, esses problemas de ABI são amplamente específicos para MSVC, embora não exclusivamente - até mesmo compiladores C em plataformas Unix (e até mesmo versões diferentes do mesmo compilador!) Sofrem de interoperabilidade menos que perfeita. Eles geralmente são próximos o suficiente, entretanto, que eu não ficaria surpreso em descobrir que você poderia vincular com sucesso uma DLL construída pelo Clang com um executável construído pelo GCC.
Stuart Olsen,
17

@computerfreaker escreveu uma ótima explicação de por que a falta de ABI impede a passagem de objetos C ++ pelos limites de DLL em geral, mesmo quando as definições de tipo estão sob controle do usuário e a mesma sequência de token exata é usada em ambos os programas. (Existem dois casos que funcionam: classes de layout padrão e interfaces puras)

Para os tipos de objetos definidos no C ++ Standard (incluindo aqueles adaptados da Standard Template Library), a situação é muito, muito pior. Os tokens que definem esses tipos NÃO são os mesmos em vários compiladores, pois o padrão C ++ não fornece uma definição de tipo completa, apenas os requisitos mínimos. Além disso, a pesquisa de nome dos identificadores que aparecem nessas definições de tipo não resolve o mesmo. Mesmo em sistemas onde há uma ABI C ++, tentar compartilhar esses tipos entre os limites do módulo resulta em um comportamento indefinido massivo devido a violações da regra de definição única.

Isso é algo com que os programadores do Linux não estavam acostumados a lidar, porque o libstdc ++ do g ++ era um padrão de fato e praticamente todos os programas o usavam, satisfazendo assim o ODR. A libc ++ do clang quebrou essa suposição, e então o C ++ 11 veio com mudanças obrigatórias para quase todos os tipos de biblioteca padrão.

Apenas não compartilhe tipos de biblioteca padrão entre os módulos. É um comportamento indefinido.

Ben Voigt
fonte
16

Algumas das respostas aqui tornam a aprovação nas aulas de C ++ realmente assustadora, mas gostaria de compartilhar um ponto de vista alternativo. O método C ++ virtual puro mencionado em algumas das outras respostas, na verdade, acabou sendo mais limpo do que você imagina. Eu construí todo um sistema de plugins em torno do conceito e está funcionando muito bem há anos. Eu tenho uma classe "PluginManager" que carrega dinamicamente as dlls de um diretório especificado usando LoadLib () e GetProcAddress () (e os equivalentes do Linux, portanto, o executável para torná-lo plataforma cruzada).

Acredite ou não, este método é tolerante mesmo se você fizer algumas coisas malucas como adicionar uma nova função no final de sua interface virtual pura e tentar carregar dlls compilados na interface sem essa nova função - eles carregarão muito bem. Claro ... você terá que verificar um número de versão para ter certeza de que seu executável apenas chama a nova função para dlls mais recentes que implementam a função. Mas a boa notícia é: funciona! Então, de certa forma, você tem um método rudimentar para desenvolver sua interface ao longo do tempo.

Outra coisa legal sobre interfaces virtuais puras - você pode herdar quantas interfaces quiser e nunca terá o problema do diamante!

Eu diria que a maior desvantagem dessa abordagem é que você precisa ter muito cuidado com os tipos que passa como parâmetros. Nenhuma classe ou objeto STL sem envolvê-los com interfaces virtuais puras primeiro. Sem structs (sem passar pelo vodu do pacote de pragma). Apenas tipos primários e ponteiros para outras interfaces. Além disso, você não pode sobrecarregar as funções, o que é um inconveniente, mas não um obstáculo.

A boa notícia é que, com um punhado de linhas de código, você pode criar classes e interfaces genéricas reutilizáveis ​​para envolver strings STL, vetores e outras classes de contêiner. Alternativamente, você pode adicionar funções à sua interface como GetCount () e GetVal (n) para permitir que as pessoas percorram as listas.

As pessoas que criam plug-ins para nós acham muito fácil. Eles não precisam ser especialistas no limite ABI ou algo assim - eles apenas herdam as interfaces nas quais estão interessados, codificam as funções que suportam e retornam false para aquelas que não suportam.

A tecnologia que faz todo esse trabalho não se basear em nenhum padrão até onde eu sei. Pelo que percebi, a Microsoft decidiu fazer suas tabelas virtuais dessa forma para que eles pudessem fazer COM, e outros criadores de compiladores decidiram seguir o exemplo. Isso inclui GCC, Intel, Borland e a maioria dos outros grandes compiladores C ++. Se você está planejando usar um compilador incorporado obscuro, essa abordagem provavelmente não funcionará para você. Teoricamente, qualquer empresa de compiladores poderia mudar suas tabelas virtuais a qualquer momento e quebrar coisas, mas considerando a enorme quantidade de código escrito ao longo dos anos que depende dessa tecnologia, eu ficaria muito surpreso se algum dos principais jogadores decidisse quebrar a classificação.

Portanto, a moral da história é ... Com exceção de algumas circunstâncias extremas, você precisa de uma pessoa responsável pelas interfaces que possa garantir que o limite ABI permaneça limpo com tipos primitivos e evite sobrecarga. Se você concordar com essa estipulação, então eu não teria medo de compartilhar interfaces para classes em DLLs / SOs entre compiladores. Compartilhar aulas diretamente == problemas, mas compartilhar interfaces virtuais puras não é tão ruim.

Ph0t0n
fonte
Esse é um bom ponto ... Eu deveria ter dito "Não tenha medo de compartilhar interfaces com classes". Vou editar minha resposta.
Ph0t0n
2
Ei, essa é uma ótima resposta, obrigado! O que o tornaria ainda melhor, na minha opinião, seriam alguns links para leituras adicionais que mostram alguns exemplos das coisas que você está mencionando (ou até mesmo algum código) - por exemplo, para agrupar as classes STL, etc. Caso contrário, estou lendo essa resposta, mas estou um pouco perdido em como essas coisas realmente seriam e como pesquisá-las.
Ela782
8

Você não pode transmitir objetos STL com segurança através dos limites de DLL, a menos que todos os módulos (.EXE e .DLLs) sejam construídos com a mesma versão do compilador C ++ e as mesmas configurações e sabores do CRT, o que é altamente restritivo e claramente não é o seu caso.

Se você deseja expor uma interface orientada a objetos de sua DLL, você deve expor interfaces puras C ++ (que é semelhante ao que COM faz). Considere a leitura deste artigo interessante sobre CodeProject:

HowTo: Exportar classes C ++ de uma DLL

Você também pode querer expor uma interface C pura no limite da DLL e, em seguida, construir um wrapper C ++ no site do chamador.
Isso é semelhante ao que acontece no Win32: o código de implementação do Win32 é quase C ++, mas muitas APIs do Win32 expõem uma interface C pura (também há APIs que expõem interfaces COM). Em seguida, ATL / WTL e MFC envolvem essas interfaces C puras com classes e objetos C ++.

Mr.C64
fonte