分享

C++Builder和Visual C++之间互相用dll的方法

 louis.sun 2010-09-13

在 C++Builder 工程里使用 Visual C++ DLL——第1部分:C函数

 

译者序:

  第一次读这篇文章是在 2001 年 10 月,帮我解决了一点小问题。本来不好意思翻译,因为英语水平实在太差。最近发现不少网友在问在 C++Builder 的工程里调用 Visual C++ DLL 的问题,也许是用 C++Builder 的人比以前多了吧。于是把心一横,不就是板儿砖嘛?“抛砖引玉”,希望它能给你帮点小忙,也欢迎指出翻译中的错误。

source:http://www./articles/vcdll.htm

  很可能有一天你的老板问你是否能用 C++Builder 创建一个 GUI,调用现有的用 Microsoft Visual C++ 编译的 32 位 DLL。经常地,原始 DLL 的源代码不会提供给你,也许因为 DLL 来自第三方供应商,也可能是 22 岁的实习生不小心从网络上删除了 \DLL\SRC 目录。给你一个 DLL 和头文件,这篇文章为你示范如何在你的 C++Builder 工程里调用这种 DLL。

    * 在 C++Builder 工程里调用 DLL 函数
    * Visual C++ DLL 带来的问题
    * 第1步:识别在 Visual C++ DLL 里使用的调用习惯
    * 第2步:检查 DLL 里的连接名字
    * 第3步:为 Visual C++ DLL 生成引入库
    * 第4步:把引入库添加到你的工程里
    * 结束语

在 C++Builder 工程里调用 DLL 函数

  调用 Visual C++ DLL 给 C++Builder 程序员提出了一些独特的挑战。在我们试图解决 Visual C++ 生成的 DLL 之前,回顾一下如何调用一个 C++Builder 创建的 DLL 可能会有所帮助。调用 C++Builder 创建的 DLL 要比 Visual C++ 的少了许多障碍。

  为了在你的 C++Builder 工程里调用 DLL,你需要三种元素:DLL 本身,带有函数原型的头文件,和引入库(你可以在运行时载入 DLL,而不是使用引入库,但为了简单我们按引入库的方法做)。调用 DLL 函数,首先通过选择菜单 Project | Add to Project 的方法,把引入库添加到你的 C++Builder 工程里;其次,在需要调用 DLL 函数的 C++ 源文件里为 DLL 头文件插入 #include 声明;最后添加调用 DLL 函数的代码。

  程序清单 A 和 B 包含了做为测试 DLL 的源代码。注意,测试代码实现了两种不同的调用习惯(__stdcall 和 __cdecl)。这样帮是有充分的理由的。当你设法调用一个用 Visual C++ 编译的 DLL 时,大多让你头疼的事情都是由于处理不同的调用习惯产生的。还要注意一点,有一个函数,它没有明确列出使用的调用习惯。这个未知函数作为不列出调用习惯的 DLL 函数的标识。

//------------------------------------------
// Listing A: DLL.H

#ifdef __cplusplus
extern "C" {
#endif

#ifdef _BUILD_DLL_
#define FUNCTION __declspec(dllexport)
#else
#define FUNCTION __declspec(dllimport)
#endif

FUNCTION int __stdcall   StdCallFunction(int Value);
FUNCTION int __cdecl     CdeclFunction  (int Value);
FUNCTION int             UnknownFunction(int Value);

#ifdef __cplusplus
}
#endif


//------------------------------------------
//Listing B: DLL.C

#define _BUILD_DLL_
#include "dll.h"

FUNCTION int __stdcall StdCallFunction(int Value)
{
    return Value + 1;
}

FUNCTION int __cdecl   CdeclFunction(int Value)
{
    return Value + 2;
}

FUNCTION int UnknownFunction(int Value)
{
    return Value;
}

  从清单 A 和 B 创建测试 DLL,打开 C++Builder,选择菜单 File | New 调出 Object Repository。选择 DLL 图标,单击 OK 按钮。C++Builder 会创建一个新的工程,带有一个源文件。这个文件包含一个 DLL 的入口函数和一些 include 声明。现在选择 File | New Unit。保存新的单元为 DLL.CPP。从清单 A 拷贝粘贴文本插入头文件 DLL.H。从清单 B 拷贝代码,把它插入 DLL.CPP。确定 #define _BUILD_DLL_ 位于 #include "DLL.H" 声明的上面。

  保存工程为 BCBDLL.BPR。接下来,编译工程,看看生成的文件。C++Builder 生成了一个 DLL 和以 .LIB 为扩展名的引入库。

  这时,你有了在 C++Builder 里调用 DLL 所需的三个元素:DLL 本身,带有函数原型的头文件,用来连接的引入库。现在我们需要一个用来调用 DLL 函数的 C++Builder 工程。在 C++Builder 里创建一个新的工程,保存到你的硬盘上。从 DLL 工程目录里拷贝 DLL、引入库、DLL.H 头文件到新的目录。其次,在主单元里添加 #include 声明,包含 DLL.H。最后,添加调用 DLL 函数的代码。清单 C 列出了调用由清单 A 和 B 生成的 DLL 中每个函数的代码。

//------------------------------------------
// Listing C: MAINFORM.CPP - DLLTest program
#include <vcl\vcl.h>
#pragma hdrstop

#include "MAINFORM.h"
#include "dll.h"
//---------------------------------------------------------
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
  : TForm(Owner)
{
}
//---------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
    int Value = StrToInt(Edit1->Text);
    int Result= StdCallFunction(Value);
    ResultLabel->Caption = IntToStr(Result);
}
//---------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
    int Value = StrToInt(Edit1->Text);
    int Result= CdeclFunction(Value);
    ResultLabel->Caption = IntToStr(Result);
}
//---------------------------------------------------------
void __fastcall TForm1::Button3Click(TObject *Sender)
{
    int Value = StrToInt(Edit1->Text);
    int Result= UnknownFunction(Value);
    ResultLabel->Caption = IntToStr(Result);
}

Visual C++ DLL 带来的问题

  在理想世界里,调用 Visual C++ 创建的 DLL 不会比调用 C++Builder 建造的 DLL 难。不幸地,Borland 和 Microsoft 有几点不一致的地方。首先,Borland 和 Microsoft 在 OBJ 和引入库的文件格式上不同(Visual C++ 使用 COFF 库格式,而 Borland 使用 OMF 格式)。这就意味着你不能把一个 Microsoft 生成的引入库添加到C++Builder 的工程里。感谢 Borland IMPLIB 这个实用工具,文件格式的不同得以克服。

  两个产品在连接名字(linker name)习惯上也不同。这是 C++Builder 调用 Visual C++ DLL 的主要障碍。在 DLL 或 OBJ 里的每一个函数有一个连接名字。连接器用连接名字在连接期间解决(resolve)声明了原型的函数。如果连接器不能找到它认为是程序需要的连接名字的函数,它将产生一个未解决的外部错误(unresolved external error)。

  关于函数连接名字,Borland 和 Microsoft 在下面两点上不同:

    * 1- Visual C++ 有时修饰导出的 __stdcall 函数。
    * 2- Borland C++Builder 在引入这个被修饰的函数时,认为是 __cdecl 函数。

  那么,这件事为什么这样重要呢?拿分歧#1 __stdcall 调用习惯来说。如果你用 Visual C++ 创建了一个 DLL,它包含一个 __stdcall 修饰的函数叫做 MyFunction(),Visual C++ 将给函数一个连接名字,为 _MyFunction@4。当 Borland 连接器设法解决调用构造这个函数的时候,它认为要找一个名为 MyFunction 的函数。因为 Visual C++ DLL 引入库不包含叫作 MyFunction 的函数,Borland 连接器报告一个未解决的外部错误,意识是没有找到函数。

  解决这三个问题的方法要依赖 Visual C++ DLL 的编译方式。我把整个过程分为四步。
第1步:识别在 Visual C++ DLL 里使用的调用习惯

  为了与命名习惯缠结交战,你必须首先确定在 DLL 里函数使用的调用习惯。你可以通过查看 DLL 的头文件来确定。在 DLL 头文件里的函数原型形式如下:

  __declspec(dllimport) void CALLING_CONVENTION MyFunction(int nArg);

  CALLING_CONVENTION 应该是 __stdcall 或 __cdecl(具体例子参见清单 A)。很多时候,调用习惯没有被指定,在这种情况下默认为 __cdecl。
第2步:检查 DLL 里的连接名字

  如果在第 1 步中显示 DLL 利用 __stdcall 调用习惯,你需要进一步检查 DLL,确定 Visual C++ 在创建它时采用的命名习惯。Visual C++ 默认情况下要修饰 __stdcall 函数,但如果写这个 DLL 的程序员在他们的工程里增加一个 DEF 文件,可以阻止命名修饰。如果供应商没有使用 DEF 文件,你的工会稍微繁琐一些。

  命令行工具 TDUMP 允许你检查 DLL 导出函数的连接名字。下面向 DLL 调用 TDUMP 的命令。

  TDUMP -ee -m MYDLL.DLL > MYDLL.LST

  TDUMP 能报告许多关于 DLL 的信息。我们仅对 DLL 的导出函数感兴趣。-ee 命令选项指示 TDUMP 仅列出导出信息。-m 开关告诉 TDUMP 按 DLL 函数的原始格式显示。如果没有 -m 开关,TDUMP 将尝试把修饰过的函数转化为人们易读的格式。如果 DLL 很大的话,你应该重定向 TDUMP 的输出到一个文件里(通过附加的 > MYDLL.LST)。

  TDUMP 为源程序清单 A 和 B 的测试 DLL 输出如下:

  Turbo Dump Version 5.0.16.4 Copyright (c) 1988, 1998 Borland International
  Display of File DLL.DLL

  EXPORT ord:0000='CdeclFunction'
  EXPORT ord:0002='UnknownFunction'
  EXPORT ord:0001='_StdCallFunction@4'

  注意在 __stdcall 函数上的前缀下划线和后缀 @4。__cdecl 和未指定调用方式的函数没有任何修饰符。如果 Visuall C++ DLL 编译的时候带 DEF 文件,在 __stdcall 函数上的修饰符将不会出现。
第3步:为 Visual C++ DLL 生成一个引入库

  这是关键部分。由于 C++Builder 和 Visual C++ 的库文件格式不同,你不能把 Visual C++ 创建的引入库添加到你的 C++Builder 工程里。你必须用随 C++Builder 一起发行的命令行工具创建一个 OMF 格式的引入库。依靠上面两步得出的结论,这一步或者很顺利,或者需要一些时间。

  如前面所述,C++Builder 和 Visual C++ 在关于怎样给 DLL 函数命名上是不一致的。由于命名习惯的不同,如果 C++Builder 和 Visual C++ 对 DLL 调用习惯的实现不一致,你需要创建一个带有别名的引入库。表 A 列出了不一致的地方。

表A:Visual C++和C++Builder命名习惯

调用习惯    VC++ 命名       VC++ (使用了DEF)    C++Builder 命名
-----------------------------------------------------------------
__stdcall   _MyFunction@4   MyFunction          MyFunction
__cdecl     MyFunction      MyFunction          _MyFunction

  C++Builder 栏列出 Borland 连接器想要找的连接名字。第一个 Visual C++ 栏列出 Visual C++ 工程里没有使用 DEF 文件时的连接名字。第二个 Visual C++ 栏包含了使用 DEF 文件时 Visual C++ 创建的连接名字。注意,两个产品仅在一种情况下一致:Visual C++ 工程包含 DEF 文件的 __stdcall 函数。下一关,你需要创建一个带有别名的引入库,使 Visual C++ 命名与 C++Builder 命名相一致。

表 A 显示出几种你在创建引入库时可能需要处理的组合。我把组合分成两种情况。

第 1 种情况:DLL 只包含 __stdcall 函数,DLL 供应商利用了 DEF 文件

  表 A 显示,仅当 DLL 使用了 __stdcall 函数时 VC++ 和 C++Builder 是一致的。而且,DLL 必须带有 DEF 文件编译,以防止 VC++ 修饰连接名字。头文件会告诉你是否使用了 __stdcall 调用习惯(第 1 步),TDUMP 将显示函数是否被修饰(第 2 步)。如果 DLL 包含没有被修饰的 __stdcall 函数,Visual C++ 和 C++Buidler 在给函数命名上保持一致。你可以运行 IMPLIB 为 DLL 创建一个引入库。不需要别名。

IMPLIB 的命令格式如下:

  IMPLIB (destination lib name) (source dll)

例如:

  IMPLIB mydll.lib mydll.dll

第 2 种情况:DLL 包含 __cdecl 函数或者被修饰的 __stdcall 函数

  如果你的 DLL 供营商坚持创建于编译器无关的 DLL,你很幸运地可以把它归入第 1 种情况。不幸地,有几种可能使你不能把它归入第 1 种情况。第一,如果 DLL 供应商在函数声明的时候省略了调用习惯,则默认为 __cdecl,__cdecl 强迫你进入情况 2。第二,即使你的供应商利用了 __stdcall 调用习惯,他们可能忽视了利用 DEF 文件去掉 Visual C++ 的修饰符。

  然而你找到了这里,Good Day,欢迎来到第 2 种情况。你被用一个函数名与 C++Builder 不同的 DLL 困住。摆脱这个麻烦的唯一办法就是创建一个引入库,为 Visual C++ 的函数名定义一个和 C++Builder 的格式兼容的别名。幸运地,C++Builder 命令行工具允许你创建一个带有别名的引入库。

  第一步,用 C++Builder 带的 IMPDEF 程序给 Visual C++ DLL 创建一个 DEF 文件。IMPDEF 创建的 DEF 文件可以列出 DLL 导出的所有函数。你可以这样调用IMPDEF:

  IMPDEF (Destination DEF file) (source DLL file)

例如:

  IMPDEF mydll.def mydll.dll

  运行 IMPDEF 之后,选择一个编辑器打开产生的 DEF 文件。对用 Visual C++ 编译源程序清单 A 和 B 生成 DLL,IMPDEF 创建的 DEF 文件如下:

  EXPORTS
      ; use this type of aliasing
      ; (Borland name)   = (Name exported by Visual C++)
      _CdeclFunction   = CdeclFunction
      _UnknownFunction = UnknownFunction
      StdCallFunction  = _StdCallFunction@4

  下一步将修改 DEF 文件,让 DLL 函数的别名看起来和 C++Builder 的函数一样。你可以这样创建一个 DLL 函数的别名,列出一个 C++Builder 兼容的名字,后面接原始的 Visual C++ 连接名字。对于程序清单 A 和 B 的测试 DLL 来说,带别名的 DEF 如下:

  EXPORTS
      ; use this type of aliasing
      ; (Borland name) = (Name exported by Visual C++)
      _CdeclFunction = CdeclFunction
      _UnknownFunction = UnknownFunction
      StdCallFunction = _StdCallFunction@4

  注意,在左边的函数名与表 A 中 Borland 兼容的名字相匹配。在右边的函数名是真实的 Visual C++ DLL 函数的连接名字。

  最后一步将从别名 DEF 文件创建一个别名引入库。你又要靠 IMPLIB 实用程序了,只是这一次,用别名 DEF 文件做为源文件代替它原来的 DLL。格式为:

  IMPLIB (dest lib file) (source def file)

例如:

  IMPLIB mydll.lib mydll.def

  创建了引入库,还要继续进行到第四步。你首先应该检查引入库,以保证每一个 DLL 函数与 C++Builder 具有一致的命名格式。你可以用 TLIB 实用程序检查引入库。

  TLIB mydll.lib, mydll.lst

为测试 DLL 生成的列表文件如下:

    Publics by module

    StdCallFunction size = 0
            StdCallFunction

    _CdeclFunction  size = 0
            _CdeclFunction

    _UnknownFunction size = 0
            _UnknownFunction

第 4 步:把引入库添加到你的工程里

  一旦你为 Visual C++ DLL 创建了一个引入库,你可以用菜单 Project | Add to Project 把它添加到你的 C++Builder 工程里。你使用引入库的时候不必考虑它是否包含有别名。把这个引入库添加到你的工程里的之后,建造(build)你的工程,看看是不是可以成功的连接。
结束语:

  这篇文章为你示范了如何在 C++Builder 工程里调用 Visual C++ DLL 的函数。这些技巧对 C++Builder 1 和 C++Builder 3,Visual C++ 4.x 或 Visual C++ 5 创建的 DLL 生效(我还没有测试 Visual C++ 6)。

  你可能注意到,这篇文章仅讨论了如何调用 DLL 里 C 风格的函数。没有尝试去做调用 Visual C++ DLL 对象的方法。因为对于成员函数的连接名字被改编(mangled),C++ DLL 表现出更加困难的问题。编译器要使用一种名字改编(name mangling)方案,以支持函数重载。不幸地,C++ 标准没有指定编译器应当如何改编类的方法。由于没有一个严格的标准到位,Borland 和 Microsoft 各自为名字改编发展了他们自己的技术,并且两者的习惯是不兼容的。在理论上,你可以用同样的别名技术调用位于 DLL 里的一个类的成员函数。但你应该考虑创建一个 COM 对象来代替。COM 带来了许多它自己的问题,但它强制执行以一种标准方式调用对象的方法。由 Visual C++ 创建的 COM 对象可以在任一开发环境里被调用,包括 Delphi 和 C++Builder。

  C++Builder 3.0 引入了一个新的命令行实用程序叫做 COFF2OMF.EXE。这个实用程序可以把 Visual C++ 引入库转化为 C++Builder 的引入库。此外,对 __cdecl 函数,这个程序还会自动的产生从 Visual C++ 格式到 C++Builder 格式的别名。如果 DLL 专用 __cdecl 调用习惯,自动别名可以简化第 3 步。

 

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

 

 

source:http://www./articles/vcdll2.htm

注意:这篇文章描述如何把 C++ 类从 Visual C++ DLL 引入到 BCB 的工程中。在我们开始之前,我觉得必须给出一点警告。这篇文章不是真的准备大量发布的。如果“文章”跌宕起伏,难以阅读,或包含错误,我道赚!我没有时间去改良它。我决定继续并发布的唯一原因是因为很多开发者问到怎么处理这个问题。我认为,一篇写的很烂的文章总比什么都没有好。我希望这个不连贯概念的搜集品会给你带来帮助。

在上一篇文章如何“在 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 里使用的某种形式。

    * 三种技术摘要
    * 技术 1: 把 C++ 类包裹到 C 库里
    * 技术 2: 创建 COM 包装
    * 技术 3: 使用带虚函数的抽象基类(pseudo-COM)
    * 结论
    * 下载

三种技术摘要

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 准备例子。
下载

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多