8

这个问题与“如何在 VS 版本之间制作一致的 dll 二进制文件?”有关。

  • 我们有使用 VC6 构建的应用程序和 DLL 以及使用 VC9 构建的新应用程序。VC9 应用程序必须使用用 VC6 编译的 DLL,其中大部分是用 C 编写的,一个是用 C++ 编写的。
  • 由于名称修饰/修改问题,C++ 库存在问题。
  • 使用 VC9 编译所有内容目前不是一种选择,因为似乎有一些副作用。解决这些问题将非常耗时。
  • 我可以修改 C++ 库,但它必须使用 VC6 编译。
  • C++ 库本质上是另一个 C 库的 OO 包装器。VC9-app 使用一些静态函数以及一些非静态函数。

虽然静态函数可以用类似的东西来处理

// Header file
class DLL_API Foo
{
    int init();
}

extern "C"
{
    int DLL_API Foo_init();
}

// Implementation file
int Foo_init()
{
    return Foo::init();
}

使用非静态方法并不容易。

据我了解,Chris Becke关于使用类 COM 接口的建议对我没有帮助,因为接口成员名称仍将被修饰,因此无法从使用不同编译器创建的二进制文件中访问。我在吗?

唯一的解决方案是使用对象的处理程序编写 C 风格的 DLL 接口,还是我遗漏了什么?在那种情况下,我想,直接使用封装的 C 库可能会更省力。

4

5 回答 5

9

使用与调用 EXE 不同的 C++ 编译器编译的 DLL 时要考虑的最大问题是内存分配和对象生存期。

我假设您可以超越名称修饰(和调用约定),如果您使用具有兼容修饰的编译器(我认为 VC6 与 VS2008 广泛兼容),或者如果您使用 extern "C",这并不难.

你会遇到问题的地方是当你从 DLL 中使用new(或malloc) 分配一些东西,然后你把它返回给调用者。调用者的delete(或free)将尝试从不同的堆中释放对象。这将大错特错。

您可以做 COM 风格IFoo::Release的事情,也可以做一MyDllFree()件事。这两个,因为它们回调到 DLL 中,将使用delete(or free()) 的正确实现,因此它们将删除正确的对象。

或者,您可以确保使用LocalAlloc(例如),以便 EXE 和 DLL 使用相同的堆。

于 2008-12-01T14:54:31.793 回答
3

接口成员名称不会被修饰——它们只是 vtable 中的偏移量。您可以在头文件中定义一个接口(使用 C 结构,而不是 COM“接口”),因此:

struct IFoo {
    int Init() = 0;
};

然后,您可以从 DLL 中导出一个函数,而无需修改:

class CFoo : public IFoo { /* ... */ };
extern "C" IFoo * __stdcall GetFoo() { return new CFoo(); }

如果您使用的是生成兼容 vtable 的编译器,这将正常工作。Microsoft C++ 自(至少,我认为)用于 DOS 的 MSVC6.1 以来生成了相同格式的 vtable,其中 vtable 是指向函数的简单指针列表(在多重继承情况下具有 thunking)。GNU C++(如果我没记错的话)生成带有函数指针和相对偏移量的 vtables。这些彼此不兼容。

于 2008-12-01T14:46:52.697 回答
3

嗯,我认为Chris Becke 的建议很好。我不会使用Roger 的第一个解决方案,它仅在名称上使用接口,并且正如他所提到的,可能会遇到抽象类和虚拟方法的编译器处理不兼容的问题。Roger 在他的后续文章中指出了有吸引力的 COM 一致案例。

  1. 痛点:你需要学会发出COM接口请求并正确处理IUnknown,至少依赖IUnknown:AddRef和IUnknown:Release。如果接口的实现可以支持多个接口,或者如果方法也可以返回接口,您可能还需要熟悉 IUnknown:QueryInterface。

  2. 这是关键的想法。所有使用接口实现(但不实现)的程序都使用一个通用的#include "*.h" 文件,该文件将接口定义为结构 (C) 或 C/C++ 类 (VC++) 或结构(非 VC++,但 C++)。*.h 文件会根据您是在编译 C 语言程序还是 C++ 语言程序而自动进行适当调整。您不必仅仅为了使用 *.h 文件而了解该部分。*.h 文件所做的是定义接口结构或类型,比方说,IFoo,及其虚拟成员函数(并且只有函数,在这种方法中对数据成员没有直接可见性)。

  3. 头文件被构造为以一种适用于 C 和适用于 C++ 的方式遵守 COM 二进制标准,而与使用的 C++ 编译器无关。(Java JNI 人想出了这一点。)这意味着它可以在任何来源的单独编译的模块之间工作,只要一个完全由函数入口指针(一个 vtable)组成的结构被所有它们都映射到相同的内存(例如,它们必须全部为 x86 32 位,或全部为 x64)。

  4. 在通过某种包装类实现 COM 接口的 DLL 中,您只需要一个工厂入口点。像一个

    extern "C" HRESULT MkIFooImplementation(void **ppv);

它返回一个 HRESULT(你也需要了解这些),并且还会在你提供的用于接收 IFoo 接口指针的位置返回一个 *pv。(我正在略读,这里需要您需要更仔细的细节。不要相信我的语法)您为此使用的实际函数原型也在 *.h 文件中声明。

  1. 关键是工厂入口,它始终是一个未修饰的 extern "C",它完成了所有必要的包装类创建,然后将 Ifoo 接口指针传递到您指定的位置。这意味着用于创建类的所有内存管理以及用于完成它的所有内存管理等都将在您构建包装器的 DLL 中进行。这是您必须处理这些细节的唯一地方。

  2. 当您从工厂函数中获得 OK 结果时,您已经获得了一个接口指针,并且它已经为您保留(已经为您提供的接口指针执行了一个隐式 IFoo:Addref 操作)。

  3. 完成接口后,通过调用接口的 IFoo:Release 方法释放它。它是最终版本实现(如果您制作了更多 AddRef'd 副本),它将拆除工厂 DLL 中的类及其接口支持。无论包含工厂函数的 DLL 是否使用与调用代码相同的库,这都是让您正确依赖接口后一致的动态存储分配和释放的原因。

  4. 您可能也应该实现 IUnknown:QueryInterface(作为方法 IFoo:QueryInterface),即使它总是失败。如果您想在使用 COM 二进制接口模型时更加复杂,因为您有更多经验,您可以学习提供完整的 QueryInterface 实现。

这可能是太多的信息,但我想指出,你面临的关于 DLL 的异构实现的很多问题都在 COM 二进制接口的定义中得到解决,即使你不需要所有这些,它提供可行的解决方案这一事实很有价值。根据我的经验,一旦你掌握了这一点,你将永远不会忘记它在 C++ 和 C++ 互操作情况下的强大功能。

I haven't sketched the resources you might need to consult for examples and what you have to learn in order to make *.h files and to actually implement factory-function wrappers of the libraries you want to share. If you want to dig deeper, holler.

于 2008-12-02T02:49:18.573 回答
1

您还需要考虑其他一些事情,例如各种库正在使用哪些运行时。如果没有对象被共享,那很好,但乍一看似乎不太可能。
Chris Becker 的建议非常准确——使用实际的COM 接口可以帮助您获得所需的二进制兼容性。你的旅费可能会改变 :)

于 2008-12-01T14:44:40.393 回答
0

不好玩,伙计。你很沮丧,你应该给这个:

唯一的解决方案是使用对象的处理程序编写 C 风格的 DLL 接口,还是我遗漏了什么?在那种情况下,我想,直接使用封装的 C 库可能会更省力。

仔细一看。祝你好运。

于 2008-12-01T15:07:56.430 回答