分享
 
 
 

在C++Builder里创建可以被Visual C++使用的DLL

王朝vc·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

在C++Builder里创建可以被Visual C++使用的DLL

shadowstar's home: http://shadowstar.126.com/

source:http://www.bcbdev.com/articles/bcbdll.htm

在前两篇文章里,我们讨论了如何在C++Builder工程里调用用MS Visual C++创建的DLL。这篇文章讨论相反的一种情形,举例说明如何用C++Builder创建一个DLL,使它可以在Visual C++工程里调用。

简介: 为什么这个这么难

指导方针摘要

例1: 隐式连接

例2: 显式连接

例3: 用#define组装隐式连接

例4: 用stdcall函数隐式连接

结论

简介:为什么这个这么难

如果你用BCB创建了一个DLL,它可以被BCB的可执行文件调用,你知道这种使用DLL的方式没什么难度。当你构造一个DLL,BCB生成一个带“.LIB”扩展名的引入库。把这个LIB文件添加到你的工程里。连接器按引入库决定DLL内部调用。当你运行你的程序时,DLL隐式的被载入,你不必去考虑DLL内部调用工作是。

当EXE文件是由Microsoft Visual C++编译的时候,情况会变得比较复杂。有3个主要的问题。首先,BCB和MSVC对DLL中的函数命名方式是不一致的。BCB使用一种习惯,MSVC使用另一种不同的习惯。当然,两种习惯是不兼容的。命名问题在如何在C++Builder工程里使用VC++编译的DLL那篇文章里已经讨论过了。表1总结了各个编译器在各自的调用习惯下,导出的MyFunction函数。注意Borland给__cdecl函数前加了一个下划线,而MSVC没有。另一方面,MSVC认为导出的__stdcall函数前面带有下划线,后面还有一些垃圾。

表1: Visual C++ and C++Builder 命名习惯

调用习惯 VC++命名 VC++(使用了DEF) C++Builder命名

-----------------------------------------------------------------------

__stdcall _MyFunction@4 MyFunction MyFunction

__cdecl MyFunction MyFunction _MyFunction

第2个问题是Borland引入库与MSVC不是二进制兼容的。当你编译DLL时,由BCB创建的引入库不能被MSVC用来连接。如果你想使用隐式连接,那么你需要创建一个MSVC格式的引入库。另一种可选择的办法就是采用显式连接(LoadLibrary和GetProcAddress)。

第3个问题是不能从DLL里导出C++类和成员函数,如果你想让MSVC的用户也可以调用它。好吧,那不完全属实。你的DLL能导出C++类,但是MSVC不能使用它们。原因就是C++成员函数名被编译器改编(mangled)。这个改编的名字结果了DLL。为了调用在DLL里被改编的函数,你必需知道被改编的是哪个函数。Borland和Microsoft使用了不同的名字改编方案。结果是,MSVC不能恰好看到Borland编译的DLL里的C++类和成员函数。

注意:

Borland和Microsoft没有采用相同的方式改编函数,因为依照ANSI C++标准,C++编译器不被假定追随相同的指导方针。名字改编只是实现的细节。

这三个问题使得Borland创建的DLL可以在MSVC里被调用变得非常困难,但并非不可能的。这篇文章描述了一套指导方针,你可以跟着制作与Microsoft兼容的BCB DLL。我们讨论四种不同的技术。三种采用引入库隐式连接调用,一种在运行时利用显式连接。

指导方针摘要

你可以跟着下面的指导方针摘要列表建造你的DLL。第1个列表讨论隐式连接;第2个列表描述显式连接;第3种技术采用#define组装隐式连接;最后一个例子利用假的MSVC DLL工程为__stdcall函数创建引入库。

技术1: 隐式连接

------------------------------------------------------------------------------

1- 使用__cdecl调用习惯代替__stdcall。

2- 导出简单的"C"风格函数,没有C++类或成员函数。

3- 确定你有一个 extern "C" {} 包围你的函数原型。

4- 创建DEF文件,包含与Microsoft兼容的导出函数别名。别名也就是不包含前面的下划线。

DEF文件内容如下:

EXPORTS

; MSVC name = Borland name

Foo = _Foo

Bar = _Bar

5- 把DEF文件加入到你的工程里重新编译它。

6- 把DLL和DLL头文件拷贝到你的MSVC工程目录里。

7- 运行impdef为DLL创建第2个DEF文件。这个DEF文件用来创建引入库。

> impdef mydll.def mydll.dll

8- 运行Microsoft的LIB工具,用上一步创建的DEF文件创建COFF引入库。调用格式为:

> lib /DEF mydll.def

9- 把用LIB.EXE创建的LIB文件添加到你的MSVC工程里。

技术2: 显式连接

------------------------------------------------------------------------------

1- 使用__cdecl或__stdcall,如果你使用__stdcall可以跳过第4,5步。

2- 导出简单的"C"风格函数,没有C++类或成员函数。

3- 确定你有一个extern "C" {}包围你的函数原型。

4- 如果你使用__cdecl,那么你可能想去掉导出函数前面的下划线,但你不必这么做。你可以用例1的第4,5步去掉下划线。如果你没有去掉下载线,在调用GetProcAddress函数时函数名必须前面的下划线。

5- 把DLL拷贝到MSVC工程目录里。

6- 在MSVC应用程序中,使用LoadLibrary API函数载入DLL。

7- 调用GetProcAddress API在DLL里查找你想要的调用函数,保存GetProcAddress函数返回的函数指针。当你想调用函数的时候,提取函数指针。

8- 当你用完DLL时调用FreeLibrary。

技术3: 用#define组装隐式连接

------------------------------------------------------------------------------

1- 用__cdecl调用习惯代替__stdcall。

2- 导出简单的"C"风格函数,没有C++类或成员函数。

3- 确定你有一个extern "C" {}包围你的函数原型。

4- 在你的DLL头文件里,为每一个导出函数名创建一个#define。

#define会调用预编译器在每一个函数名前加上下划线。因为我们只想为MSVC创建别名,所以代码检查_MSC_VER。

#ifdef _MSC_VER

#define Foo _Foo

#define Bar _Bar

#endif

5- 把DLL和DLL头文件拷贝到MSVC工程目录里。

6- 运行impdef为DLL函数DEF文件。

> impdef mydll.def mydll.dll

7- 使用Microsoft的LIB工具为DEF文件创建COFF格式的引入库。

>lib /def mydll.def

8- 把LIB.EXE创建的LIB文件添加到MSVC工程里。

技术4: 用__stdcall函数隐式连接

------------------------------------------------------------------------------

1- 当建造你的DLL时使用__stdcall调用习惯。

2- 导出简单的"C"风格函数,没有C++类或成员函数。

3- 确定你有一个extern "C" {}包围你的函数原型。

4- 为MSVC创建一个引入库。这一部分比较困难。你不能用LIB.EXE为__stdcall函数创建引入库。你必须创建一个由MSVC编译的的假的DLL。这样做,按这些步骤:

4a- 用MSVC创建一个不使用MFC的DLL

4b- 从BCB里拷贝覆盖DLL头文件和DLL源代码

4c- 编辑你的DLL源代码,抛开每一个例程的函数体部分,使用一个假的返回值返回

4d- 配置MSVC工程生成的DLL,采用和BCB DLL同的的名字

4e- 把DEF文件添加到MSVC工程,禁止它对__stdcall命名进行修饰(_Foo@4)

5- 编译第4步得到的虚假DLL工程。这将会生成一个DLL(你可以把它丢到垃圾筒里)和一个LIB文件(这是你需要的)。

6- 把从第5步得到的LIB文件添加到你需要调用这个BCB DLL的MSVC工程里。LIB文件会确保连接。为MSVC可执行文件配置BCB DLL(不是虚假DLL)。

注意:

一般情况下,隐式连接比显式连接要优先考虑,因为对程序员来说隐式连接更简单,而且它是类型安全的(错误发生在连接时而不是运行时)。不管用哪种方法,当你在编译器间共享DLL时,如果你选择坚持使用隐式连接,就必须为每一个编译器创建兼容的引入库。创建兼容的引入库比用显式连增加的负担就是要注意更多的要求。

注意:

如果你想使你的DLL可以被Visual Basic的开发者使用,显式连接的指导方针同样适用。如果你想把你的DLL给VC开发者,按显式连接的指导方针,采用__stdcall调用习惯。

下面4个部分详细描述每一种技术。

例1: 显式连接

这个例子详细描述了上一部分技术1的指导方针。技术1的指针方针可以分为两组。1-5项处理在BCB这边编译DLL;6-9项处理在MSVC这边使用DLL。我们将沿这条主线分别进行讨论。

在这个例子里,我们将用BCB建造一个DLL,它导出两个函数: Foo和Bar。两个函数都返回一个整型值。函数原型为:

int Foo (int Value);

int Bar (void);

然后我们在MSVC里建造一个测试EXE,用来调用Borland DLL。

用BCB编译DLL

下面两个程序清单包含我们的DLL源代码。清单1要在BCB和MSVC之间共享的头文件;清单2包含我们的DLL函数实现部分。创建一个BCB DLL工程,从清单1和2中拷贝代码粘贴到工程里。或者你可以下载这篇文章的源代码以节省时间。BCB DLL工程已经为你设置好了。(参见最下面的下载部分)

// ----------------------------------------------

// Listing 1- DLL header file

#ifndef BCBDLL_H

#define BCBDLL_H

#ifdef __cplusplus

extern "C" {

#endif

#ifdef BUILD_DLL

#define IMPORT_EXPORT __declspec(dllexport)

#else

#define IMPORT_EXPORT __declspec(dllimport)

#endif

IMPORT_EXPORT int __cdecl Foo (int Value);

IMPORT_EXPORT int __cdecl Bar (void);

#ifdef __cplusplus

}

#endif

#endif

// ----------------------------------------------

// ----------------------------------------------

// Listing 2- DLL source code

#include <windows.h>

#pragma hdrstop

#define BUILD_DLL

#include "bcbdll.h"

int __cdecl Foo (int Value)

{

return Value + 1;

}

int __cdecl Bar (void)

{

static int ret = 0;

return ret++;

}

// ----------------------------------------------

关于头文件有两个要注意的地方。首先,观察我们用 extern "C" 的方法确保函数名不会被C++编译器改编;其次,注意到在我们建造DLL时,导出函数有一个特殊指示的前缀__declspec(dllexport)。当我们从MSVC里使用DLL时,函数前缀变为__declspec(dllimport)。这个指示的改变是通过IMPORT_EXPORT宏定义实现的。

最后,注意我们显式声明了__cdecl为调用习惯。技术上,我们可以省略__cdecl关键字,因为__cdecl已经是默认的。但是,我想不管怎样把它列出来是一个好习惯。通过列出调用习惯,你显式的告诉人们你选择了__cdecl作为一个前提。同样,默认的调用习惯在两个编译器里可以通过编译开关改变。你肯定不想这些编译器开关影响到你DLL的可用性。

头文件本身满足了指导方针中的1-3项 。我们需要做的下一件事情是处理第4项: 给导出函数建立别名。

首先,按现在的情况建造DLL代码。其次,运行TDUMP工具检查函数的函数名确实包含前面的下划线。

c:> tdump -m -ee bcbdll.dll

Turbo Dump Version 5.0.16.12 Copyright (c) 1988, 2000 Inprise Corporation

Display of File BCBDLL.DLL

EXPORT ord:0001='_Bar'

EXPORT ord:0002='_Foo'

EXPORT ord:0003='___CPPdebugHook'

注意:

使用TDUMP时别忘了用 -m 开关。TDUMP尝试反改编(unmangle)被修饰的名字,使他们更容易阅读。但是,当你查看一个DLL的时候,明智的选择是查看函数的原始格式。-m 开关告诉TDUMP显示原始函数名。

像你看到的那样,Foo和Bar都包含前端下划线。至于__CPPdebugHook,你可以不理它,它是幕后操纵的,当它不存在好了。它对你没什么意义,你也不能让它走开,因此就不要把它放在心上了。

为了用别名去掉下划线,我们需要做三件事:首先创建DLL的DEF文件;然后调整DEF文件,为Borland名字创建MSVC的别名;最后,把DEF文件添加到你的BCB工程里,重建DLL。

要创建DEF文件,对DLL运行Borland的IMPDEF工具。

C:> impdef bcbdllx.def bcbdll.dll

我选择bcbdllx.def为文件名,因为稍后(在我们创建MSVC引入库之前)我们将使用其它DEF文件。我想避免两者混淆。bcbdllx.def内容如下:

LIBRARY BCBDLL.DLL

EXPORTS

_Bar @1 ; _Bar

_Foo @2 ; _Foo

___CPPdebugHook @3 ; ___CPPdebugHook

注意到在Foo和Boo前端的下划线。如果DLL把Foo和Bar导出为_Foo和_Bar,当MSVC用户设法建造他们的工程的时候,将看到连接错误。我们需要剥去下划线。我们用在DEF文件里给函数别名的方法实现。

DEF文件别名允许我们为真实的函数导出担当代理或占位符的函数名。在DLL里的真实的函数仍然是_Foo和_Bar。代理名将是Foo和Bar(注意没有了下划线)。当我们给两个函数别名的时候,DLL将导出两个将的符号,它们归诸于原来的函数。

完成别名, 编辑DEF文件,改变成下面的样子:

LIBRARY BCBDLL.DLL

EXPORTS

Bar = _Bar

Foo = _Foo

这个DEF文件创建两个新的出口,Foo和Bar,它们分别担当_Foo和_Bar的点位符。把这个DEF文件保存到你的硬盘上。一旦你完成了这些工作,便可以把DEF文件添加到你的BCB工程里,使用Project-Add菜单项。添加后,BCB会在工程管理器(Project Manager)的树状结构里显示出DEF文件。

一旦你把DEF文件加入到工程里,做一次完全的重建。工程连接好之后,再次对DLL运行TDUMP,检查从DLL里导出的带下划线函数。

>tdump -m -ee bcbdll.dll

Turbo Dump Version 5.0.16.12 Copyright (c) 1988, 2000 Inprise Corporation

Display of File BCBDLL.DLL

EXPORT ord:0004='Bar'

EXPORT ord:0005='Foo'

EXPORT ord:0002='_Bar'

EXPORT ord:0001='_Foo'

EXPORT ord:0003='___CPPdebugHook'

对TDUMP的输出有两点要注意的事情要注意。首先,观察Foo和Bar到场了(没有前端下划线)。现在DLL导出函数名与MSVC的一致了。还注意到原来的函数,_Foo和_Bar,还在那儿。被修饰过的函数仍就从DLL里导出。使用DEF文件别名并不隐藏原来的函数。

你可能会想把这原来的两个函数用什么办法隐藏起来。但是,这么做将会危害到从BCB工程里使用DLL的人们。记得BCB的连接器期望在那儿有一个前端下划线。如果你真的用了什么方法从DLL里把_Foo和_Bar隐藏了(以我的知识是不可能实现的),那么你的DLL从BCB里调用将变得非常困难。

如果TDUMP的输出没有列出代理函数(不带下划线的函数),那么返回上一步,检查你的DEF文件。在你可以继续之前,你需要得到别名的出现。如果DLL看起来OK了,那么该是转到MSVC这边的时间了。

从MSVC里调用DLL

一旦你拥有了一个被反改编__cdecl函数出口的DLL模型,下一步就是要为MSVC用户生成一个引入库。为这,你将需要刚刚创建的DLL,使用Borland的IMPDEF实用工具(再一次),和来自MSVC的LIB.EXE工具。第一步是创建DLL的DEF文件。为这,我建议你拷贝DLL和DLL头文件到你的MSVC工程目录里,在那儿工作。

C:> impdef bcbdll.def bcbdll.dll

IMPDEF将创建一个DEF文件,内容如下:

C:> impdef bcbdll.def bcbdll.dll

LIBRARY BCBDLL.DLL

EXPORTS

Bar @4 ; Bar

Foo @5 ; Foo

_Bar @2 ; _Bar

_Foo @1 ; _Foo

___CPPdebugHook @3 ; ___CPPdebugHook

打开DEF文件,改变它的内容为:

LIBRARY BCBDLL.DLL

IMPORTS

Bar @4 ; Bar

Foo @5 ; Foo

注意到我们移除了包含下划线的函数,和调试钩挂(debug hook)函数。我们还把EXPORT改成了IMPORTS,因为我们现在是在引入函数,而不是导出它们(我怀疑它对MSVC LIB.EXE来说会产生不同)。

下一步,我们用Microsoft LIB.EXE,从DEF文件那儿创建一个COFF格式的库。语法为:

lib /DEF:bcbdll.def /out:bcbdll_msvc.lib

注意:

MSVC命令行实用工具在默认情况下不在你的配置的路径里。你可能需要运行一个MSVC带的批处理文件,使得LIB.EXE可以被直接调用。批处理文件叫做VCVARS32.BAT,它位于DevStudio安装路径的\VC\BIN子目录下。

这里,所有艰苦的工作都做完了。现在你需要做就是把你的DLL,MSVC LIB文件,和DLL文件件加入到你的MSVC客户端。要使用DLL,需要添加LIB文件到MSVC工程里,并且在源代码内#include DLL头文件。

我准备了一个MSVC的简单工程来证明上面的概念。清单3给出客户端DLL的源代码。没什么特别的地方,就是一个main函数,一个DLL头文件的#include,和对DLL的几个函数调用。主要是你正确的添加了引入库,由LIB.EXE生成的那个,添加到MSVC工程里。

// ----------------------------------------------

// Listing 3- MSVC DLL client code

#include <iostream>

#include <windows.h>

using namespace std;

#include "bcbdll.h"

int main()

{

cout << "Foo(10) = " << Foo(10) << endl;

cout << "Bar() = " << Bar() << endl;

cout << "Bar() = " << Bar() << endl;

cout << "Bar() = " << Bar() << endl;

return 0;

}

// ----------------------------------------------

例2:显式连接

这个例子向你展示了如何从MSVC里使用显式连接调用BCB编译的DLL。用显式连接,你不必摆弄创建一个MSVC兼容的引入库。显示连接不利的是它需要在用户端做更多的工作,它不及隐式连接类型安全,错误被延期到运行时而不是连接时。虽然显式连接有许多不利因素,但在某些情况下它还是十分有用的。

在这个例子里,我们将创建一个DLL,它导出两个函数:Foo和Bar。函数的原型同上一个例子一样。

int Foo (int Value);

int Bar (void);

这一显式连接的指导方针与隐式连接的相仿。我们需要导出简单的C函数,需要防止C++名字改编。如果我们用__cdecl调用习惯,那么我们可能想要为BCB导出的函数建立别名,以去掉它们前端的下划线。如果我们选择不用别名去掉下划线的方法,那么当按名字载入函数时,我们必须包含下划线。换句话说,当你对__cdecl函数起作用时,你必须在某几点上处理下划线。你也可以在BCB建造DLL的时候处理下划线,或者在运行时调用DLL时处理它。我们利用__stdcall代替__cdecl以回避整个讨论的下划线问题。这是我们在这个例子里要做的。清单4和5给出的我们DLL的源代码。

注意:

如果你导出__stdcall函数,至关紧要的是要让客户端应用程序知道。一些人容易犯一个错误,认为使用__stdcall只不过是去掉了__cdecl函数前面的下划线。别掉进这个陷井。__stdcall函数处理堆栈方式也__cdecl是不同的。如果客户端应用程序把__stdcall当作__cdecl函数调用(也就是,堆栈将被破坏,客户端程序会死得很难看),将要发生一些错误。

// ----------------------------------------------

// Listing 4- DLL header file

#ifndef BCBDLL_H

#define BCBDLL_H

#ifdef __cplusplus

extern "C" {

#endif

#ifdef BUILD_DLL

#define IMPORT_EXPORT __declspec(dllexport)

#else

#define IMPORT_EXPORT __declspec(dllimport)

#endif

IMPORT_EXPORT int __stdcall Foo (int Value);

IMPORT_EXPORT int __stdcall Bar (void);

#ifdef __cplusplus

}

#endif

#endif

// ----------------------------------------------

// ----------------------------------------------

// Listing 5- DLL source code

#include <windows.h>

#define BUILD_DLL

#include "bcbdll.h"

int __stdcall Foo (int Value)

{

return Value + 1;

}

int __stdcall Bar (void)

{

static int ret = 0;

return ret++;

}

// ----------------------------------------------

注意这段代码几乎与隐式连接的一模一样。唯一不同的地方就是把Foo和Bar的调用习惯改成__stdcall代替__cdecl。

现在让我们看一下调用DLL的MSVC程序代码。代码如清单6所示。

// ----------------------------------------------

// Listing 6- MSVC client code

#include <iostream>

#include <windows.h>

using namespace std;

HINSTANCE hDll = 0;

typedef int (__stdcall *foo_type) (int Value);

typedef int (__stdcall *bar_type) ();

foo_type Foo=0;

bar_type Bar=0;

void DLLInit()

{

hDll = LoadLibrary("bcbdll.dll");

Foo = (foo_type)GetProcAddress(hDll, "Foo");

Bar = (bar_type)GetProcAddress(hDll, "Bar");

}

void DLLFree()

{

FreeLibrary(hDll);

}

int main()

{

DLLInit();

cout << "Foo() = " << Foo(10) << endl;

cout << "Bar() = " << Bar() << endl;

cout << "Bar() = " << Bar() << endl;

cout << "Bar() = " << Bar() << endl;

DLLFree();

return 0;

}

// ----------------------------------------------

这段代码片段里有许多需要消化的地方。首先也是最重要的,观察代码本身是编译器中立的。你可以在BCB或MSVC里编译它。我首先在BCB里编译它,确信它可以按我所想的工作。

第二,注意到代码没有为#include bcbdll.h操心。有一个重要的原因。bcbdll.h为Foo和Bar函数定义的原型。但是,我们不把我们的代码同任何预先定义的那些原型连接。通常,这些原型的存根来自引入库。但是这个例子示范的是显式连接,当你显示地连接时,是不使用引入库的,在头文件里的Foo和Bar原型对我们来说没多大意义。

第三件要注意的事情是关于这段代码里出现的typedef和函数指针,位于源文件的顶部附近。晃式连接需要你在运行时用API GetProcAddrress得到DLL函数的地址。你必须把GetProcAddress返回的结果存储到某个地方。最好的地点是把结果存储到函数指针里。通过把函数地址存储到函数指针里,你可以使用正常的函数调用语法调用函数(如 Foo(10))。

typedef声明创建了两个新的类型: foo_type和bar_type。它们都是函数指针类型。foo_type声明了一个指向__stdcall函数的类型,这个函数打官腔一个整型参数,返回一个整型值。bar_type定义了一个指向__stdcall类型的、没有参数、有一个整型返回值的函数。这些typedef产生了两个效果。第一,它们提供了清晰的方式来声明函数指针变量Foo和Bar。第二,它们使我们可以很方便的转换GetProcAddress返回的结果。从GetProcAddress返回的结果是一个指向__stdcall类型的、没有参数、有一个整型返回值的函数。除非你的函数与这个格式相同,否则你需要转换GetProcAddress的结果(这个转换是显式连接比隐式连接缺管类型安全的原因)。

在typedef的下面有两个变量Foo和Bar。这两个是函数指针变量。它们会保存我们想要调用的两个函数的地址。注意这些变量的名字是任意的。我选择Foo和Bar是为了使代码像隐式连接。不要犯这样的错误,Foo和Bar变量名没有与DLL里的真实函数建立连接。我们可以把变量命名为Guido和Bjarne,如果你想的话。

在函数指针声明下面,你会看到两个叫DllInit和DllFree的函数实体。这两个实体处理载入DLL,查找导出函数,和在我们使用赛后释放程序库。用这种方法,其余的代码不知道DLL是显式连接的。它可以像往常一样调用Foo和Bar(或者Guido和Bjarne,如果你改变了名字)。唯一要协调的是你必须在调用任何DLL程序之前调用DllInit。我们也应当细致的,调用DllFree释放程序库。

注意:

当在命名总题上Borland编译器和Microsoft编译器之间大战之时,GetProcAddress是你的最后一道防线。这包括Borland __cdecl命名带一个前端下划线(如 _Foo)。也包括改编C++名字。如果有人支持你用改编函数名字的DLL,你可以永远传递这些难看的参数,把改编名字给GetProcAddress。不管你实际上你能调用函数而没碰到其它的什么问题,但是至少你将会有一个机会。

这就是全部。在MSVC里编译代码,你就完成了。你不必摆弄DEF文件或是引入库。但是在你这边的代码里有些琐碎的工作要处理。

例3: 用#define组装隐式连接

这个例子展示了可能是从MSVC工程里调用BCB DLL最简单的一种方法,但它也可能是最没有吸引力的一种方法。代码使用一个狡诈的#define,当检查到是Microsoft编译器时给__cdecl函数前加上下划线。也就是说,我们简单的#define了Foo为_Foo。

这种技术的优势在于我们不必实行任何别名。 我们能直接导出包含下划线的__cdecl函数。但是,我们仍就必须用Microsoft的LIB.EXE创建一个MSVC兼容的引入库。

这种技术是关键是MSVC不期望__cdecl函数有任何的修饰(见表1)。它们应当和看起来一样。 如果MSVC应用程序试图执行一个__cdecl函数Foo,它期望在DLL里查找一个没有下划线的函数Foo。如果我们改变MSVC的代码,让它调用_Foo,那么它将试图在DLL里查找一个叫做_Foo的函数。

Borland给__cdecl函数前加上了下划线。我们可以哄骗MSVC,让它在调用函数的时候在函数名的前面加一个下划线。紧记我们只想在MSVC这边添加一个下划线,而不是Borland这边。

#define组装的DLL代码与例1里清单2的代码完全一样。唯一不同的是DLL头文件。当检测到是MSVC时,DLL头文件为每一个函数原型加一个下划线。清单7展示了修改后的头文件。

// ----------------------------------------------

// Listing 7- DLL header file

#ifndef BCBDLL_H

#define BCBDLL_H

#ifdef __cplusplus

extern "C" {

#endif

#ifdef BUILD_DLL

#define IMPORT_EXPORT __declspec(dllexport)

#else

#define IMPORT_EXPORT __declspec(dllimport)

#endif

// #define kludge. If we are being compiled with MSVC, then just tack on a

// leading underscore because Borland C++ will export Foo and Bar as _Foo

// and _Bar respectively

#ifdef _MSC_VER

#define Foo _Foo

#define Bar _Bar

#endif

IMPORT_EXPORT int __cdecl Foo (int Value);

IMPORT_EXPORT int __cdecl Bar (void);

#ifdef __cplusplus

}

#endif

#endif

// ----------------------------------------------

在头文件里,除#define组装之外,你还必须创建一个MSVC兼容的引入库。你可以按前面的步骤完成。对编译好的DLL运行IMPDEF,得到一个DEF文件。然后运行Microsoft LIB.EXE工具创建一个COFF格式的引入库。这时,你不必考虑去编辑DEF文件。最后,拷贝DLL,COFF引入库,和DLL文件件到你的MSVC工程里。把LIB文件添加到你的MSVC工程里,重建。

这是创建MSVC引入库的命令行例子。注意我们不必编辑DEF文件。我们刚好可以把它传递给LIB.EXE。

// Create def file

> impdef bcbdll.def bcbdll.dll

// create COFF import library using MS lib.exe

>lib /def bcbdll.def

例4: 用__stdcall函数隐式连接

在我们进行之前,让我们调查一下为什么我们需要单独论述__stdcall函数。MSVC没有提供与Borland的IMPLIB相当的工具。你不能攫取DLL,生成一个MSVC可用的引入库。最接近的工具是LIB.EXE,它可以通过一个DEF文件创建一个引入库。DEF文件必须是手动创建,或利用Borland的IMPDEF工具生成的。

没什么大不了的啊?你仍能创建MSVC引入库,只是必须通过中间步骤创建一个DEF文件,然后把它传递给LIB.EXE工具。正确的,在你采用__cdecl函数的时候。当你转到__stdcall的时候,问题就发生了。问题是Microsoft的LIB.EXE工具为导出__stdcall函数的DLL生成引入库显得无能为力。

因为这个原因,我把用__stdcall隐式连接分离出来作为它自己的一部分。我们需要跟着一个不同步骤的次序来创建Microsoft兼容的引入库。(同样注意到我把这部分放到最后的好理由,至少这些步骤是冗长乏味的)。

既然我们不能用LIB.EXE为用__stdcall的BCB DLL生成引入库,那我们需要提出一种不同的策略。有一种生成引入库的方法(可能是唯一的方法),依靠只要你建造一个DLL的,MSVC就可以生成一个引入库这一事实。如果你建造一个包含__stdcall函数的MSVC DLL,编译器和连接器会正确的分解导出的__stdcall函数,生成引入库。

那么你会问它会怎么帮助我们呢?毕竟,我们正在用Borland C++编译DLL。在MSVC里创建一个DLL工程有什么好处?我们想让EXE用MSVC编译,但是DLL应当保持在BCB这边。这个问题的答案是我们在MSVC里编译虚假DLL工程,唯一的目的是生成一个__stdcall的引入库。由MSVC创建的DLL可以被丢到垃圾筒里。我们不需要它。

这种技术是建立在虚假DLL工程的基础之上的。我们在MSVC里创建一个虚假DLL工程,就是得到生成Microsoft兼容的引入库的好处。于是我们可以把这个引入库和BCB生成的DLL相结合,再提供给MSVC用户,使得他们可以调用我们的带有__stdcall函数的Borland DLL。

这是这种技术所必须的几步。首先,用BCB编译你的DLL。用__stdcall调用习惯,导出简单的C函数,用extern "C"包装所有的声明。DLL的代码与例2中清单4和5的代码相同,因此我不把它们再列出来了。第二步是在MSVC里创建虚假DLL工程。编译虚假DLL工程,盗取生成的引入库。最后一步是把这个引入库添加到任一想要调用Borland DLL的MSVC工程里。

这一技术最有挑战兴趣的是围绕虚假DLL工程和引入库的基因。建造一个虚假DLL工程,用MSVC创建一个non-MFC DLL工作区。编辑MSVC工程设置,以便使生成DLL的函数与BCB DLL的名字相匹配(在我们的例子里是bcbdll.dll)。这个设置可以在Project-Settings-Link下找到。从Borland工程目录里拷贝你的DLL头文件源代码到虚假DLL工程目录。如果你的工程由多个CPP文件组成,那么只需拷贝包含导出声明的头文件。把CPP源代码文件添加到虚假工作区。

下一步,进入每一个导出函数的定义,删除每个函数实体的代码。以一堆空函数告终。如果函数有返回值,在适当的位置保留返回语句。只是一些虚假的返回值(比如0)。除丢弃函数体之外,移除任何不必要的#include语句(你应当可以移大部分#include,因为所有的函数体都是空的)。

我们的BCB DLL与例2的清单4和5包含同样的代码。 清单8展示了同样的代码被修整下来后的版本。这个修整下来后的版被添加到虚假DLL工作区。

// ----------------------------------------------

// Listing 8- dummy DLL source code

#define BUILD_DLL

#include "bcbdll.h"

int __stdcall Foo (int Value)

{

return 0;

}

int __stdcall Bar (void)

{

return 0;

}

// ----------------------------------------------

这时,我们应当可以在MSVC里编译虚假DLL工作。但是在我们编译之前,我们必须再实行一步,以抗击Microsoft编译器的一些特性。我们的虚假DLL导出__stdcall函数。当Microsoft DLL导出__stdcall函数时,通常都给函数名做了修饰,添加了前端下划线,附加了'@'符号和一个数字的结尾(见文章开始处的表1)。例如,Foo将被导出为_Foo@4。这不是我们想要的行为。虚假DLL的全部的目的就是为我们的BCB DLL生成MSVC引入库。我们的BCB DLL包含简单的、没有下划线的、__stdcall函数(Foo和Bar)。它没有给生成引入库带来任何好处,因为与修饰的名字(_Foo@4和_Bar@0)不匹配 DLL包含简单的、没有下划线的、__stdcall函数(Foo和Bar)。它没有给生成引入库带来任何好处,因为与修饰的名字(_Foo@4和_Bar@0)不匹配。

幸运地,我们可以防止MSVC修饰虚假__stdcall函数,方法是添加一个DEF文件到虚假DLL工程里。DEF文件简单的列出每一个要导出的函数。内容如下:

LIBRARY BCBDLL.DLL

EXPORTS

Bar

Foo

注意:

在DEF文件里的程序库名应当与由MSVC生成的虚假DLL名字相匹配,而它转而应当与用BCB创建的DLL名字相匹配。如果这三项不匹配,那么你会运行出各种不同的错误(通常是未解决的连接器错误)。

把DEF文件添加到虚假DLL工程里,建造虚假DLL。当MSVC建造DLL工程的时候,它会创建一个引入库。这个引入库是让MSVC用户可以用隐式连接的方式调用导出__stdcall函数的BCB DLL的关键因素,把它连同你的DLL一起提供给MSVC用户。你的用户应当把这个引入库添加到任何调用你的BCB DLL的MSVC工程里。

结论

这篇文章提供了四种技术,让Microsoft Visual C++可以调用由BCB编译的DLL。我希望这篇文章把每一种技术描述的很充分(这些内容有的不太容易理解)。为了帮助你理解每一种技术,我为每一种可代利用的技术制作了例子代码,可以从这儿下载。zip文件解压出四个子目录,一个对应一种技术。每一个子目录包含一个用BCB5 DLL工程的DLL目录,和一个用来调用BCB DLL的MSVC工程的EXE目录。MSVC工程是VC++ 5工程,但它们应当可以在MSVC 6下正常的工作。

下载

这篇文章的下载

bcbdll.zip

所有4种技术的源代码 (130 kB).

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有