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 int
pode 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 pack
diretiva 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 _stdcall
em um lugar e um _cdecl
em 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
/ free
pois 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 GetCCDLL
pode ser mutilada _Z8GetCCDLLv
no GCC e ?GetCCDLL@@YAPAUCCDLL_v1@@XZ
no 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 GetProcAddress
o 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 GetCCDLL
funçã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 pack
e 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 pod
classe é especializada para todos os tipos de dados básicos, de forma que int
serão automaticamente agrupados int32_t
, uint
serão agrupados uint32_t
etc. 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 bool
especialização tem um pouco de lógica extra, uma vez que é convertida em a int8_t
e, 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 pod
ele 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 ShowMessage
funçã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 pod
em um normal wstring
e 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.
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.