在VB 中调用动态连接库 |
作为一种简单易用的Windows开发环境,Visual Basic从一推出就受到了广大编程人员的欢迎。它使 程序员不必再直接面对纷繁复杂的Windows消息,而可以将精力主要集中在程序功能的实现上,大大提高了编程效率。但凡事有利必有弊。VB中高度的封装和模块化减轻了编程者的负担,同时也使开发人员失去了许多访问低层API函数和直接与Windows交互的机会。因此,相比而言,VB应用程序的执行效率和功能比C/C++或Delphi生成的程序要差。为了解决这个问题,在一个大型的VB开发应用中,直接调用Windows API函数几乎是不可避免的;同时,还有可能需 要程序员自己用C/C++等开发一些动态连接库,用于在VB中调用。本文主要讨论在32位开发环 境Visual Basic 5.0中直接调用Windows 95 API函数或用户生成的32位动态连接库的方法 与规则。 Windows动态连接库是包含数据和函数的模块,可以被其它可执行文件(EXE、DLL、OCX 等)调用。动态连接库包含两种函数:输出(exported)函数和内部(internal)函数。输出函数可以被其它模块调用,而内部函数则只能在动态连接库内部使用。尽管动态连接库也能输出 数据,但实际上它的数据通常是只在内部使用的。使用动态连接库的优点是显而易见的。将应 用程序的一部分功能提取出来做成动态连接库,不但减小了主应用程序的大小,提高了程序 运行效率,还使它更加易于升级。多个应用程序共享一个动态连接库还能有效地节省系统资 源。正因为如此,在Windows系统中,动态连接库得到了大量的使用。 一般来说,动态连接库都是以DLL为扩展名的文件,如Kernel32.dll、commdlg.dll等。但也有例外,如16位Windows的核心部件之一GDI.exe其实也是一个动态库。编写动态连接库的工具很多,如VisualC++、BorlandC++、Delphi等,具体方法可以参见相关文档。下面只以Visual C++5.0为例,介绍一下开发应用于VisualBasic5.0的动态连接库时应注意的问题(本文中所有涉及C/C++语言或编译环境的地方,都以VC5为例;所有涉及VisualBasic的地方都以VB5 为例)。 作为一种32位Windows应用程序的开发工具,VB5生成的exe文件自然也都是32位的,通常情况下也只能调用32位的动态连接库。但是,并不是所有的32位动态库都能被VB生成的exe 文件正确地识别。一般来说,自己编写用于VB应用程序调用的动态连接库时,应注意以下几个方面的问题: 1、生成动态库时要使用__stdcall调用约定,而不能使用缺省的__cdecl调用约定;__stdcall 约定通常用于32位API函数的调用。 2、在VC5中的定义文件(.def)中,必须列出输出函数的函数名,以强制VC5系统将输出函数的装饰名(decoratedname)改成普通函数名;所谓装饰名是VC的编译器在编译过程中生成的输出函数名,它包含了用户定义的函数名、函数参数及函数所在的类等多方面的信息。由于在VC5中定义文件不是必需的,因此工程不包含定义文件时VC5就按自己的约定将用户定义的输出函数名修改成装饰名后放到输出函数列表中,这样的输出函数在VB生成的应用程序中是不能正确调用的(除非声明时使用Alias子句)。因此需要增加一个.def文件,其中列出用户需要的函数名,以强制VC5不按装饰名进行输出。 3、VC5中的编译选项"结构成员对齐方式(structure member alignment)" 应设成4字节,其原因将在后文详细介绍。 4、由于在C中整型变量是4个字节,而VB中的整型变量依然只有2个字节,因此在C中声 明的整型(int)变量在VB中调用时要声明为长整型(long),而C中的短整型(short)在VB中则 要声明成整型(integer);下表针对最常用的C语言数据类型列出了与之等价的Visual Basic 类型(用于32位版本的Windows)。 |
A.处理使用字符串的系统Windows API过程
如果调用的系统Windows API过程要使用字符串,那么声明语句中必须增加一个Alias 子句,以指定正确的字符集。包含字符串的系统Windows API函数实际有两种格式:ANSI和Unicode( 关于ANSI和Unicode两种字符集的区别将在后面详细阐述)。因此,在Windows头文件中,每 个包含字符串的函数都同时有ANSI版本和Unicode版本。例如,下面是SetWindowText函数 的两种C语言描述。可以看到,第一个描述将函数定义为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" _ Alias "SetWindowTextA" (ByVal hwnd As Long,ByVal _ lpString As String) As Long |
应当注意,对于VB中使用的系统WindowsAPI函数,应该指定函数的ANSI版本,因为只 有WindowsNT才支持Unicode版本,而Windows95不支持这个版本。仅当应用程序只运行 在WindowsNT平台上的时候才可以使用Unicode版本。
B.函数名是不标准的名称
有时,个别的DLL过程的名称不是有效的标识符。例如,它可能包含了非法的字符(如连 字符),或者名称是VB的关键字(如GetObject)。在这种情况下,可以使用Alias关键字。例 如,操作环境DLLs中的某些过程名以下划线开始。尽管在VB标识符中允许使用标识符,但是 下划线不能作为标识符的第一个字符。为了使用这种过程,必须先声明一个名称合法的过程, 然后用Alias子句引用过程的真实名称:
Declare Function lopen Lib "kernel32" Alias "_lopen" _ (ByVal lpPathName As String,ByVal iReadWrite _ As Long) As Long |
在上例中,lopen是VB中使用的过程名称。而_lopen则是动态连接库中可以识别的名 称。
C.使用序号标识DLL过程
除了使用名称之外,还可以使用序号来标识DLL过程。某些动态连接库中不包含过程的名称,在声明它们包含的过程时必须使用序号。同使用名称标识的DLL过程相比,如果使用序号,在最终的应用程序中消耗的内存将比较少,而且速度会快些。但是,一个具体的API的序号 在不同的操作系统中可能是不同的。例如GetWindowsDirectory在Win95下的序号为432,而在WindowsNT4.0下为338。总而言之,如果希望应用程序能够在不同的操作系统下运行,那么最好不要使用序号来标识API过程。如果过程不属于API,或者应用程序使用的范围很有 限,那么使用序号还是有好处的。
要使用序号来声明DLL过程,Alias子句中的字符串需要包含过程的序号,并在序号的 前面加一个数字标记字符(#)。例如,Windowskernel中的GetWindowsDirectory函数的序 号为432;可以用下面的语句来声明该DLL过程:
Declare Function GetWindowsDirectory Lib "kernel32" _ Alias "#432" (ByVal lpBuffer As String,_ ByVal nSize As Long) As Long |
在这里,可以使用任意的合法名称作为过程的名称,VB将用序号在DLL中寻找过程。
为了得到要声明的过程的序号,可以使用Dumpbin.exe等实用工具(Dumpbin.exe是Microsoft VisualC++提供的一个实用工具,它的使用说明可以参见VC的文档)。利用Dumpbin,可以提取出.dll文件中的各种信息,例如DLL中的函数列表,它们的序号以及与代码有关的其它信息。
(2)、字符串参数的传递:
与简单数据类型相比,字符串类型(String、String*n)的参数传递要复杂得多,这主要是Windows 95 API和VB使用的字符串类型不同的缘故。VB使用被称为BSTR的String数据类型,它是由自动化(以前被称为OLE Automation)定义的数据类型。一个BSTR由头部和字符串组成,头部包含了字符串的长度信息,字符串中可以包含嵌入的null值。大部分的BSTR是 Unicode的,即每个字符需要两个字节。BSTR通常以两字节的两个null字符结束。下图表示 了一个BSTR类型的字符串。
(前缀)aTest/0
头部BSTR指向数据的第一个字节
另一方面,大部分的DLL过程(包括Windows 95 API中的所有过程)使用LPSTR类型字符串,这是指向标准的以null结束的C语言字符串的指针,它也被称为ASCIIZ字符串。LPSTR 没有前缀。下图显示了一个指向ASCIIZ字符串的LPSTR。
aTest/0
LPSTR指向一个以null结尾的字符串数据的第一个字节
如果DLL过程需要一个LPSTR(指向以null结束的字符串的指针)作为参数,可以在VB 中将一个字符串以传值的方式传递给它。因为指向BSTR的指针实际指向以null值结束的字符串的第一个数据字节,所以对于DLL过程来说,它就是一个LPSTR。这样传入动态连接库的字符串,DLL过程也可以对它进行修改,尽管它是以传值方式传入的。只有当DLL过程需要一个指向LPSTR的指针时,才以传址的方式传入字符串,这时DLL过程得到的是一个指向字符串指针的指针(相当于C/C++中的char**),而不是通常所用的字符串的首地址(相当于C/C++中的char*)。
当需要把一个字符串数组整个传入动态连接库时,情况就变得复杂多了,用传递简单数据类型数组的方式来传递字符串数组是行不通的。当我们以传值的方式将一个字符串数组的第一个元素传进动态连接库时,DLL过程得到的实际上是该元素压入堆栈段后的地址,而不是数据段中整个数组的首地址。也就是说,这时DLL过程只能得到数组的第一个元素,而无法访问整个数组。而以传址方式传入第一个元素时,DLL过程只能得到指向该元素在堆栈段中地址的指针,同样无法访问整个数组。这不能不说是VB的一个不足。因此,在程序设计中,如果确实需要将整个字符串数组传入动态库,就必须采取其它方法。
我们知道,在VB中,有一种Byte数据类型。每个Byte型变量占一个字节,不含符号位,因 此所能表示的范围为0到255。这种数据类型是专门用于存放二进制数据的。为了将整个字符 串数组传进动态库,可以用字节数组来保存字符串。由于Byte是一种简单数据类型,因此字节 数组的传递是非常简单的。首先,需要把一个字符串正确地转变成一个字节数组。这要涉及一 些字符集的知识。Windows 95和VB使用不同的字符集,Windows 95 API使用的是ANSI或DBCS 字符集,而VB使用的则是Unicode字符集。所谓ANSI字符集,是指每个字符都用一个字节表示, 因此最多只能有28=256个不同的字符,这对于英语来说已经足够了,但不能完全支持其它语 言。DBCS字符集支持很多不同的东亚语言,如汉语、日语和朝鲜语,它使用数字0-255表示ASCII 字符,其它大于255或小于0的数字表明该字符属于非拉丁字符集;在DBCS中,ASCII字符的长 度是一个字节,而汉语、日语和其它东亚字符的长度是2个字节。而Unicode字符集则完全用 两个字节表示一个字符,因此最多可以表示216=65536个不同字符。也就是说,ANSI字符集中 所有的字符都只占一个字节,DBCS字符集中ASCII字符占一个字节,汉字占两个字节,Unicode 字符集中每个字符都占两个字节。由于VB与WindowsAPI使用的字符集不同,因此在进行字符 串到字节数组的转换时,当用Asc函数取得一个字符的字节码后,需要判断它是否是一个ASCII 字符;如果是ASCII字符,则在转换后的字节数组中就只占一个字节,否则要占两个字节。
下面给出了转换函数:GetChar Byte得到一个字符的高字节或低字节,它的第一个参数 是一个字符的ASCII码,第二个参数是标志取高字节还是低字节;StrToByte按DBCS或ANSI格 式将一个字符串转换成一个字节数组,第一个参数是待转换的字符串,第二个参数是转换后的一个定长字节数组,若该数组长度不足以存放整个字符串,则截去超长的部分;ChangeStrAryToByte 利用前两个函数将字符串数组转换成字节数组,第一个参数是定长的字符串数组,其中每个元素都是一个字符串(各个元素包含的字符数可以不同),第二个参数是一个变长的字节数组, 保存转换后的结果。
Function GetCharByte(ByVal OneChar As Integer,ByVal IsHighByte As Boolean) As Byte ' 该函数获得一个字符的高字节或低字节
If IsHighByte Then
If OneChar >= 0 Then
GetCharByte = CByte(OneChar / 256)
'右移8位,得到高字节
Else
GetCharByte = CByte((OneChar
And &H7FFF) / 256) Or &H80
End If
Exit Function
Else
GetCharByte = CByte(OneChar And &HFF)
'屏蔽掉高字节,得到低字节
Exit Function
End If
End Function
Sub StrToByte(Strtochange As String,ByteArray() As Byte)
'该函数将一个字符串转换成字节数组
Dim LowBound,UpBound As Integer
Dim i,count,length As Integer
Dim OneChar As Integer
count = 0
length = Len(Strtochange)
LowBound = LBound(ByteArray)
UpBound = UBound(ByteArray)
For i = LowBound To UpBound
ByteArray(i) = 0 '初始化字节数组
Next
For i = LowBound To UpBound
count = count + 1
If count <= length Then
OneChar = Asc(Mid(Strtochange,1))
If (OneChar > 255) Or (OneChar < 0) Then
'该字符是非ASCII字符
ByteArray(i) = GetCharByte(OneChar,True) '得到高字节
i = i + 1
If i <= UpBound Then ByteArray(i)
= GetCharByte(OneChar,False)
'得到低字节
Else
'该字符是ASCII字符
ByteArray(i) = OneChar
End If
Else
Exit For
End If
Next
End Sub
Sub ChangeStrAryToByte(StrAry()
As String,ByteAry() As Byte)
'将字符串数组转换成字节数组
Dim LowBound,StartPos,MaxLen As Integer
Dim TmpByte() As Byte
LowBound = LBound(StrAry)
UpBound = UBound(StrAry)
count = 0
ReDim ByteAry(0)
For i = LowBound To UpBound
MaxLen = LenB(StrAry(i))
ReDim TmpByte(MaxLen + 1)
ReDim Preserve ByteAry(count + MaxLen + 1)
Call StrToByte(StrAry(i),TmpByte) '转换一个字符串
StartPos = count
Do
ByteAry(count) = TmpByte(count - StartPos)
count = count + 1
If ByteAry(count - 1) = 0 Then Exit Do
Loop '将每一个字符串对应
的字节数组按顺序填入结果数组中
ReDim Preserve ByteAry(count - 1)
Next i
End Sub
下面看一个转换的例子:
DimResultAry()asByte DimSomeStr(2)asString SomeStr(0)="测试1" SomeStr(1)="测试222" SomeStr(2)="测试33" CallChangeStrAryToByte (SomeStr,ResultAry)'转换字符串数组 |
当转换完成以后,查看字节数组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分隔。
这样,字符串数组就全部转换成了字节数组,然后只要将字节数组的第一个元素以传址的方式传入动态连接库,DLL过程就可以正确地访问数组中的所有字符串了。但是,使用这种方法,当DLL过程处理结束返回VB时,VB得到的仍然是字节数组。如果需要在VB中再次得到该字节数组表示的字符串,还要把整个字节数组重新以0为分割符分成多个子数组(每个子数组都对应原来字符串数组中的一个元素),然后使用VB函数StrConv将每个子数组转换成字符串(转换时第二个参数选vbUnicode),就可以显示或进行其它操作了。例如,其中一个子数组的名字是SubAry,则函数StrConv(SubAry,vbUnicode)就返回了它所对应的字符串。
总之,VB应用程序和动态库间字符串参数的传递是一个比较复杂的过程,使用时要非常谨慎。同时应尽可能避免传递字符串数组类型的参数,因为这很容易引起下标越界、堆栈溢出等严重错误。