这个问题的简短回答是不要。因为没有标准的 C++ ABI(应用程序二进制接口、调用约定、数据打包/对齐、类型大小等的标准),您将不得不跳过很多圈子来尝试执行标准的处理方式在你的程序中使用类对象。甚至不能保证在您跳过所有这些障碍之后它会起作用,也不能保证在一个编译器版本中工作的解决方案在下一个版本中也能工作。
只需使用extern "C" 创建一个纯 C 接口,因为 C ABI 定义明确且稳定。
如果您真的,真的想要跨 DLL 边界传递 C++ 对象,这在技术上是可行的。以下是您必须考虑的一些因素:
数据打包/对齐
在给定的类中,单个数据成员通常会专门放置在内存中,因此它们的地址对应于类型大小的倍数。例如,int 可能与 4 字节边界对齐。
如果您的 DLL 是使用与您的 EXE 不同的编译器编译的,则给定类的 DLL 版本可能与 EXE 的版本具有不同的打包,因此当 EXE 将类对象传递给 DLL 时,DLL 可能无法正确访问该类中的给定数据成员。 DLL 会尝试从它自己的类定义指定的地址读取,而不是 EXE 的定义,并且由于所需的数据成员实际上并未存储在那里,因此会产生垃圾值。
您可以使用#pragma pack 预处理器指令解决此问题,这将强制编译器应用特定的打包。 The compiler will still apply default packing if you select a pack value bigger than the one the compiler would have chosen,所以如果你选择一个大的打包值,一个类在编译器之间仍然可以有不同的打包。对此的解决方案是使用#pragma pack(1),这将强制编译器在一个字节的边界上对齐数据成员(本质上,不会应用打包)。 这不是一个好主意,因为它可能会导致性能问题甚至在某些系统上崩溃。但是,它将确保类的数据成员对齐方式的一致性记忆。
会员重新排序
如果你的类不是standard-layout,编译器can rearrange its data members in memory。这是如何完成的没有标准,因此任何数据重新排列都可能导致编译器之间的不兼容。因此,将数据来回传递给 DLL 将需要标准布局类。
调用约定
一个给定的函数可以有多个calling conventions。这些调用约定指定如何将数据传递给函数:参数是存储在寄存器中还是堆栈中?参数压入堆栈的顺序是什么?函数完成后谁清理堆栈上的所有参数?
保持标准调用约定很重要;如果您将函数声明为 _cdecl(C++ 的默认值),并尝试使用 _stdcall bad things will happen 调用它。但是,_cdecl 是 C++ 函数的默认调用约定,因此这是不会破坏的一件事,除非您故意在一处指定 _stdcall 并在另一处指定 _cdecl 来破坏它。
数据类型大小
根据this documentation,在 Windows 上,无论您的应用是 32 位还是 64 位,大多数基本数据类型都具有相同的大小。但是,由于给定数据类型的大小由编译器强制执行,而不是任何标准(所有标准保证都是 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)),因此最好使用 fixed-size datatypes 来确保尽可能兼容数据类型大小。
堆问题
如果您的 DLL 链接到的 C 运行时版本与您的 EXE 不同,the two modules will use different heaps。考虑到模块是使用不同的编译器编译的,这是一个特别可能出现的问题。
为了缓解这种情况,所有内存都必须分配到一个共享堆中,并从同一个堆中释放。幸运的是,Windows 提供了 API 来帮助解决这个问题:GetProcessHeap 将允许您访问主机 EXE 的堆,HeapAlloc/HeapFree 将允许您在此堆中分配和释放内存。 请务必不要使用普通的malloc/free,因为无法保证它们会按您预期的方式工作。
STL 问题
C++ 标准库有自己的一组 ABI 问题。 no guarantee 给定的 STL 类型在内存中的布局方式相同,也不能保证给定的 STL 类在一个实现之间具有相同的大小(特别是,调试构建可能会将额外的调试信息放入给定的 STL 类型)。因此,任何 STL 容器都必须在通过 DLL 边界并在另一端重新打包之前解压缩为基本类型。
名称修改
您的 DLL 可能会导出您的 EXE 想要调用的函数。但是,C++ 编译器do not have a standard way of mangling function names。这意味着在 GCC 中名为 GetCCDLL 的函数可能会被修改为 _Z8GetCCDLLv,在 MSVC 中可能会被修改为 ?GetCCDLL@@YAPAUCCDLL_v1@@XZ。
您已经无法保证静态链接到您的 DLL,因为使用 GCC 生成的 DLL 不会生成 .lib 文件,而在 MSVC 中静态链接 DLL 需要一个。动态链接似乎是一个更简洁的选项,但名称修改会妨碍您:如果您尝试 GetProcAddress 错误的修改名称,调用将失败并且您将无法使用您的 DLL。这需要一点技巧来解决,这也是为什么跨 DLL 边界传递 C++ 类是一个坏主意的一个重要原因。
您需要构建您的 DLL,然后检查生成的 .def 文件(如果已生成;这将根据您的项目选项而有所不同)或使用 Dependency Walker 之类的工具来查找损坏的名称。然后,您需要编写自己的 自己的 .def 文件,为损坏的函数定义一个未损坏的别名。例如,让我们使用我在前面提到的GetCCDLL 函数。在我的系统上,以下 .def 文件分别适用于 GCC 和 MSVC:
海合会:
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVC:
EXPORTS
GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1
重建您的 DLL,然后重新检查它导出的函数。一个未修改的函数名称应该在其中。 请注意,您不能以这种方式使用重载函数:未损坏的函数名称是一个特定的函数重载的别名,由损坏的名称定义。另请注意,每次更改函数声明时都需要为 DLL 创建一个新的 .def 文件,因为损坏的名称会更改。最重要的是,通过绕过名称修改,您将覆盖链接器试图为您提供的有关不兼容问题的任何保护。
如果你 create an interface 让你的 DLL 跟随,整个过程会更简单,因为你只需要一个函数来定义别名,而不需要为 DLL 中的每个函数创建别名。但是,同样的警告仍然适用。
将类对象传递给函数
这可能是困扰交叉编译器数据传递的最微妙和最危险的问题。即使您处理其他所有事情,there's no standard for how arguments are passed to a function。这可能会导致subtle crashes with no apparent reason and no easy way to debug them。您需要通过指针传递 all 参数,包括任何返回值的缓冲区。这是笨拙且不方便的,并且是另一种可能有效也可能无效的解决方法。
将所有这些解决方法放在一起并在some creative work with templates and operators 的基础上进行构建,我们可以尝试安全地跨 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 特化有一点额外的逻辑,因为它先转换为 int8_t,然后是 @987654374 @ 与 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)
现在我们可以创建一个使用这些 pod 类型的 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 并在消息框中显示它。毕竟,这只是一个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 问题、过去的 mangling 问题,并且我们的 MSVC DLL 正在使用 GCC EXE。
总之,如果您绝对必须跨 DLL 边界传递 C++ 对象,那么您就是这样做的。但是,这些都不能保证适用于您的设置或其他任何人的设置。其中任何一个都可能随时中断,并且可能会在您的软件计划发布主要版本的前一天中断。这条路充满了我可能应该被枪杀的黑客、风险和一般的白痴。如果您确实走这条路,请格外小心地进行测试。真的......根本不要这样做。