扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
在上一篇文章如何“在 C++Builder 工程里使用 Visual C++ DLL”中,我描述了如何为 MSVC DLL 创建一个 Borland 兼容的引入库。主要的难点在于 MSVC 和 Borland 使用的函数命名格式不同。举例来说,Borland 认为 __cdecl 函数在它们的开头有一个下划线,但 MSVC 没有。幸运的是,你可以用 Borland 命令行实用工具克服名称的不同,这些工具有 TDUMP、IMPLIB、IMPDEF、和 COFF2OMF。方法是用这些命令行工具创建一个带有 Borland 兼容函数名的 Borland 兼容引入库。一旦你拥有了 Borland 兼容引入库,你便可以开始工作了。你可以简单的连接引入库来使用 MSVC DLL。
不幸地,这种策略不能完全带你走出这片森林。在上一篇 DLL 文章的结尾,我丢下了一个小炸弹。你只能调用 MSVC DLL 里简单的 C 函数,而不能引入类或类成员函数。Doh!
那么如果你需要从 MSVC DLL 引入 C++ 类要做些什么呢?啊……这个,如果是那样的话,你就被关到角落里了,没有多少可选择的余地(通常在你退到角落里的时候,你的选项都不是令人满意的)。这篇文描述了三种可以带你走出角落的方法。
坏消息:当你准备花点时间研究这篇垃圾的时候,我觉得,再次,被迫发出警告。所有三种技术需要你有 Microsoft Visual C++。你不需要有要调用的 DLL 的源代码,但你需要有可以调它的工具。三种技术都或多或少使用包装技术,我们用 MSVC 把 DLL 包装成可以在 BCB 里使用的某种形式。
Ok, 现丑了。这就是那三种技术。
下面描述各种技术的更多详细内容。在每一个例子中,我们将假定 MSVC DLL 导出的类形式如下:
class CFoo { public: CFoo(int x); ~CFoo(); int DoSomething(int y); };
在前一篇有关 VC++ DLL 的文章里,我们知道在一个 Borland 工程里调用从一个 MSVC DLL 导出的简单的 C 函数是可能的。利用这条信息可知,我们可以在 MSVC 里创建一个 DLL 工程,来导出简单的 C 函数给 BCB 用。这个 MSVC 包裹的 DLL 会作为 C++ DLL 的客户端。包裹 DLL 将导出简单的 C 函数,以创建的 CFoo 对象调,调用 CFoo 成员函数,和销毁 CFoo 对象。
CFoo 类包含三个我们关心的函数:构造函数,析构函数,和所有重要的 DoSomething 函数。我们需要把每一个函数包裹成与其等价的 C 函数。
// original class class CFoo { public: CFoo(int x); ~CFoo(); int DoSomething(int y); }; // flattened C code void* __stdcall new_CFoo(int x) { return new CFoo(x); } int __stdcall CFoo_DoSomething(void* handle, int y) { CFoo *foo = reinterpret_cast<CFoo *>(handle); return foo->DoSomething(y); } void __stdcall delete_CFoo(void *handle) { CFoo *foo = reinterpret_cast<CFoo *>(handle); delete foo; }
这里有几个比较重要的地方要注意。首先,注意每一个 C++ 成员函数被映射为一个简单的 C 函数。其次,观察到我们为 C 函数明确地使用 __stdcall 调用习惯。在前一篇 DLL 文章里,我们知道简单的调用在 MSVC DLL 里的无格式 C 函数,真是很麻烦。如果我们放弃越过种种艰难困苦去用它,我们可以使这个努力稍微容易一点。让 Borland 调用 Microsoft DLL 最简单的办法是 DLL 导出无格式,无修饰,__stdcall 调用习惯的 C 函数。Borland 和 Microsoft 对 __cdecl 函数的处理上是不同的。通常,他们对 __stdcall 函数也不同,因为 MSVC 修饰 __stdcall 函数,但我们可以通过添加一个 DEF 文件到 MSVC 工程里来阻止这种行为。参见下载部分的例子有 DEF 文件的例子。
其它关于代码要注意的事情是 new_CFoo 函数返回一个指向 CFoo 对象的指针。BCB 调用者必须在本地保存这个指针。这可能看起来和这篇文章的主题有点矛盾。毕竟,我想 BCB 不能使用来自 MSVC DLL 的 C++ 对象?如果那是正确的,那么为什么我们还要返回一个 CFoo 对象指针呢?
答案是 BCB 不能调用 MSVC DLL 导出类的成员函数。但是,这并不意味着它不能存储这样对象的地址。new_CFoo 返回的是一个 CFoo 对象的指针。BCB 客户端可以存储这个指针,但不能用。BCB 不能废弃它(不应当尝试这么做)。让这个观点更容易理解一点,new_CFoo 返回一个空指针(总之它不能返回别的什么东西)。在 BCB 这边,除了存储它,然后把它传回给 DLL,没有什么可以安全地处理这个空指针的方法。
Ok,在我们继续前进之前,还有另外两个要十分注意的地方。首先,注意 CFoo_DoSomething 把空指针作为它的第一个参数。这个空指针与 new_CFoo 返回的是同一个空指针。空指针用 reinterpret_cast 被追溯到 CFoo 对象(你知道,当你看到一个 reinterpret_cast 的时候,你正在处理是难看的代码)。DoSomething 成员函数在转换之后被调用。最后,注意空指针也是 delete_CFoo 函数的参数。包装 DLL 删除对象是至关紧要的。你不应当在 BCB 里对空指针调用 delete。显然它不会按你想的去做。
下面的程序清单展示了 C 函数的 DLL 头文件。这个头文件可以在 MSVC 和 BCB 之间共享。
// DLL header file #ifndef DLL_H #define DLL_H #ifdef BUILD_DLL #define DLLAPI __declspec(dllexport) #else #define DLLAPI __declspec(dllimport) #else #ifdef __cplusplus extern "C" { #endif DLLAPI void* __stdcall new_CFoo(int x); DLLAPI int __stdcall CFoo_DoSomething(void* handle, int y); DLLAPI void __stdcall delete_CFoo(void *handle); #ifdef __cplusplus } #endif #endif
这是一个典型的 DLL 头文件。注意到一个令人好奇的事情,在头文件里看不到 CFoo 类。头文件仅包含用以包装 CFoo 的无格式 C 函数。
下面的程序清单展示了如何在 BCB 里调用 DLL。
#include "dll.h" void bar() { int x = 10; int y = 20; int z; void * foo = new_CFoo(x); z = CFoo_DoSomething(foo, y); delete_CFoo(foo); }
这样就可以了。尽管不太漂亮,但还能用。事实上,不管这个技术多么奇异,在其它一些不能调用 DLL 的情形,同样可以用这种方法。举例来说,Delphi 程序员使用相同的技术,因为 Delphi 不能调用 C++ 成员函数。Delphi 程序员必须把 C++ 类包裹成 C 代码,并连接成 C OBJ 文件。开源工具 SWIG (swig.org) 被设计用来生成象这样的包装函数,在那里允许你使用类似 Python 的角本语言调用 C++ 对象。
不幸地,我还没有这种技术的例子(嗨,我说过这篇文章不是为黄金时段准备的)。但这个主意是这样工作的。在 MSVC 里创建一个 COM 对象。或许你可以运行向导。创建一个进程内服务器(如 DLL,不是 EXE)。同样,确认你创建了一个 COM 对象,而不是自动控制对象。自动控制只会是使每一件事更困难。除非你也需要在 VB 或 ASP 页面用 C++ 类,那也可以用无格式 COM,而不用自动控制。
在 COM 工程内部,创建一个新的 COM 对象。MSVC 大概想让你创建一个 COM 接口。既然我们正在包装一个称做 CFoo 的类,一个好的接口名应当是 IFoo。MSVC 也会让你为执行类的 COM 对象命名。CFooImpl 是一个不错的候选者。
COM 对象应当用聚合包装 C++ DLL 类。换句话说,COM 对象包含 CFoo 成员变量。不要设法从 CFoo 继承你的 COM 类。对每一个 C++ DLL 类(CFoo)的成员函数,在你的 COM 对象里创建一个类似的函数。如果可能的话,用相同的名字,传递相同的参数,返回相同类型的值。你需要调整一些事情。比如,字符串在 COM 里通常被传递为 BSTR。同样,返回值被特别地传递为输出参数,因为 COM 方法应当返回一个错误代码。当你做完这些,C++ 类的每一个成员函数在 COM 包装里应当有一个相应的函数。
在你 build COM 包装之后,用 regsrv32.exe 注册它。一旦注册,你应当能例示这个 COM 对象,并且用 BCB 代码调用它包装的成员函数。
再一次,我为上面介绍的这种技术没有可运行的演示道歉。
技术 3 是一种 pseudo-COM 方法。COM 是一个二进制对象规范。COM 对象可以被 BCB 和 MSVC 调用,而不管 COM 对象是用什么编译器编译的。因此,这个二进制用什么魔法工作的呢?答案就是基于要讲的这种技术。
COM 函数调用通过函数查找表来分派。神奇地是这个函数查找表与 C++ 虚函数表用同样的方法正确地工作。事实上,他们就是相同的。COM 不过是虚函数和虚函数表的一种美称的形式。
COM 可以工作,是因为 BCB 和 MSVC 真正使用相同的虚分派系统。COM 依赖于大多数 Win32 C++ 编译器都用相同的方法生成和使用 vtable 的这个事实。因为两个编译器用相同的虚分派系统,我们就能在 MSVC 里用虚函数创建一个包装类,它可以被 BCB 调用。这正是 COM 所做的。
这是 pseudo-COM 包装类的 DLL 头文件。它包括一个抽象基类,IFoo,它服务于 pseudo-COM 接口。它还包括两个 C 函数,用来创建和删除 IFoo 对象。这个头文件在 MSVC 和 BCB 之间共享。
#ifndef DLL_H #define DLL_H #ifdef BUILD_DLL #define DLLAPI __declspec(dllexport) #else #define DLLAPI __declspec(dllimport) #endif // psuedo COM interface class IFoo { public: virtual int __stdcall DoSomething(int x) = 0; virtual __stdcall ~IFoo() = 0; }; #ifdef __cplusplus extern "C" { #endif DLLAPI IFoo* __stdcall new_IFoo(int x); DLLAPI void __stdcall delete_IFoo(IFoo *f); #ifdef __cplusplus } #endif #endif
注意到两个 C 函数类似技术 1 的函数,除了现在它们与 IFoo 合作,而不是空指针。这种技术比第一种提供了更多的类型安全。
这里是 MSVC 包装的源代码。它包括一个从 IFoo 继承而来的称作 CFooImpl 的类。CFooImpl 是 IFoo 接口的实现。
#define BUILD_DLL #include "dll.h" IFoo::~IFoo() { // must implement base class destructor // even if its abstract } // Note: we declare the class here because no one outside needs to be concerned // with it. class CFooImpl : public IFoo { private: CFoo m_Foo; // the real C++ class from the existing MSVC C++ DLL public: CFooImpl(int x); virtual ~CFooImpl(); virtual int __stdcall DoSomething(int x); }; CFooImpl::CFooImpl(int x) : m_Foo(x) { } int __stdcall CFooImpl::DoSomething(int x) { return m_Foo.DoSomething(x); } CFooImpl::~CFooImpl() { } IFoo * __stdcall new_IFoo(int x) { return new CFooImpl(x); } void __stdcall delete_IFoo(IFoo *f) { delete f; }
这儿有许多好的素材资料。首先,注意到现在我们有一个类在 BCB 和 MSVC 之间共享的头文件。好象是一件好事。更重要的是,注意到 BCB 工程将只与 IFoo 类打交道。真正的 IFoo 实现由叫做 CFooImpl 的派生类提供,那是在 MSVC 工程内部。
BCB 客户端代码将与 IFoo 对象以多态性合作。要得到一个包装实例,BCB 代码可以调用 new_IFoo 函数。new_IFoo 的工作像一个函数工厂,提供新的 IFoo 实例。new_Foo 返回一个指向 IFoo 实例的指针。然而,指针是多态的。指针的静态类型是 IFoo,但它实际的动态类型将被指向 CFooImpl(BCB 代码是不知道真相的)。
这是 BCB 客户端的代码。
#include "dll.h" void bar() { int x = 10; int y = 20; int z; IFoo *foo = new_IFoo(x); z = foo->DoSomething(y); delete_IFoo(foo); }
现在给出在技术 3 上某些部分的注释。第一,至关紧要的是你从 MSVC DLL 里删除 IFoo 指针。这个由调用 delete_IFoo 函数传递 IFoo 指针完成。不要尝试从 BCB 里删除对象。
void bar() { IFoo *foo = new_IFoo(x); delete foo; // BOOM!!! }
这段代码将在痛苦中死去。问题是 IFoo 是被在 MSVC 包装 DLL 里的 new_IFoo 函数创建的。同样地,IFoo 对象占的内存是被 MSVC 内存管理器分配的。当你删除一个对象时,只有权删除和它用同一个内存管理器创建的对象。如果你在 BCB 这边对指针调用 delete,那么你是用 Borland 内存管理器删除。现在,我可能错了,但是我愿意拿我的房子和一个生殖器打赌,要么二个,不能企图让 Microsoft 内存管理器和 Borland 内存管理器一起工作。当你用 Borland 内存管理器删除指针的时候,难道它会尝试联系 Microsoft 内存管理器,让它知道它应当释放的哪些内存?
另外解释一项,BCB 代码完全按照 IFoo 虚函数接口工作。在 BCB 这边你看不到任何 CFooImpl 类的事件。CFooImpl 在 MSVC 包装工程的内存。当你从 BCB 这边调用 DoSomething 的时候,调用通过虚函数表被分派到 CFooImpl。
如果你在这个概念上理解有困难的话,不要着急。我或许没有把它描述的很好。下面的内容可以帮助理解,在 BCB 这边,你可以用 CPU viewer 单步跟踪代码。它允许你单步跟踪每一条汇编指令,看看 vtable 是怎么进行查找工作的。
注意:
如果你使用这种 pseudo-COM 技术,确定你没有尝试重载虚函数。换句话说,不要创建象这样的接口: class IFoo { public: virtual int __stdcall DoSomething(int x) = 0; virtual int __stdcall DoSomething(float x) = 0; virtual int __stdcall DoSomething(const char *x) = 0; }; 不应当重载虚接口函数的原因是 MSVC 和 BCB 在 vtable 上不可能(或许不会)制定相同的方法。当我试验重载时,在 BCB 这边调用 DoSomething(int),在 MSVC 那边象是分派到 DoSomething(float)。Borland 和 Microsoft 在 vtable 格式上不重载的时候是一致的。这可能解释了为什么 COM 对象不使用重载函数。 If you need to wrap a C++ class with overloaded functions, then you should create a distinct function name for each one. class IFoo { public: virtual int __stdcall DoSomething_int (int x) = 0; virtual int __stdcall DoSomething_float(float x) = 0; virtual int __stdcall DoSomething_str (const char *x) = 0; }; |
Ok, 我们到哪儿了?啊,在文章开始,我们讲了关于为什么 BCB 不能调用 DLL 里的 C++ 成员函数,如果 DLL 是被 MSVC 编译的。原因就是两种编译器在成员函数命名上不一致。我们讨论了三种(有点讨厌)工作方法。每一种工作方法由一个为 C++ DLL 而建立的 MSVC 包装 DLL。包装 DLL 用一些 BCB 可以理解的格式揭露 C++ 类。第一种技术把每一个 C++ 类的成员函数包裹成无格式的 C 函数。第二种技术把每一个成员函数映射成 COM 对象的成员。最后一种技术依赖虚函数是按查找表分派而不是名称的事实。在这种策略里,每一个 C++ 成员函数被映射成一个抽象类的虚函数。
下载部分包括这篇文章的例子代码。第一个下载包含原始的 MSVC C++ DLL,我们设法与它合作。三种技术的每一个例程使用相同的 DLL。仍就没有为技术 2 准备例子。
为这篇文章的下载 | |
cppdll.zip | VC++ 5 DLL 工程,导出 C++ CFoo 类 |
vcdll2tech1.zip | 技术 1 的代码, 把类包裹成 C 函数 |
vcdll2tech3.zip | 技术 3 的代码,虚函数/抽象基类包装 |
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者