这个问题的简短答案是don't。因为没有标准的C++ABI https://stackoverflow.com/questions/2171177/what-is-application-binary-interface-abi(应用程序二进制接口、调用约定、数据打包/对齐、类型大小等的标准),您将不得不跳过很多障碍来尝试并强制执行处理程序中的类对象的标准方法。甚至不能保证在您跳过所有这些麻烦后它会起作用,也不能保证在一个编译器版本中有效的解决方案在下一个编译器版本中也能有效。
只需使用以下命令创建一个普通的 C 接口extern "C"
,由于 C ABIis明确且稳定。
如果你真的,really想要跨 DLL 边界传递 C++ 对象,这在技术上是可行的。以下是您必须考虑的一些因素:
数据打包/对齐
在给定的类中,各个数据成员通常会专门放置在内存中,因此它们的地址对应于类型大小的倍数。例如,一个int
可能与 4 字节边界对齐。
如果您的 DLL 是使用与 EXE 不同的编译器编译的,则给定类的 DLL 版本可能与 EXE 版本具有不同的打包,因此当 EXE 将类对象传递给 DLL 时,DLL 可能无法正确访问该类中的给定数据成员。 DLL 将尝试从其自己的类定义(而不是 EXE 的定义)指定的地址读取数据,并且由于所需的数据成员实际上并未存储在那里,因此将产生垃圾值。
您可以使用以下方法解决此问题#pragma pack https://stackoverflow.com/a/3318475/2245528预处理器指令,这将强制编译器应用特定的打包。如果您选择的包值大于编译器选择的值,编译器仍将应用默认打包 https://stackoverflow.com/questions/22754240/always-same-effect-of-pragma-pack16-and-pragma-pack8,因此,如果您选择较大的打包值,一个类在编译器之间仍然可以具有不同的打包。解决这个问题的方法是使用#pragma pack(1)
,这将强制编译器在一字节边界上对齐数据成员(本质上,不会应用任何打包)。这不是一个好主意,因为它可能会导致性能问题甚至在某些系统上崩溃。然而,它will确保类的数据成员在内存中对齐方式的一致性。
会员重新排序
如果你的班级不是标准布局 http://en.cppreference.com/w/cpp/types/is_standard_layout,编译器可以重新排列内存中的数据成员 https://stackoverflow.com/questions/11340028/order-of-storage-inside-a-structure-object。对于如何完成此操作没有标准,因此任何数据重新排列都可能导致编译器之间的不兼容。因此,将数据来回传递到 DLL 将需要标准布局类。
调用约定
有多个调用约定 http://msdn.microsoft.com/en-us/library/984x0h58.aspx给定的函数可以有。这些调用约定指定如何将数据传递给函数:参数是存储在寄存器中还是堆栈中?参数按什么顺序压入堆栈?函数完成后谁清理堆栈上剩余的参数?
保持标准的调用约定很重要;如果你将一个函数声明为_cdecl
,C++ 的默认值,并尝试使用它来调用它_stdcall
坏事将会发生 https://stackoverflow.com/questions/3404372/stdcall-and-cdecl/12465133#12465133. _cdecl
然而,这是 C++ 函数的默认调用约定,因此这是不会破坏的一件事,除非您故意通过指定_stdcall
在一个地方和一个_cdecl
在另一个。
数据类型大小
根据本文档 https://stackoverflow.com/questions/3404372/stdcall-and-cdecl/12465133#12465133,在 Windows 上,无论您的应用程序是 32 位还是 64 位,大多数基本数据类型都具有相同的大小。但是,由于给定数据类型的大小是由编译器强制执行的,而不是由任何标准强制执行的(所有标准保证是1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)
),这是一个好主意,使用固定大小的数据类型 http://en.cppreference.com/w/cpp/types/integer尽可能确保数据类型大小兼容性。
堆问题
如果您的 DLL 链接到的 C 运行时版本与 EXE 不同,这两个模块将使用不同的堆 https://stackoverflow.com/questions/10820114/do-statically-linked-dlls-use-a-different-heap-than-the-main-program。鉴于模块是使用不同的编译器编译的,这是一个特别可能出现的问题。
为了缓解这种情况,所有内存都必须分配到共享堆中,并从同一堆中释放。幸运的是,Windows 提供了 API 来帮助解决此问题:获取进程堆 http://msdn.microsoft.com/en-us/library/windows/desktop/aa366569(v=vs.85).aspx将允许您访问主机 EXE 的堆,并且堆分配 http://msdn.microsoft.com/en-us/library/windows/desktop/aa366597(v=vs.85).aspx/HeapFree http://msdn.microsoft.com/en-us/library/windows/desktop/aa366701(v=vs.85).aspx将允许您在此堆中分配和释放内存。重要的是你不要使用正常的malloc
/free
因为无法保证它们会按照您期望的方式工作。
STL问题
C++ 标准库有其自己的一组 ABI 问题。有没有保证 https://stackoverflow.com/questions/20646989/is-it-safe-to-return-stdwstring-from-a-dll给定的 STL 类型在内存中以相同的方式布局,也不保证给定的 STL 类从一个实现到另一个实现具有相同的大小(特别是,调试版本可能会将额外的调试信息放入给定的 STL 类型中) 。因此,任何 STL 容器在穿过 DLL 边界并在另一端重新打包之前都必须先解包为基本类型。
名称修改
您的 DLL 可能会导出您的 EXE 想要调用的函数。然而,C++编译器没有修改函数名称的标准方法 https://stackoverflow.com/questions/10127982/why-is-name-mangling-not-standardized。这意味着一个名为GetCCDLL
可能会被损坏_Z8GetCCDLLv
在海湾合作委员会和?GetCCDLL@@YAPAUCCDLL_v1@@XZ
在 MSVC 中。
您已经无法保证静态链接到 DLL,因为用 GCC 生成的 DLL 不会生成 .lib 文件,而在 MSVC 中静态链接 DLL 则需要一个。动态链接似乎是一个更干净的选择,但名称修改会妨碍您:如果您尝试GetProcAddress http://msdn.microsoft.com/en-us/library/windows/desktop/ms683212(v=vs.85).aspx错误的损坏名称,调用将失败并且您将无法使用 DLL。这需要一些技巧才能解决,并且是为什么跨 DLL 边界传递 C++ 类是一个坏主意的一个相当重要的原因。
您需要构建 DLL,然后检查生成的 .def 文件(如果生成了;这将根据您的项目选项而有所不同)或使用 Dependency Walker 等工具来查找损坏的名称。然后,你需要写你的own.def 文件,定义重整函数的未重整别名。作为示例,让我们使用GetCCDLL
我在前面提到过的功能。在我的系统上,以下 .def 文件分别适用于 GCC 和 MSVC:
GCC:
EXPORTS
GetCCDLL=_Z8GetCCDLLv @1
MSVC:
EXPORTS
GetCCDLL=?GetCCDLL@@YAPAUCCDLL_v1@@XZ @1
重建 DLL,然后重新检查它导出的函数。未损坏的函数名称应该在其中。请注意,您不能以这种方式使用重载函数:未损坏的函数名称是别名一种特定的函数重载由损坏的名称定义。另请注意,每次更改函数声明时,您都需要为 DLL 创建一个新的 .def 文件,因为损坏的名称将会更改。最重要的是,通过绕过名称修改,您将覆盖链接器试图为您提供的有关不兼容问题的任何保护。
如果您这样做,整个过程会更简单创建一个界面 https://stackoverflow.com/a/318137/2245528以便您的 DLL 遵循,因为您只需为一个函数定义别名,而不需要为 DLL 中的每个函数创建别名。然而,同样的警告仍然适用。
将类对象传递给函数
这可能是困扰交叉编译器数据传递的最微妙和最危险的问题。就算你处理好其他一切对于如何将参数传递给函数没有标准 https://stackoverflow.com/questions/22676050/is-there-a-standard-procedure-for-passing-class-objects-by-value。这可能会导致没有明显原因的微妙崩溃,也没有简单的方法来调试它们 https://stackoverflow.com/questions/22594530/memory-corruption-with-getprocessheap-heapalloc。你需要通过all通过指针的参数,包括任何返回值的缓冲区。这是笨拙和不方便的,也是另一种可能有效也可能无效的解决方法。
将所有这些解决方法放在一起并在此基础上构建使用模板和运算符进行一些创造性的工作 https://stackoverflow.com/a/3565808/2245528,我们可以尝试安全地跨 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)
The pod
类专门针对每种基本数据类型,因此int
将自动换行至int32_t
, uint
将被包装到uint32_t
等等。这一切都发生在幕后,这要归功于超载=
and ()
运营商。我省略了其余的基本类型专业化,因为除了底层数据类型(bool
专业化有一点额外的逻辑,因为它被转换为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)
现在我们可以创建一个使用这些 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
, not a 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 https://en.wikipedia.org/wiki/Proof_of_concept,不是一个完整的实用程序库。
现在我们可以构建 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 问题、过去的损坏问题,并且我们的 MSVC DLL 正在与 GCC EXE 配合使用。
总之,如果你绝对must跨 DLL 边界传递 C++ 对象,这就是您的做法。但是,这些都不能保证适用于您或其他任何人的设置。其中任何一个都可能随时中断,并且可能会在您的软件计划发布主要版本的前一天中断。这条道路充满了黑客、风险和一般的愚蠢行为,我可能应该被枪杀。如果您确实走这条路,请极其谨慎地进行测试。真的……根本不要这样做。