作为一种简单易用的Windows开发环境,Visual Basic从一推出就受到了广大编程人员的欢迎。它使 程序员不必再直接面对纷繁复杂的Windows消息,而可以将精力主要集中在程序功能的实现上,大大提高了编程效率。但凡事有利必有弊。 Windows动态连接库是包含数据和函数的模块,可以被其它可执行文件(EXE、DLL、OCX 等)调用。动态连接库包含两种函数:输出(exported)函数和内部(internal)函数。输出函数可以被其它模块调用,而内部函数则只能在动态连接库内部使用。尽管动态连接库也能输出 数据,但实际上它的数据通常是只在内部使用的。使用动态连接库的优点是显而易见的。将应 用程序的一部分功能提取出来做成动态连接库,不但减小了主应用程序的大小,提高了程序 运行效率,还使它更加易于升级。多个应用程序共享一个动态连接库还能有效地节省系统资 源。正因为如此,在Windows系统中,动态连接库得到了大量的使用。
(一)、指定动态库: Declare语句中的Lib子句用来告诉Visual Basic如何找到包含过程的.dll文件。 如果引用的过程属于Windows核心库(User32、Kernel32或GDI32),则可以不包含文件扩展名,如:
对于其它动态连接库,可以在Lib子句指定文件的路径: Declare Function lzCopy Lib "c:windowslzexpand.dll" _ (ByVal S As Integer, ByVal D As Integer) As Long ①.exe文件所在的目录 ②当前目录 ③Windows系统目录 ④Windows目录 ⑤Path环境变量中的目录 下表中列出了常用的操作系统环境库文件。 动态链接库描述 Advapi32.dll高级API服务,支持大量的API(其中包括许多安全与注册方面的调用) Comdlg32.dll通用对话框API库 Gdi32.dll图形设备接口API库 Kernel32.dllWindows32位核心的API支持 Lz32.dll32位压缩例程 Mpr.dll多接口路由器库 Netapi32.dll32位网络API库 Shell32.dll32位ShellAPI库 User32.dll用户接口例程库 Version.dll版本库 Winmm.dllWindows多媒体库 Winspool.drv后台打印接口,包含后台打印API调用。 对于Windows的系统API函数,可以利用VB提供的工具API Viewer查找某一函数及其相 关数据结构和常数的声明,并复制到自己的程序中。 (二)、使用别名: Declare语句中的Alias子句是一个可选的部分,用户可以通过它所标识的别名对动态 库中的函数进行引用。例如,在下面的语句中,声明了一个在VB中名为MyFunction的函数,而它在动态库Mydll.dll中最初的名字是MyFunctionX。 Private Declare Function MyFunction Lib "Mydll.dll" _ Alias "MyFunctionX" ( ) As Long 需要注意的是,Alias子句中的函数名是大小写敏感的,也就是说,必须与函数在生成时的声明(如在C源文件中的声明)一致。这是因为32位动态库与16位动态库不同,其中的函数名是区分大小写的。同样道理,如果没有使用Alias子句,那么在Function(或Sub)后的函数名也是区分大小写的。 通常在以下几种情况时需要使用Alias子句: 如果调用的系统Windows API过程要使用字符串,那么声明语句中必须增加一个Alias 子句,以指定正确的字符集。包含字符串的系统Windows API函数实际有两种格式:ANSI和Unicode( 关于ANSI和Unicode两种字符集的区别将在后面详细阐述)。因此,在Windows头文件中,每 个包含字符串的函数都同时有ANSI版本和Unicode版本。例如,下面是SetWindowText函数 的两种C语言描述。可以看到,{dy}个描述将函数定义为SetWindowTextA,尾部的"A" 表明它是一个ANSI函数: WINUSERAPI BOOL WINAPI SetWindowTextA(HWND hWnd, LPCSTR lpString); 第二个描述将它定义为 SetWindowTextW, 尾部的"W" 表明它是一个 Unicode 函数: WINUSERAPI BOOL WINAPI SetWindowTextW(HWND hWnd, LPCWSTR lpString); 因为两个函数实际的名称都不是"SetWindowText",要引用正确的函数就必 须增加一个Alias子句: Private Declare Function SetWindowText Lib "user32" _ 应当注意,对于VB中使用的系统WindowsAPI函数,应该指定函数的ANSI版本,因为只 有WindowsNT才支持Unicode版本,而Windows95不支持这个版本。仅当应用程序只运行 在WindowsNT平台上的时候才可以使用Unicode版本。 B.函数名是不标准的名称 有时,个别的DLL过程的名称不是有效的标识符。例如,它可能包含了非法的字符(如连 字符),或者名称是VB的关键字(如GetObject)。在这种情况下,可以使用Alias关键字。例 如,操作环境DLLs中的某些过程名以下划线开始。尽管在VB标识符中允许使用标识符,但是 下划线不能作为标识符的{dy}个字符。为了使用这种过程,必须先声明一个名称合法的过程, 然后用Alias子句引用过程的真实名称: Declare Function lopen Lib "kernel32" Alias "_lopen" _ 在上例中,lopen是VB中使用的过程名称。而_lopen则是动态连接库中可以识别的名 称。 C.使用序号标识DLL过程 除了使用名称之外,还可以使用序号来标识DLL过程。某些动态连接库中不包含过程的名称,在声明它们包含的过程时必须使用序号。同使用名称标识的DLL过程相比,如果使用序号,在最终的应用程序中消耗的内存将比较少,而且速度会快些。但是,一个具体的API的序号 在不同的操作系统中可能是不同的。例如GetWindowsDirectory在Win95下的序号为432,而在WindowsNT4.0下为338。总而言之,如果希望应用程序能够在不同的操作系统下运行,那么{zh0}不要使用序号来标识API过程。如果过程不属于API,或者应用程序使用的范围很有 限,那么使用序号还是有好处的。 要使用序号来声明DLL过程,Alias子句中的字符串需要包含过程的序号,并在序号的 前面加一个数字标记字符(#)。例如,Windowskernel中的GetWindowsDirectory函数的序 号为432;可以用下面的语句来声明该DLL过程: Declare Function GetWindowsDirectory Lib "kernel32" _ 在这里,可以使用任意的合法名称作为过程的名称,VB将用序号在DLL中寻找过程。 为了得到要声明的过程的序号,可以使用Dumpbin.exe等实用工具(Dumpbin.exe是Microsoft VisualC++提供的一个实用工具,它的使用说明可以参见VC的文档)。利用Dumpbin,可以提取出.dll文件中的各种信息,例如DLL中的函数列表,它们的序号以及与代码有关的其它信息。
(三)、使用值或引用传递 在缺省的情况下,VB以引用方式传递所有参数(ByRef)。这意味着并没有传递实际的参 数值,VB只传递了数据的32位地址。另外有许多DLL过程要求参数以值方式传递(ByVal)。这意味着它们需要实际的数据,而不是数据的内存地址。 Declare Function InvertRect Lib "user32" Alias _ 动态连接库的参数传递是一个复杂的问题,也是VB中调用动态连接库时最容易出现错误的地方。参数类型或传递方式的声明错误都可能导致应用程序出现GPF(通用保护错误),甚至使操作系统崩溃,因此我们将在后面专门详细地讨论这个问题。 (四)、灵活的参数类型 某些DLL过程的同一个参数能够接受多种数据类型。如果需要传递多种类型的数据,可 以将参数声明为AsAny,从而取消类型限制。例如,下面的声明中的第三个参数(lpptAsAny) 既可以传递一个POINT结构的数组,也可以传递一个RECT结构: Declare Function MapWindowPoints Lib "user32" Alias _ AsAny子句提供了一定的灵活性,但是,由于它不进行任何的类型检查,风险也随之增 加。因此在使用AsAny子句时,必须仔细检查所有参数的类型。 正确的函数声明是在VB中调用动态连接库的前提,但要想在VB中用对、用好动态库中的 函数,仅仅有声明还是远远不够的。前面已经说过,由于VB不能验证应用程序传递到动态连接 库中的参数值是否正确,因此就要求程序员应对参数类型有非常详细的了解,否则很容易引 起应用程序发生通用保护错或导致潜在的Bug,降低软件的可靠性。下面将参数类型分为简单数据类型、字符串、和用户自定义类型三种分别进行讨论。 (1)、简单数据类型: 简单数据类型是指Numeric数据类型(包括Integer、Long、Single、Double、Currency类型)、Byte数据类型和Boolean数据类型。它们的共同的特点是结构简单,操作系统在处理时不必进行特殊的转换。 简单数据类型参数的传递比较简单。我们知道,在VB中传递参数的方式有两种:传值(Byval) 和传址(ByRef),缺省的方式是传址。所谓传值,就是对一个变量的具体值进行传递;而传址则 是传递变量的地址。例如,在VB程序中需要将一个整型变量m=10的值传进动态库,如果用传值 方式,那么传进动态库的值就是10,而在传址方式下,传入的则是变量m的地址,相当于C/C++ 中&m的值。需要注意的是,以传值方式传进动态连接库的变量,其值在动态库中是不能 被改变的;如果需要在动态连接库中修改传入参数的值,则必须使用传址方式。一般来说,在VB 和动态连接库之间传递单个的简单数据类型,只要注意了以上几个方面就可以了。当需要将 一个简单数据类型的整个数组传进动态库时,必须将相应参数声明为传址方式,然后把数组 的{dy}个元素作为参数传入,这样在动态连接库中就得到了数组的首地址,从而可以对整个 数组进行访问。例如,声明了一个名为ReadArray的DLL过程,要求传入一个整型数组aArray: Declare Function ReadArray Lib "mydll.dll" _ 在调用时可以采用如下方式: Dim ret,I(5) as Integer 将整个数组传入动态连接库 与简单数据类型相比,字符串类型(String、String*n)的参数传递要复杂得多,这主要是Windows 98 API和VB使用的字符串类型不同的缘故。VB使用被称为BSTR的String数据类型,它是由自动化(以前被称为OLE Automation)定义的数据类型。一个BSTR由头部和字符串组成,头部包含了字符串的长度信息,字符串中可以包含嵌入的null值。大部分的BSTR是 Unicode的,即每个字符需要两个字节。BSTR通常以两字节的两个null字符结束。下图表示 了一个BSTR类型的字符串。
Function GetCharByte(ByVal On Sub StrToByte(StrToChange As String, ByteArray() As Byte) count = 0 For i = LowBound To UpBound For i = LowBound To UpBound If (On Sub ChangeStrAryToByte(StrAry() LowBound = LBound(StrAry) For i = LowBound To UpBound 下面看一个转换的例子: DimResultAry()asByte 当转换完成以后,查看字节数组ResultAry,其中包含了21个元素,依次是:178,226,202,212,49,0,178,226,202,212,50,50,50,0,178,226,202,212,51,51,0。其中,[178,226]是"测"的字节码,[202,112]是"试"的字节码,49,50,51 分别为字符1、2、3的ASCII码。可见,经过转换后,字符串数组中的各个元素按顺序放在了字节数组中,相互间以终止符0分隔。 这样,字符串数组就全部转换成了字节数组,然后只要将字节数组的{dy}个元素以传址的方式传入动态连接库,DLL过程就可以正确地访问数组中的所有字符串了。但是,使用这种方法,当DLL过程处理结束返回VB时,VB得到的仍然是字节数组。如果需要在VB中再次得到该字节数组表示的字符串,还要把整个字节数组重新以0为分割符分成多个子数组(每个子数组都对应原来字符串数组中的一个元素),然后使用VB函数StrConv将每个子数组转换成字符串(转换时第二个参数选vbUnicode),就可以显示或进行其它操作了。例如,其中一个子数组的名字是SubAry,则函数StrConv(SubAry,vbUnicode)就返回了它所对应的字符串。 总之,VB应用程序和动态库间字符串参数的传递是一个比较复杂的过程,使用时要非常谨慎。同时应尽可能避免传递字符串数组类型的参数,因为这很容易引起下标越界、堆栈溢出等严重错误。 用户自定义类型在VB中是一种重要的数据类型,它为编程者提供了很大的灵活性,使开发人员可以根据需要构造自己的数据结构。它相当于C/C++中的结构类型(structure)。在VB中,允许程序员以传址的方式将自定义数据类型参数传入动态库,DLL过程也可以将修改后的参数返回VB程序。但是,在VB中仍然不支持以传值的方式传递用户自定义类型参数。 传递用户自定义类型参数时,必须确保VB中的数据类型的成员与动态库中的结构成员是一一对应的,所占空间也必须严格一致。这里所说的一一对应,不仅是指VB 中的所有结构成员在动态库的结构中都必须有对应的元素,而且它们在数据结构中定义的顺序也必须严格一致,这是VB中使用的"数据结构成员对齐方式"决定的。在VB 中,数据结构使用双字对齐方式(4-byte alignment),因此,在用户自己生成用于VB 调用的动态连接库时,也必须把编译选项"structure member alignment" 设为4字节(如前文所述)。 所谓结构成员对齐方式是指一个数据结构内部,其成员的排列方式。譬如,在VB中,其对齐方式是4字节,这就好象在一个数据结构内部分成了很多个4字节大小的小单元,如果相邻 两个或多个数据成员的大小可以放在一个单元中,那么就放在一起;否则这些小单元中可能 会出现未用的空字节。我们来看下面一个数据类型: Type TestType 它的三个成员的大小加起来是2+1+4=7。但是,由于m1和m2的字节总长度是3,小于4,它 们就存放于一个单元中;但该单元剩下的一个字节不足以放下一个Long型的成员m3,于是m3 就被放在下一个单元中,它们之间就有了一个未用的空字节;因此,整个结构所占实际长度是8 字节。同理,如果将m3和m2的位置交换一下,它所占的尺寸就变成了9字节。可见,成员在结构 中的声明顺序也是非常重要的。 通常,当一个用户自定义类型中不包含字符串时,向动态连接库中传递该类型的参数是没有什么问题的。如果只传递一个自定义类型变量,则既可以传递该变量名,也可以传递该变 量的{dy}个成员,它们的效果是一样的,都是将该变量的地址传进了动态库;同样,如果要传递一个自定义类型的数组,则既可以传递该数组的{dy}个元素,也可以传递{dy}个元素的{dy}个成员。但是,如果用户自定义类型中包含字符串类型时,又该如何与动态连接库传递参数呢?答案是令人遗憾的:在VB中,你无法将一个包含字符串成员的用户自定义类型变量或数 组安全、正确地传入动态库中。如果你这样做了,即使某次侥幸得到了正确的结果,在其背后也隐藏着许多致命的危险。因此,如果一定要在用户自定义类型中包含字符串变量,并且该类型的变量又要作为参数传入动态库时,你{zh0}修改类型定义,把其中的字符串成员用相应的字节数组类型替换掉(转换方法可参见前文),这样就可以在VB 和动态库间传递这种类型的参数了。 另外,在VB 中还可以把一个函数的指针传递到动态库中,方法也并不复杂。但笔者强烈建议{zh0}不要这么做,因为这样一来VB 应用程序就几乎xx丧失了它所应有的安全性。如果 确实需要传递函数指针的话,那么还是编一个C/C++ 的程序来完成这项工作吧。 总之,在VB中调用DLL过程是一个比较复杂的问题,编程人员必须很好地把握,才能达到既提高了程序效率,开拓了程序功能,又不降低程序安全性的目的。另外需要特别指出的一点是,在本文中提到的所有动态连接库,都是指没有使用自动化(OLE Automation)技术的动态库,Windows API和大多数用户自编的动态连接库都是这种类型的。对于使用了OLE Automation技术的动态连接库,其参数传递的方式有所不同,读者可以参阅有关OLE 技术的书籍,在此不再涉及。 Function GetCharByte(ByVal On Sub StrToByte(StrToChange As String, ByteArray() As Byte) count = 0 For i = LowBound To UpBound For i = LowBound To UpBound If (On Sub ChangeStrAryToByte(StrAry() LowBound = LBound(StrAry) For i = LowBound To UpBound 下面看一个转换的例子: DimResultAry()asByte 当转换完成以后,查看字节数组ResultAry,其中包含了21个元素,依次是:178,226,202,212,49,0,178,226,202,212,50,50,50,0,178,226,202,212,51,51,0。其中,[178,226]是"测"的字节码,[202,112]是"试"的字节码,49,50,51 分别为字符1、2、3的ASCII码。可见,经过转换后,字符串数组中的各个元素按顺序放在了字节数组中,相互间以终止符0分隔。 这样,字符串数组就全部转换成了字节数组,然后只要将字节数组的{dy}个元素以传址的方式传入动态连接库,DLL过程就可以正确地访问数组中的所有字符串了。但是,使用这种方法,当DLL过程处理结束返回VB时,VB得到的仍然是字节数组。如果需要在VB中再次得到该字节数组表示的字符串,还要把整个字节数组重新以0为分割符分成多个子数组(每个子数组都对应原来字符串数组中的一个元素),然后使用VB函数StrConv将每个子数组转换成字符串(转换时第二个参数选vbUnicode),就可以显示或进行其它操作了。例如,其中一个子数组的名字是SubAry,则函数StrConv(SubAry,vbUnicode)就返回了它所对应的字符串。 总之,VB应用程序和动态库间字符串参数的传递是一个比较复杂的过程,使用时要非常谨慎。同时应尽可能避免传递字符串数组类型的参数,因为这很容易引起下标越界、堆栈溢出等严重错误。 用户自定义类型在VB中是一种重要的数据类型,它为编程者提供了很大的灵活性,使开发人员可以根据需要构造自己的数据结构。它相当于C/C++中的结构类型(structure)。在VB中,允许程序员以传址的方式将自定义数据类型参数传入动态库,DLL过程也可以将修改后的参数返回VB程序。但是,在VB中仍然不支持以传值的方式传递用户自定义类型参数。 传递用户自定义类型参数时,必须确保VB中的数据类型的成员与动态库中的结构成员是一一对应的,所占空间也必须严格一致。这里所说的一一对应,不仅是指VB 中的所有结构成员在动态库的结构中都必须有对应的元素,而且它们在数据结构中定义的顺序也必须严格一致,这是VB中使用的"数据结构成员对齐方式"决定的。在VB 中,数据结构使用双字对齐方式(4-byte alignment),因此,在用户自己生成用于VB 调用的动态连接库时,也必须把编译选项"structure member alignment" 设为4字节(如前文所述)。 所谓结构成员对齐方式是指一个数据结构内部,其成员的排列方式。譬如,在VB中,其对齐方式是4字节,这就好象在一个数据结构内部分成了很多个4字节大小的小单元,如果相邻 两个或多个数据成员的大小可以放在一个单元中,那么就放在一起;否则这些小单元中可能 会出现未用的空字节。我们来看下面一个数据类型: Type TestType 它的三个成员的大小加起来是2+1+4=7。但是,由于m1和m2的字节总长度是3,小于4,它 们就存放于一个单元中;但该单元剩下的一个字节不足以放下一个Long型的成员m3,于是m3 就被放在下一个单元中,它们之间就有了一个未用的空字节;因此,整个结构所占实际长度是8 字节。同理,如果将m3和m2的位置交换一下,它所占的尺寸就变成了9字节。可见,成员在结构 中的声明顺序也是非常重要的。 通常,当一个用户自定义类型中不包含字符串时,向动态连接库中传递该类型的参数是没有什么问题的。如果只传递一个自定义类型变量,则既可以传递该变量名,也可以传递该变 量的{dy}个成员,它们的效果是一样的,都是将该变量的地址传进了动态库;同样,如果要传递一个自定义类型的数组,则既可以传递该数组的{dy}个元素,也可以传递{dy}个元素的{dy}个成员。但是,如果用户自定义类型中包含字符串类型时,又该如何与动态连接库传递参数呢?答案是令人遗憾的:在VB中,你无法将一个包含字符串成员的用户自定义类型变量或数 组安全、正确地传入动态库中。如果你这样做了,即使某次侥幸得到了正确的结果,在其背后也隐藏着许多致命的危险。因此,如果一定要在用户自定义类型中包含字符串变量,并且该类型的变量又要作为参数传入动态库时,你{zh0}修改类型定义,把其中的字符串成员用相应的字节数组类型替换掉(转换方法可参见前文),这样就可以在VB 和动态库间传递这种类型的参数了。 另外,在VB 中还可以把一个函数的指针传递到动态库中,方法也并不复杂。但笔者强烈建议{zh0}不要这么做,因为这样一来VB 应用程序就几乎xx丧失了它所应有的安全性。如果 确实需要传递函数指针的话,那么还是编一个C/C++ 的程序来完成这项工作吧。 总之,在VB中调用DLL过程是一个比较复杂的问题,编程人员必须很好地把握,才能达到既提高了程序效率,开拓了程序功能,又不降低程序安全性的目的。另外需要特别指出的一点是,在本文中提到的所有动态连接库,都是指没有使用自动化(OLE Automation)技术的动态库,Windows API和大多数用户自编的动态连接库都是这种类型的。对于使用了OLE Automation技术的动态连接库,其参数传递的方式有所不同,读者可以参阅有关OLE 技术的书籍,在此不再涉及。 |