【问题标题】:Loading classes from DLL that was exported by another compiler从另一个编译器导出的 DLL 加载类
【发布时间】:2014-06-30 17:09:47
【问题描述】:

如果我们打算使用来自不同编译器的 DLL,我必须向我的团队解释为什么从 DLL 导出类不是一个好的解决方案。但我找不到证明。标准中是否有诸如“编译器不应该提供向后兼容性,不同的编译器也可以实现自己的命名导出符号的风格,因此从 DLL 导出的类可以由同一个编译器使用”? 我知道这是真的,但我怎么能证明呢?另外,如果您知道我的其他论点,请帮忙!

【问题讨论】:

  • 最大的问题是CRT不兼容。使用 Visual Studio,每个编译器版本(甚至像 Release/Debug 这样的配置)都有一个独立的堆。在 1 个堆中分配内存的问题你不能在第二个堆中安全地释放它。来自不同堆的空闲内存块通常会导致堆损坏,这可能看起来是随机的,因为应用程序可能不会在堆损坏后立即崩溃。使用 .dll 除非您隔离内存分配/解除分配,否则将所有分配/解除分配保持在同一个堆中并不容易。
  • 如果库提供删除函数,或者对于类,如果导出的类可访问抛出函数(即分配将在 DLL 端)并具有析构函数,则可以解决独立堆的问题,不是吗?
  • 看看this list,几乎所有的命中都是由导出非纯类造成的脆弱紧密耦合引起的问题。

标签: c++ class dll compiler-construction export


【解决方案1】:

这里有几点您可能会觉得有趣/有用,可以告诉您的团队。

  • 不同的编译器会以不同的方式处理 C++ 名称。这个简单的名称修改问题可以通过显式 .def 文件来规避。

  • 需要正确编译器选项(-mms-bitfields,...)的不同结构对齐问题。

  • 底层异常和内存模型的根本冲突:

    • MSVC DLL 中的 new/delete 或 malloc/free 无法与 Cygwin newlib new/delete 或 malloc/free 协作。根本无法释放使用不同的 new/malloc 在函数中分配的空间。

    • 由 MSVC DLL 引发的异常不会被 Cygwin 可执行文件捕获,反之亦然。

    • 慢速 GNU SJLJ 异常模型(用于 GCC-3.x 及更早版本)与 MSVC++ 模型兼容,但新的 DWARF2 模型(将由 GCC-4.x 使用) , 将不兼容。

here 给出了更完整的解释,我从那里无耻地复制了上面的内容。看到您使用 MinGW,这应该非常相关。

另外,如果您还没有这样做,请查看this SO question 的讨论。

【讨论】:

  • 1) 不能提供.def 文件让任何编译器避免命名问题吗? 2) #pragma pack 可以避免的结构对齐,我们已经有了我们的标准,所以,这不是反对导出类的论据。 3) 管理由drescherjm 慷慨描述的堆,可以通过正确的删除器和reglament 避免:在DLL 中分配的所有内容都必须在那里销毁。恐怕这也不是反对出口类的论据。
  • 4) 从 DLL 中引发异常也是一个很好的论据,但并不完全反对类的导出。不过,您的帖子对我非常有用,非常感谢您的解释和链接。
  • @Arkady:不客气。如今,编译器在相互交流方面做得比几年前要好得多,所以很多问题都可以抵消——但你需要知道你在做什么以及你和你的团队(当前和未来)有足够的知识和谨慎,以避免任何陷阱。
  • 谢谢。我需要反对导出类的论据,或者,如果最终没有论据并且这是个好主意 - 理解这一点:-)
【解决方案2】:

不同的编译器使用不同的运行时库。 std::stringstd::vector<int>std::shared_ptr 都有不同的实现。类有不同的布局(对齐和打包只是布局的一部分)。如果在任何地方甚至只有一个 inline 函数,以及在需要分配内存时创建对象时,关于此布局的假设都会被纳入两个模块。

C++ 有一个定义规则是有原因的,一旦你开始混合编译器,甚至不同的编译器选项,就会违反该规则。

以下是可以安全共享的内容:

  • 标准布局的纯数据类,只要您小心地将解除分配器与分配器匹配即可。你不能跨越图书馆的界限。此外,两侧的包装也需要设置相同。
  • 纯虚拟基类,仅包含纯虚拟实例成员函数。没有实例数据,也没有静态的任何东西。接口之间没有继承。并且只有具有实现接口的具体子类的模块才允许在接口指针之间进行转换。

请注意,这些都不需要dllexport。只需包含具有相同打包选项的相同头文件就足够了,因为函数是通过 v-table 找到的,而不是通过 DLL 导入。

当然,您还需要从 DLL 导出与 C 兼容的全局函数,以引导对象的创建。对于那些__declspec(dllexport) 将被使用,或者一个模块定义文件。我还推荐extern "C" 用于这些全球出口。

我强烈建议您阅读“组件对象模型”(COM),它有各种其他名称,例如 DCOM、ActiveX 等。您不需要使用所有相同的机制,它们非常复杂,但至少尝试了解“仅与虚函数的接口”和“具有自释放的引用计数”以及“对象本身遍历继承图”方案如何提供混合不同语言和编译器的能力。 (旁注:COM 的存在是仅 v-table 类兼容的原因,因为 COM 是用于 v-table 布局的 Windows 标准。它与 Windows 与 ABI 一样接近。)

【讨论】:

  • 谢谢,这正是我平时所做的,在我不得不创建一些COM接口之后,我总是导出finctions和纯接口,因为它是安全的并且可以在不同的编译器之间处理(不同的gcc和至少是 VS 版本,它已经过测试)。当我们需要使用不同的编译器时,我正在寻找反对类导出的论据,如果在这种情况下类导出是个坏主意?
  • @Arkady:这取决于您所说的“类”是什么意思。我列出了两个避免所有麻烦的子案例。但是,如果导出任何具有数据成员的非标准布局类,编译器将不会就布局达成一致,并且在尝试访问同一数据成员时将使用不同的偏移量。我相信你明白这会造成什么破坏。
  • @Arkady:本质上,导出 C 兼容的数据类型和全局函数是安全的。这些全局函数可以在内部使用 C++。由于 COM 布局要求,纯虚拟接口(在 Windows 上)与指向函数指针表的指针是 C 兼容的。其他任何事情都会中断。如果一个std::string implementation 使用小字符串优化,另一个在字符缓冲区中使用长度前缀,另一个将长度存储在std::string 对象本身内怎么办?但是任何标准库类都存在这个问题,以及你自己的任何类型
  • @Arkady:100% 坏了。你有#include <vector>,两个编译器找到了两个不同的头文件vectorGame over. 在定义要共享的类类型时,您不能使用在系统路径中找到的任何头文件。
  • 其实我之前的评论应该说“在定义要共享的类类型时,不能使用任何编译器提供的头文件”。 <windows.h>之类的操作系统头文件都可以,只要所有编译器都使用官方的(我认为cygwin gcc没有)。
【解决方案3】:

嗯.. 我以为只有扩展的 dll 才能加载 c++ 类 而扩展的 dll 是用于 MFC 的,所以只有 Visual Studio 才能让我猜到...

【讨论】:

  • 我通常导出函数和接口而不是类,但我记得,我在项目中导出了一次使用 MinGW32(gcc4.8.1) 的类。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-08-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-16
  • 1970-01-01
相关资源
最近更新 更多