在 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 #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 工程里调用 DLL 函数的更多相关文章
- 在 C++Builder 工程里使用 Visual C++ DLL(3个工具) good
译者序: 第一次读这篇文章是在 2001 年 10 月,帮我解决了一点小问题.本来不好意思翻译,因为英语水平实在太差.最近发现不少网友在问在 C++Builder 的工程里调用 Visual C++ ...
- c++ 调用DLL函数,出现错误
c++ 调用DLL函数,出现错误 Run-Time Check Failure #0 - The value of ESP was not properly saved across a funct ...
- 一个简洁通用的调用DLL函数的帮助类
本次介绍一种调用dll函数的通用简洁的方法,消除了原来调用方式的重复与繁琐,使得我们调用dll函数的方式更加方便简洁.用过dll的人会发现c++中调用dll中的函数有点繁琐,调用过程是这样的:在加载d ...
- 动态调用DLL函数有时正常,有时报Access violation的异常
动态调用DLL函数有时正常,有时报Access violation的异常 typedef int (add *)(int a,int b); void test() { hInst=LoadL ...
- (原创)一个简洁通用的调用DLL函数的帮助类
本次介绍一种调用dll函数的通用简洁的方法,消除了原来调用方式的重复与繁琐,使得我们调用dll函数的方式更加方便简洁.用过dll的人会发现c++中调用dll中的函数有点繁琐,调用过程是这样的:在加载d ...
- C++利用模板在Windows上快速调用DLL函数
更新日志 --------- 2021/08/01 更新V2.2 增加 GetHmodule 函数 - 允许用户获取HMODULE以验证加载DLL是否成功. 2021/08/03 更新V2.3 增加 ...
- C++ Builder创建和调用dll中的资源
程序开发中经常会用到一些图标.图片.光标.声音等,我们称它们为资源(Resource).当多个窗口用到同样的资源时,可以将这些公共的资源放到一个dll文件里调用,这样,由于定位资源比在磁盘中定位文件花 ...
- GO语言 -- 调用DLL函数,填平所有的坑,最详尽攻略
编译dll文件(源代码c++):g++ -shared main.cpp -o test.dll set GOARCH=386 第一个DLL函数,第一个参数,要求传入一个指针,直接指向[]byte类型 ...
- C#中通过调用Dll函数时,执行一段时间后,就会报内存可能被破坏的错的解决办法
遇到同样的问题,已经解决的:http://blog.csdn.net/youxiazzz12/article/details/24313347
随机推荐
- 万万没想到,3D打印居然可以做这些逆天设计
3D打印一直被冠以“高科技”头衔,似乎离我们的日常生活还很遥远.其实不然,随着技术的创新,3D打印技术逐渐深入各个领域,工业生产.商业.医学.建筑.艺术等领域都能看到3D打印技术的影子.它将会改变我们 ...
- 水果项目第1集-想法>需求->功能->数据库设计->类设计
懒,懒人,我是个懒人. 懒人想做点事,总是拖拖拉拉,迟迟没有开始. 很久很久以前,就想做属于自己的产品,但是至今还没有一个属于自己的产品. 两年前,终于想好,要做一个网上卖水果的系统,手机上点点,水果 ...
- android text
"@you bang--- go on -------" 需要做分享内容,前面有段格式固定写死,同时颜色为灰色:后面的内容可以编辑,颜色为黑色,同时支持多行 有人用textview ...
- Intellij Idea 12 开发Android 报Caused by: java.lang.UnsatisfiedLinkError: FindLibrary return null;
这次开发是用的百度地图api,导入两个so文件,结果启动的时候总是报Caused by: java.lang.UnsatisfiedLinkError: findlibrary return null ...
- Android之Fragment学习总结(1)
对于Fragment的学习: 近日初步学习了Fragment这以特殊的组件,其依托与一个Activity,与Activity的生命周期息息相关,为其设置的视图只有当其关联到一个Activity才会起效 ...
- Uploadify 上传文件插件详解
Uploadify 上传文件插件详解 Uploadify是JQuery的一个上传插件,实现的效果非常不错,带进度显示.不过官方提供的实例时php版本的,本文将详细介绍Uploadify在Aspnet中 ...
- STL之map应用 +hash表(51nod 1095)
题目:Anigram单词 题意:给出词典,再给出一些单词,求单词的Anigram数量. 思路:先将字串转换成哈希表,然后再用map链接. hash表构造方法汇总:http://www.cnblogs. ...
- MySQL MHA配置
MySQL环境: master:192.168.202.129:3306 slave:192.168.202.129:3307,192.168.202.129:3307,192.168.202.130 ...
- VM - Bridge Adapter
如何让外部可以连到本地的虚拟机. 1. 网络模式 - Bridged Adapter 2. 确保本机插上网线 3. 如果虚拟机是 Windows 8.1, 需要开启如下选项.
- vim 空格 制表符
set tabstop=4 设定tab宽度为4个字符set shiftwidth=4 设定自动缩进为4个字符set expandtab 用space替代tab的输入 ...