4

给定:可执行文件使用 dll。他们有不同的 c/c++ 运行时。它们之间的接口存在哪些限制?除了他们使用相同的编译器,相同的 Boost 版本(但不同的预编译 boost 库)。

我知道不同的运行时可以有不同的堆。因此,delete 必须对应于同一堆中的 new。

最重要的是,我们不能通过接口 STL 对象传递,因为当我们构建 exe STL 对象时与一个运行时链接,而在构建 dll 时,相同的对象(如果我们通过引用或通过接口复制传递它)将与另一个运行时链接。另一个运行时可以具有该对象的不同实现。

让我们考虑案例:

  1. 我认为以下是安全的。具有参数的 Dll 导出函数:对包含私有 STL 类作为成员的导出用户定义类的引用。Dll 为这个对象分配内存。Exe 在要删除该对象时调用该对象的 Release 方法。

  2. 我认为以下内容并不安全。用户定义的类在 exe 中实例化并通过 exe/dll 接口传递。此类包含私有 STL 类作为成员。exe 和 dll 共享此用户类的头文件/实现文件。当此类构建在不同的项目中时,将使用不同的 STL 实现。例如 string::size() 的不同实现(来自不同的运行时)将应用于内存中的同一对象。

  3. 我认为以下是安全的。用户定义的类在 exe 中实例化并通过 exe/dll 接口传递。此类不依赖于标准库,它仅使用原始 C++ 类型。exe 和 dll 共享此用户类的头文件/实现文件。此外,我们必须控制 new 和 delete 对应于同一个堆。例如,我们可以重载 new/delete,以便它们使用 ::GetProcessHeap。

  4. 我认为以下是不安全的:通过 exe/dll 接口传递 boost 对象,因为它们可以依赖于标准库类。也删除可能不对应于新的堆。

  5. 我认为以下内容是不安全的:即使我们通过 exe/dll 接口传递 boost 对象并且它们不依赖于标准库类但不仅作为标头实现 - 也可以使用一个 boost lib 创建对象(对于一个运行时) 并与另一个 boost lib 一起使用(用于另一个运行时)。也删除可能不对应于新的堆。

此外,我想使用某种智能指针将对象的引用(在第 3 项中提到)从 exe 传递到 dll 以及从 dll 到 exe。我认为这个智能指针也应该重载 new/delete 以从默认进程堆中分配引用计数器。当它将尝试删除指向的对象时,它将调用由该对象重载的删除(如在 item3 中)

对于第 1 项中的对象,我想使用自定义智能指针,它将调用指向对象的释放方法(如 boost::shared_ptr 和自定义释放)

哪些问题没有被提及?请纠正我。

4

1 回答 1

4

您的问题的一般答案是:如果实际执行的操作不依赖于运行时,则对从 EXE/DLL 接收的对象执行操作是安全的。例如 vtable 调用、函数指针调用、来自 DLL 的显式函数调用。

依赖于运行时的东西(来自头文件的内联方法,任何关于 STL 对象布局的假设等)都是不安全的。

回到你的例子:

  1. 如果您调用 Release() 方法,您应该小心并确保您将从 DLL 调用 Release() 的实现,而不是由编译器创建 EXE 文件的另一个实现。确保它的最简单方法是使 Release() 虚拟化,以便调用始终是使用来自 vtable(由 DLL 提供)的方法指针的调用。

  2. 是的,来自 STL 的内联方法(如 string::size())会在这里引起问题。

  3. 对。如果 new/delete 都重载以使用 GetProcessHeap(),则代码将起作用。
  4. 一般来说,对。特别是,请参阅您需要的 boost 类的实现。
  5. 如果它们不依赖于 STL,并且您确定两个编译器的内存占用是相同的,并且您提供了自定义的 new/delete(),那么它通常应该不是问题(参见下面的注释)。

评论:

有一些低概率的事情可能会导致上面没有提到的问题:

  1. 如果您使用不同的编译器,它们可能默认使用不同的调用约定(cdecl/stdcall)。
  2. 默认对齐选项也可能不同,从而导致不同的内存布局。
  3. 如果您从 DLL 中抛出异常,具有不同运行时的 exe 可能不会捕获它们,而是会崩溃。
  4. 用 DLL 导入/导出属性标记方法并确保它们没有被内联可能会很麻烦。此外,如果您使用不同的编译器,C++ 名称修饰算法可能会有所不同。

总结一下,建议看看微软 COM 中是如何实现的,并设计类似的东西。即将 EXE/DLL 交互限制为:

  1. 接口。即仅具有纯虚方法的 C++ 类。使用虚拟 Release() 删除对象。
  2. 简单明确定义的结构。确保所有编译器的对齐选项匹配。如果您想使用非平凡的结构并且使用不同的编译器,最好保持偏执并进行如下检查:

    struct MyStruct
    {
        ...
    };
    C_ASSERT(sizeof(MyStruct) == ...);
    C_ASSERT(FIELD_OFFSET(MyStruct, MyMember) = ... );
    
  3. 类 C 函数传递和/或返回指向接口的指针。

  4. 不要从 EXE 调用的方法中抛出异常
于 2012-07-22T20:48:34.150 回答