DLL(dynamic-link library)是一个包含一个或多个被编译、链接的函数/模块的文件, 并和调用它的进程分开存储。当使用者进程启动或运行时,操作系统负责把DLL映射到调用者进程的内存地址空间里。采用DLL技术,可以提高软件模块的重用性(DLL调用者可以不太关心该DLL是用何种语言写的)并起到保护源代码的目的,同时又能减少最终生成的可执行文件的大小。
DLL包含一个导出函数表,它包含了函数在DLL内的地址,但当客户程序装入DLL时并不知道要调用的函数地址,它是通
过函数的符号化名字和序号来识别这些函数的。简单的程序通常是一个EXE从一个或多个DLL导入函数。
C/C++兼具低、高级语言的特点于一身,同时又有别的语言无法取代的特点,因此,C/C++语言写的DLL在处理某些
问题时显的尤为重要。同时由于编译器的不同和其他原因,编译器并未为跨语言调用DLL做太多工作。
在讨论VB(Pascal、Fortran)中调用C/C++DLL前,有必要先谈一下函数调用约定(Function Calling
Convention)和名字修饰(Name Decoration)。
1 函数调用约定(Function Calling Convention)
函数调用约定确定一个程序如何实现一个函数调用及参数如何传递。在单一语言
程序中,调用约定几乎总是正确的,这是因为对所有模块总有一个缺省的约定并且头文件会负责调用和被调用程序之间的一
致性。在一个混合语言程序中,不同的语言不可能分享同一个头文件。由于调用约定而产生的错误在编译时无法发现,直到
程序在运行时(run-time)实现函数调用时才出现并会立即导致应用程序崩溃。
VC++所支持的调用约定如下表:
关键字 | 栈的维护者 | 参数传递 |
__cdecl | 调用者 | 参数反序入栈(右->左) |
__stdcall | 被调用者 | 参数反序入栈(右->左) |
__fastcall | 被调用者 | 参数先存寄存器,接着入栈 |
thiscall(并非关键字) | 被调用者 | 参数入栈,this指针存ECX |
表1 VC++所支持的调用约定
我们还可以看下Fortran的调用约定和C/C++的调用约定的异同,如下表所示:
调用约定(Calling Convention) | 参数传递 | 栈的维护 |
C/C++ | 参数反序入栈(右->左) | 调用者 |
Fortran(__stdcall) | 参数反序入栈(右->左) | 被调用者 |
表2 C/C++和Fortran的调用约定
在C/C++中,可以在函数声明或定义时用关键字__stdcall指定调用约定。__stdcall调用约定经常在Windows程序
或API函数中使用。在GUI程序中,PASCAL、WINAPI和CALLBACK都被定义为__stdcall。C语言的缺省调用约定为__cdecl。
2 名字修饰(Name Decoration)
名字修饰是编译器在编译函数定义或原型时产生一个独一无二的字符串用以标识该函数的过程。C/C++程序在内部
是以修饰后的名字被标识的。当我们指定链接一个函数的时候需要名字修饰,在大多数情况下,我们不需要关心函数的名字
修饰,链接器或别的工具自会处理以使函数调用相匹配。如果想了解函数名字修饰的适用情况,请参阅你使用的工具的说明
文档。
在VC中,我们编译包含函数定义或原型的源文件后,可以通过如下两种方法之一查看函数的名字修饰的结果。
1) 使用编译清单
①在编译包含函数声明或原型的源文件时使用编译参数(/FA[c|s]),把编译参数设置为如下三者之一:
·机器码、汇编(/FAc)
·源码、汇编(/FAs)
·机器码、源码、汇编(/FAcs)
②在结果清单中找到包含未修饰的函数定义所在的行。
③检查前一行,PROC NEAR命令的标签即为经编译器修饰的函数名字。
2) 使用DUMPBIN工具
对.lib或.obj文件使用带/SYMBOLS参数选项的DUMPBIN命令,在输出文件中找到未经修饰的函数定义(在一对括号之
内),其前就是经编译器修饰的函数名字。DUMPBIN的使用如下:
dumpbin libfilename /symbols /out:outfilename
对于如下的C++函数原型:
int MyFunc(int a,double b)
变换其调用约定和连接指定符,观察其经VC6.0+SP6编译器生成的名字修饰,如下表:
| -------- | __cdecl | __fastcall | __stdcall |
extern “C” | _MyFunc | _MyFunc | @MyFunc@12 | _MyFunc@12 |
---------- | ?MyFunc@@ YAHHN@Z | ?MyFunc@@ YAHHN@Z | ?MyFunc@@ YIHHN@Z | ?MyFunc@@ YGHHN@Z |
表3 C++中名字修饰与调用约定的关系
3 VC中和VB等语言的通信
为了使VB程序(或用Pascal、Fortran语言写的程序)能够调用C/C++语言写的DLL中的函数,这些函数必须用正确的
函数调用约定,而避免由编译器所做的任何名字修饰。在Windows 3.x和16-位版本的C++中,可以使用关键词
__pascal定义导出函数。然而,32-位版本C++不再提供关键词__pascal。作为替代,在WINDEF.H头文件中PASCAL被
定义为__stdcall。这样,函数调用约定正确(被调用函数管理栈并且参数按从右向左的先后顺序传递),却产生了函
数名字修饰。因此,当用__declspec(dllexport)从C++DLL中导出函数时,导出的是经编译器修饰的名字而不是我们
所期望的PASCAL约定的名字(未经修饰的名字、全部字母大写)。
PASCAL名字修饰是字母大写、未加修饰的名字符号。__stdcall名字修饰是将名字符号前加下划线(_),并且后
加”@”和函数参数字节数(所需栈空间)。所以,如下所示的C++函数声明:
int __stdcall func(int a, double b);
将被编译器修饰为:
_func@12
C调用约定(__cdecl)把上述函数名字修饰为_func。然而,PASCAL约定的函数名应为FUNC。
为了得到修饰后的名字,可以在VC6.0的Project/Settings选项卡中的Category下拉列表中选择General,然后选
择Generate mapfile复选按钮。选择__declspec(dllexport)会使编译器做如下的工作:
·如果函数用C调用约定(__cdecl)导出,当名字被导出时被除去开头的下划线(_)。
·如果函数不用C调用约定导出,(比如,用__stdcall),就导出如上所述的修饰后的名字。
因此,为了模拟PASCAL名字修饰和函数调用约定,必须使用__stdcall所提供的“被调用函数管理栈及参数传递规
则”和未加修饰的大写符号名字。
我们不得不考虑栈的维护与管理,所以,必须选择__stdcall。同时又要使__stdcall约定所导出的符号名字符合
PASCAL约定,就必须通过在.DEF文件的EXPOTRS字段使用别名指定导出的符号名字。如下所示:
函数声明:
int __stdcall MyFunc(int a , double b);
void __stdcall InitCode(void);
在.DEF文件中:
EXPORTS
MYFUNC=_MyFunc@12
INITCODE=_InitCode@0
对于将要在VB4.0以后版本写的32-位应用程序中调用的DLL来讲,需要在.DEF文件中使用如文中所示的别名方法。
如果在VB程序中使用别名,.DEF文件中的指定别名就不再需要。在VB中,可以通过alias关键词用如下语句使用别名(DLL
的.DEF文件就不再需要使用别名):
Declare Function MyFunc Lib “dlllibname” Alias “_MyFunc@12” (…) As Integer
其中dlllibname是C++编译器生成的DLL文件的名字。
4 将字符串传递到 DLL 过程
通常,字符串应该使用 ByVal 方式传递到 APIs。Visual Basic 使用被称为 BSTR 的 String 数据类型,它是由自动化(以前被称为 OLE自动化)定义的数据类型。一个 BSTR 由头部和字符串组成,头部包含了字符串的长度信息,字符串中可以包含嵌入的 null 值。BSTR 是以指针的形式进行传递的,因而 DLL 过程能够修改字符串。(指针是一个变量,包含另外一个变量的内存地址,而不是数据。) BSTR 是 Unicode 的,即每个字符需要两个字节。BSTR 通常以两字节的 null 字符结束。
DLL 中的大部分过程(以及 Window API 中的所有过程)能够识别 LPSTR 类型,这是指向标准的以 null 结束的 C 语言字符串的指针,它也被称为 ASCIIZ 字符串。LPSTR 没有前缀。下图显示了一个指向 ASCIIZ 字符串的 LPSTR。
如果 DLL 过程需要一个 LPSTR(指向以 null 结束的字符串的指针)作为参数,可以将 BSTR 以使用值方式传递给它。因为指向 BSTR 的指针实际指向以 null 值结束的字符串的第一个数据字节,对于 DLL 过程来说,它就是一个 LPSTR。
通常,如果 DLL 过程需要 LPSTR 参数,那么使用 ByVal 关键字。如果 DLL 需要得到指向 LPSTR 的指针,则使用引用方式传递 Visual Basic 字符串。
如果要将二进制数据传递到 DLL 过程,可以将变量作为 Byte 数据类型的数组传递,不要将其作为 String 变量。字符串是假定用来包含字符的,如果将二进制数据作为 String 变量传递,外部程序可能无法正确读入数据。
假设声明了一个字符串变量,但没有初始化它,如果将其以使用值方式传递到 DLL,该字符串变量将作为 NULL 传递,而不是作为空字符串 ("")。为了消除代码中的混淆,如果要将 NULL 传递到 LPSTR 参数,请使用 vbNullString 常数。
5 将数组传递到 DLL 过程中
可以传递数组中的单个元素,方法与传递同类型的变量相同。在传递单个元素时,元素被作为基本类型的变量进行传递。
有时需要将整个数组传递到 DLL 过程中。如果 DLL 过程是专门为自动化编写的,那么将数组传递到 DLL 过程的方式与传递到 Visual Basic 过程是相同的:加上空的括号。因为 Visual Basic 使用包括 SAFEARRAY 在内的自动化数据类型,DLL 要得到 Visual Basic 数组参数就必须是能接受自动化的。关于更进一步的信息,请参考特定 DLL 的文档。
如果 DLL 过程不能直接接受自动化 SAFEARRAYs,数值数组仍可以整个进行传递:以引用方式传递数组的第一个单元。因为数值数组数据总是按顺序放在内存中,因此这种办法是可行的。只需要将数组的第一个元素传递到 DLL 过程,该 DLL 就能够访问数组的所有单元。
6 将函数指针传递到 DLL 和类型
熟悉 C 语言的程序员一定会熟悉函数指针的概念。对于不熟悉 C 语言的读者,有必要对此进行一番解释。函数指针是一种约定,程序员可以用它将一个自定义的函数的地址作为参数传递到另一个函数。后面一个函数可以不是自己编写的,但是已经进行了声明,所以可以在应用程序中使用。利用函数指针,可以调用 EnumWindows 等函数列出系统中打开的窗口,利用 EnumFontFamilies 列出所有的当前字体。利用函数指针还可以访问 Win32 API 中的其它许多函数,早期的 Visual Basic 没有提供对它们的支持。
在 Visual Basic 5.0 中,使用函数指针时仍然存在若干限制。
使用 AddressOf 关键字
如果代码要调用 Visual Basic 5.0 的函数指针,则必须将该代码放到标准的 .BAS 模块中,不可以将其放到类模块中,也不能将其附加到窗体上。在使用 AddressOf 关键字声明函数时,必须注意下列事项:
· AddressOf 只能紧接在是参数列表中的参数前;该参数可以是自定义的过程、函数或者属性的名字。
· 写在 AddressOf 后面的过程、函数、属性必须与有关的声明和过程在同一个工程中。
· AddressOf 只能用于自定义的过程、函数和属性,不能将其用于 Declare 语句声明的外部函数,也不能将其用于类型库中的函数。
· 在声明的 Sub、Function 或自定义的类型定义中,可以将函数指针传递到 As Any 或 As Long 类型的参数。
注意 可创建用 Visual C++ (或类似的工具)编译的 DLL 中的回调函数原型。要使用 AddressOf 时,原型必须使用 __stdcall 调用约定。不能将缺省调用约定与 AddressOf 并用
在变量中存储函数指针
在某些情况下,在将函数指针传递到 DLL 之前需要将其存储在一个中间变量中。如果需要将函数指针从一个 Visual Basic 传递到另一个,这种做法是很有用的。例如,在调用 RegisterClass 之类的函数时就需要用结构 (WndClass) 的成员来传递函数指针。
要将一个函数指针赋予结构中的一个成员,需要编写一个包装函数(wrapper)。例如,下面创建的 FnPtrToLong 就是一个包装函数,使用它可以将函数指针放入任何结构中:
Function FnPtrToLong (ByVal lngFnPtr As Long) As Long
FnPtrToLong = lngFnPtr
End Function
要使用该函数,首先需要声明类型,然后再调用 FnPtrToLong。AddressOf 加上回调函数的名字作为函数的参数。
Dim mt as MyType
mt.MyPtr = FnPtrToLong(AddressOf MyCallBackFunction)
7 将自定义的数据类型传递到 DLL 过程
某些 DLL 过程使用自定义的类型作为参数。(在 C 语言中,自定义类型被称为“结构”,在 Pascal 中被称为“记录”。)正如数组的情况,如果要传递自定义类型的单个成员,只需将其作为一般的数值或字符串变量即可。
可以将整个的自定义类型作为一个参数传递,这时需要使用引用方式。自定义类型不能以使用值方式传递。Visual Basic 将传递第一个成员的地址,在内存中其它成员存储在第一个的后面。在有的操作系统上,成员之间可能存在空隙。
自定义类型的成员可以是对象、数组以及 BSTR 字符串,尽管接受自定义类型的大部分 DLL 过程不希望自定义类型中包含字符串数据。如果字符串成员是固定长度的字符串,它们对于 DLL 将等同于以空字符结束的字符串,在内存中的存储方式也与其它值相同。自定义类型中的变长字符串实际是一组指向字符串数据的指针。每个变长字符串成员需要 4 个字节。
注意 如果要将包含二进制数据的自定义类型传递到 DLL 过程中,需要将二进制数据储存在 Byte 数据类型的变量数组中,不要将其存储在 String 变量中。字符串是假定用来存储字符的,如果将二进制数据作为 String 变量传递,外部过程可能读到错误的结果。