科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道应用软件在 C++Builder 工程里使用 Visual C++ DLL——第2部分:C++ 类

在 C++Builder 工程里使用 Visual C++ DLL——第2部分:C++ 类

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

你可以简单的连接引入库来使用 MSVC DLL。

作者:shadowstar 来源:CSDN 2008年5月20日

关键字: Dll C++ python 软件

  • 评论
  • 分享微博
  • 分享邮件

在上一篇文章如何“在 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, 现丑了。这就是那三种技术。

  1. 用 MSVC 创建一个 DLL,把 C++ 类包裹成简单的 C 函数。简单的 C 函数是可以在 BCB 里引入的。
  2. 用 MSVC 创建一个 COM 对象,把 C++ 类经过限制包装。BCB 可以作为 COM 客户端来调用 VC++ COM 对象。
  3. 把 C++ 类用抽象类包装起来,这个抽象类只带有一些没有实体的虚函数。这从本质上说还是 COM,只是没有了难看的部分。

下面描述各种技术的更多详细内容。在每一个例子中,我们将假定 MSVC DLL 导出的类形式如下:

class CFoo
{
public:
    CFoo(int x);
    ~CFoo();

    int DoSomething(int y);
};

技术 1: 把 C++ 类包裹到 C 库里

在前一篇有关 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++ 对象。

技术 2: 创建 COM 包装

不幸地,我还没有这种技术的例子(嗨,我说过这篇文章不是为黄金时段准备的)。但这个主意是这样工作的。在 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)

技术 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 是怎么进行查找工作的。

Tip 注意:

如果你使用这种 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领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章