在VB中調(diào)用動態(tài)連接庫
發(fā)表時間:2023-08-03 來源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]作為一種簡單易用的Windows開發(fā)環(huán)境,Visual Basic從一推出就受到了廣大編程人員的歡迎。它使 程序員不必再直接面對紛繁復(fù)雜的Windows消息,而可以將精力主要集中在程序功能的實(shí)現(xiàn)上,...
作為一種簡單易用的Windows開發(fā)環(huán)境,Visual Basic從一推出就受到了廣大編程人員的歡迎。它使 程序員不必再直接面對紛繁復(fù)雜的Windows消息,而可以將精力主要集中在程序功能的實(shí)現(xiàn)上,大大提高了編程效率。但凡事有利必有弊。VB中高度的封裝和模塊化減輕了編程者的負(fù)擔(dān),同時也使開發(fā)人員失去了許多訪問低層API函數(shù)和直接與Windows交互的機(jī)會。因此,相比而言,VB應(yīng)用程序的執(zhí)行效率和功能比C/C++或Delphi生成的程序要差。為了解決這個問題,在一個大型的VB開發(fā)應(yīng)用中,直接調(diào)用Windows API函數(shù)幾乎是不可避免的;同時,還有可能需 要程序員自己用C/C++等開發(fā)一些動態(tài)連接庫,用于在VB中調(diào)用。本文主要討論在32位開發(fā)環(huán) 境Visual Basic 5.0中直接調(diào)用Windows 95 API函數(shù)或用戶生成的32位動態(tài)連接庫的方法 與規(guī)則。
Windows動態(tài)連接庫是包含數(shù)據(jù)和函數(shù)的模塊,可以被其它可執(zhí)行文件(EXE、DLL、OCX 等)調(diào)用。動態(tài)連接庫包含兩種函數(shù):輸出(exported)函數(shù)和內(nèi)部(internal)函數(shù)。輸出函數(shù)可以被其它模塊調(diào)用,而內(nèi)部函數(shù)則只能在動態(tài)連接庫內(nèi)部使用。盡管動態(tài)連接庫也能輸出 數(shù)據(jù),但實(shí)際上它的數(shù)據(jù)通常是只在內(nèi)部使用的。使用動態(tài)連接庫的優(yōu)點(diǎn)是顯而易見的。將應(yīng) 用程序的一部分功能提取出來做成動態(tài)連接庫,不但減小了主應(yīng)用程序的大小,提高了程序 運(yùn)行效率,還使它更加易于升級。多個應(yīng)用程序共享一個動態(tài)連接庫還能有效地節(jié)省系統(tǒng)資 源。正因?yàn)槿绱耍赪indows系統(tǒng)中,動態(tài)連接庫得到了大量的使用。
一般來說,動態(tài)連接庫都是以DLL為擴(kuò)展名的文件,如Kernel32.dll、commdlg.dll等。但也有例外,如16位Windows的核心部件之一GDI.exe其實(shí)也是一個動態(tài)庫。編寫動態(tài)連接庫的工具很多,如VisualC++、BorlandC++、Delphi等,具體方法可以參見相關(guān)文檔。下面只以Visual C++5.0為例,介紹一下開發(fā)應(yīng)用于VisualBasic5.0的動態(tài)連接庫時應(yīng)注意的問題(本文中所有涉及C/C++語言或編譯環(huán)境的地方,都以VC5為例;所有涉及VisualBasic的地方都以VB5 為例)。
作為一種32位Windows應(yīng)用程序的開發(fā)工具,VB5生成的exe文件自然也都是32位的,通常情況下也只能調(diào)用32位的動態(tài)連接庫。但是,并不是所有的32位動態(tài)庫都能被VB生成的exe 文件正確地識別。一般來說,自己編寫用于VB應(yīng)用程序調(diào)用的動態(tài)連接庫時,應(yīng)注意以下幾個方面的問題:
1、生成動態(tài)庫時要使用__stdcall調(diào)用約定,而不能使用缺省的__cdecl調(diào)用約定;__stdcall 約定通常用于32位API函數(shù)的調(diào)用。
2、在VC5中的定義文件(.def)中,必須列出輸出函數(shù)的函數(shù)名,以強(qiáng)制VC5系統(tǒng)將輸出函數(shù)的裝飾名(decoratedname)改成普通函數(shù)名;所謂裝飾名是VC的編譯器在編譯過程中生成的輸出函數(shù)名,它包含了用戶定義的函數(shù)名、函數(shù)參數(shù)及函數(shù)所在的類等多方面的信息。由于在VC5中定義文件不是必需的,因此工程不包含定義文件時VC5就按自己的約定將用戶定義的輸出函數(shù)名修改成裝飾名后放到輸出函數(shù)列表中,這樣的輸出函數(shù)在VB生成的應(yīng)用程序中是不能正確調(diào)用的(除非聲明時使用Alias子句)。因此需要增加一個.def文件,其中列出用戶需要的函數(shù)名,以強(qiáng)制VC5不按裝飾名進(jìn)行輸出。
3、VC5中的編譯選項(xiàng)"結(jié)構(gòu)成員對齊方式(structure member alignment)" 應(yīng)設(shè)成4字節(jié),其原因?qū)⒃诤笪脑敿?xì)介紹。
4、由于在C中整型變量是4個字節(jié),而VB中的整型變量依然只有2個字節(jié),因此在C中聲 明的整型(int)變量在VB中調(diào)用時要聲明為長整型(long),而C中的短整型(short)在VB中則 要聲明成整型(integer);下表針對最常用的C語言數(shù)據(jù)類型列出了與之等價的Visual Basic 類型(用于32位版本的Windows)。
C語言數(shù)據(jù)類型在VisualBasic中聲明為調(diào)用時使用的表達(dá)式
ATOM ByVal variable As Integer 結(jié)果為Integer 類型的表達(dá)式
BOOL ByVal variable As Long 結(jié)果為 Long 類型的表達(dá)式
BYTE ByVal variable As Byte 結(jié)果為 Byte 類型的表達(dá)式
CHAR ByVal variable As Byte 結(jié)果為 Byte 類型的表達(dá)式
COLORREF ByVal variable As Long 結(jié)果為 Long 類型的表達(dá)式
DWORD ByVal variable As Long 結(jié)果為 Long 類型的表達(dá)式
HWND, HDC, HMENU ByVal variable As Long 結(jié)果為 Long 類型的表達(dá)式等Windows 句柄
INT, UINT ByVal variable As Long 結(jié)果為 Long 類型的表達(dá)式
LONG ByVal variable As Long 結(jié)果為 Long 類型的表達(dá)式
LPARAM ByVal variable As Long 結(jié)果為 Long 類型的表達(dá)式
LPDWORD variable As Long 結(jié)果為 Long 類型的表達(dá)式
LPINT, LPUINT variable As Long 結(jié)果為 Long 類型的表達(dá)式
LPRECT variable As type 自定義類型的任意變量
LPSTR, LPCSTR ByVal variable As String 結(jié)果為 String 類型的表達(dá)式
LPVOID variable As Any 任何變量(在傳遞字符串的時候使用ByVal)
LPWORD variable As Integer 結(jié)果為Integer 類型的表達(dá)式
LRESULT ByVal variable As Long 結(jié)果為 Long 類型的表達(dá)式
NULL As Any 或 ByVal Nothing 或
ByVal variable As Long ByVal 0& 或 VBNullString
SHORT ByVal variable As Integer 結(jié)果為Integer 類型的表達(dá)式
VOID Sub procedure 不可用
WORD ByVal variable As Integer 結(jié)果為Integer 類型的表達(dá)式
WPARAM ByVal variable As Long 結(jié)果為 Long 類型的表達(dá)式
5、VB中進(jìn)行32位動態(tài)庫的聲明時,函數(shù)名是大小寫敏感的。在獲得了需要的動態(tài)連接 庫之后,就可以在VB中進(jìn)行調(diào)用了。但是,由于VB不能驗(yàn)證應(yīng)用程序傳遞到動態(tài)連接庫中的參 數(shù)值是否正確,因此VB程序中大量的API調(diào)用可能會降低整個應(yīng)用程序的穩(wěn)定性,也會增加以 后維護(hù)的難度。所以,決定在VB程序中直接調(diào)用API函數(shù)時要慎重,但適當(dāng)?shù)氖褂肁PI調(diào)用確實(shí) 能夠有效地提高VB程序的性能。這之間的平衡需要編程人員根據(jù)實(shí)際情況來掌握。下面就具體介紹一下在VB中調(diào)用API函數(shù)時需要做的工作。
要聲明一個DLL過程,首先需要在代碼窗口的"通用(General)"部分增加一個Declare語句。如果該過程返回一個值,應(yīng)將其聲明為Function:
Declare Function publicname Lib "libname" [Alias "alias"] [([[ByVal] variable [As type] [,[ByVal] variable [As type]]...])] As Type
如果過程沒有返回值,可將其聲明為Sub:
Declare Sub publicname Lib "libname" [Alias "alias"] [([[ByVal] variable [As type] [,[ByVal] variable [As type]]...])]
缺省情況下,在標(biāo)準(zhǔn)模塊中聲明的DLL過程,可以在應(yīng)用程序的任何地方調(diào)用它。在其它類型的模塊中定義的DLL過程則是模塊私有的,必須在它們前面聲明Private關(guān)鍵字,以示區(qū)分。下面分別介紹聲明語句的各個組成部分。
(一)、指定動態(tài)庫:
Declare語句中的Lib子句用來告訴Visual Basic如何找到包含過程的.dll文件。 如果引用的過程屬于Windows核心庫(User32、Kernel32或GDI32),則可以不包含文件擴(kuò)展名,如:
Declare Function GetTickCount Lib "kernel32" Alias "GetTickCount" () As Long
對于其它動態(tài)連接庫,可以在Lib子句指定文件的路徑:
Declare Function lzCopy Lib "c:\windows\lzexpand.dll" _
(ByVal S As Integer, ByVal D As Integer) As Long
如果未指定libname的路徑,Visual Basic將按照下列順序查找該文件:
、.exe文件所在的目錄
、诋(dāng)前目錄
、踂indows系統(tǒng)目錄
④Windows目錄
、軵ath環(huán)境變量中的目錄
下表中列出了常用的操作系統(tǒng)環(huán)境庫文件。
動態(tài)鏈接庫描述
Advapi32.dll高級API服務(wù),支持大量的API(其中包括許多安全與注冊方面的調(diào)用)
Comdlg32.dll通用對話框API庫
Gdi32.dll圖形設(shè)備接口API庫
Kernel32.dllWindows32位核心的API支持
Lz32.dll32位壓縮例程
Mpr.dll多接口路由器庫
Netapi32.dll32位網(wǎng)絡(luò)API庫
Shell32.dll32位ShellAPI庫
User32.dll用戶接口例程庫
Version.dll版本庫
Winmm.dllWindows多媒體庫
Winspool.drv后臺打印接口,包含后臺打印API調(diào)用。
對于Windows的系統(tǒng)API函數(shù),可以利用VB提供的工具API Viewer查找某一函數(shù)及其相 關(guān)數(shù)據(jù)結(jié)構(gòu)和常數(shù)的聲明,并復(fù)制到自己的程序中。
(二)、使用別名:
Declare語句中的Alias子句是一個可選的部分,用戶可以通過它所標(biāo)識的別名對動態(tài) 庫中的函數(shù)進(jìn)行引用。例如,在下面的語句中,聲明了一個在VB中名為MyFunction的函數(shù),而它在動態(tài)庫Mydll.dll中最初的名字是MyFunctionX。
Private Declare Function MyFunction Lib "Mydll.dll" _
Alias "MyFunctionX" ( ) As Long
需要注意的是,Alias子句中的函數(shù)名是大小寫敏感的,也就是說,必須與函數(shù)在生成時的聲明(如在C源文件中的聲明)一致。這是因?yàn)?2位動態(tài)庫與16位動態(tài)庫不同,其中的函數(shù)名是區(qū)分大小寫的。同樣道理,如果沒有使用Alias子句,那么在Function(或Sub)后的函數(shù)名也是區(qū)分大小寫的。
通常在以下幾種情況時需要使用Alias子句:
A.處理使用字符串的系統(tǒng)Windows API過程
如果調(diào)用的系統(tǒng)Windows API過程要使用字符串,那么聲明語句中必須增加一個Alias 子句,以指定正確的字符集。包含字符串的系統(tǒng)Windows API函數(shù)實(shí)際有兩種格式:ANSI和Unicode( 關(guān)于ANSI和Unicode兩種字符集的區(qū)別將在后面詳細(xì)闡述)。因此,在Windows頭文件中,每 個包含字符串的函數(shù)都同時有ANSI版本和Unicode版本。例如,下面是SetWindowText函數(shù) 的兩種C語言描述?梢钥吹剑谝粋描述將函數(shù)定義為SetWindowTextA,尾部的"A" 表明它是一個ANSI函數(shù):
WINUSERAPI BOOL WINAPI SetWindowTextA(HWND hWnd, LPCSTR lpString);
第二個描述將它定義為 SetWindowTextW, 尾部的"W" 表明它是一個 Unicode 函數(shù):
WINUSERAPI BOOL WINAPI SetWindowTextW(HWND hWnd, LPCWSTR lpString);
因?yàn)閮蓚函數(shù)實(shí)際的名稱都不是"SetWindowText",要引用正確的函數(shù)就必 須增加一個Alias子句:
Private Declare Function SetWindowText Lib "user32" _
Alias "SetWindowTextA" (ByVal hwnd As Long, ByVal _
lpString As String) As Long
應(yīng)當(dāng)注意,對于VB中使用的系統(tǒng)WindowsAPI函數(shù),應(yīng)該指定函數(shù)的ANSI版本,因?yàn)橹?有WindowsNT才支持Unicode版本,而Windows95不支持這個版本。僅當(dāng)應(yīng)用程序只運(yùn)行 在WindowsNT平臺上的時候才可以使用Unicode版本。
B.函數(shù)名是不標(biāo)準(zhǔn)的名稱
有時,個別的DLL過程的名稱不是有效的標(biāo)識符。例如,它可能包含了非法的字符(如連 字符),或者名稱是VB的關(guān)鍵字(如GetObject)。在這種情況下,可以使用Alias關(guān)鍵字。例 如,操作環(huán)境DLLs中的某些過程名以下劃線開始。盡管在VB標(biāo)識符中允許使用標(biāo)識符,但是 下劃線不能作為標(biāo)識符的第一個字符。為了使用這種過程,必須先聲明一個名稱合法的過程, 然后用Alias子句引用過程的真實(shí)名稱:
Declare Function lopen Lib "kernel32" Alias "_lopen" _
(ByVal lpPathName As String, ByVal iReadWrite _
As Long) As Long
在上例中,lopen是VB中使用的過程名稱。而_lopen則是動態(tài)連接庫中可以識別的名 稱。
C.使用序號標(biāo)識DLL過程
除了使用名稱之外,還可以使用序號來標(biāo)識DLL過程。某些動態(tài)連接庫中不包含過程的名稱,在聲明它們包含的過程時必須使用序號。同使用名稱標(biāo)識的DLL過程相比,如果使用序號,在最終的應(yīng)用程序中消耗的內(nèi)存將比較少,而且速度會快些。但是,一個具體的API的序號 在不同的操作系統(tǒng)中可能是不同的。例如GetWindowsDirectory在Win95下的序號為432,而在WindowsNT4.0下為338。總而言之,如果希望應(yīng)用程序能夠在不同的操作系統(tǒng)下運(yùn)行,那么最好不要使用序號來標(biāo)識API過程。如果過程不屬于API,或者應(yīng)用程序使用的范圍很有 限,那么使用序號還是有好處的。
要使用序號來聲明DLL過程,Alias子句中的字符串需要包含過程的序號,并在序號的 前面加一個數(shù)字標(biāo)記字符(#)。例如,Windowskernel中的GetWindowsDirectory函數(shù)的序 號為432;可以用下面的語句來聲明該DLL過程:
Declare Function GetWindowsDirectory Lib "kernel32" _
Alias "#432" (ByVal lpBuffer As String, _
ByVal nSize As Long) As Long
在這里,可以使用任意的合法名稱作為過程的名稱,VB將用序號在DLL中尋找過程。
為了得到要聲明的過程的序號,可以使用Dumpbin.exe等實(shí)用工具(Dumpbin.exe是Microsoft VisualC++提供的一個實(shí)用工具,它的使用說明可以參見VC的文檔)。利用Dumpbin,可以提取出.dll文件中的各種信息,例如DLL中的函數(shù)列表,它們的序號以及與代碼有關(guān)的其它信息。
(三)、使用值或引用傳遞
在缺省的情況下,VB以引用方式傳遞所有參數(shù)(ByRef)。這意味著并沒有傳遞實(shí)際的參 數(shù)值,VB只傳遞了數(shù)據(jù)的32位地址。另外有許多DLL過程要求參數(shù)以值方式傳遞(ByVal)。這意味著它們需要實(shí)際的數(shù)據(jù),而不是數(shù)據(jù)的內(nèi)存地址。如果過程需要一個傳值參數(shù),而傳遞給它的參數(shù)是一個指針,那么由于得到了錯誤的數(shù)據(jù),該過程將不能正確地工作。
要使參數(shù)以使用值方式傳遞,在Declare語句中需要在參數(shù)聲明的前面加上ByVal關(guān)鍵字。例如InvertRect過程要求第一個參數(shù)用傳值方式傳遞,而第二個用引用方式傳遞:
Declare Function InvertRect Lib "user32" Alias _
"InvertRectA" (ByVal hdc As Long, lpRect As RECT) As Long
動態(tài)連接庫的參數(shù)傳遞是一個復(fù)雜的問題,也是VB中調(diào)用動態(tài)連接庫時最容易出現(xiàn)錯誤的地方。參數(shù)類型或傳遞方式的聲明錯誤都可能導(dǎo)致應(yīng)用程序出現(xiàn)GPF(通用保護(hù)錯誤),甚至使操作系統(tǒng)崩潰,因此我們將在后面專門詳細(xì)地討論這個問題。
(四)、靈活的參數(shù)類型
某些DLL過程的同一個參數(shù)能夠接受多種數(shù)據(jù)類型。如果需要傳遞多種類型的數(shù)據(jù),可 以將參數(shù)聲明為AsAny,從而取消類型限制。例如,下面的聲明中的第三個參數(shù)(lpptAsAny) 既可以傳遞一個POINT結(jié)構(gòu)的數(shù)組,也可以傳遞一個RECT結(jié)構(gòu):
Declare Function MapWindowPoints Lib "user32" Alias _
"MapWindowPoints" (ByVal hwndFrom As Long, _
ByVal hwndTo As Long, lppt As Any, _
ByVal cPoints As Long) As Long
AsAny子句提供了一定的靈活性,但是,由于它不進(jìn)行任何的類型檢查,風(fēng)險也隨之增 加。因此在使用AsAny子句時,必須仔細(xì)檢查所有參數(shù)的類型。
正確的函數(shù)聲明是在VB中調(diào)用動態(tài)連接庫的前提,但要想在VB中用對、用好動態(tài)庫中的 函數(shù),僅僅有聲明還是遠(yuǎn)遠(yuǎn)不夠的。前面已經(jīng)說過,由于VB不能驗(yàn)證應(yīng)用程序傳遞到動態(tài)連接 庫中的參數(shù)值是否正確,因此就要求程序員應(yīng)對參數(shù)類型有非常詳細(xì)的了解,否則很容易引 起應(yīng)用程序發(fā)生通用保護(hù)錯或?qū)е聺撛诘腂ug,降低軟件的可靠性。下面將參數(shù)類型分為簡單數(shù)據(jù)類型、字符串、和用戶自定義類型三種分別進(jìn)行討論。
(1)、簡單數(shù)據(jù)類型:
簡單數(shù)據(jù)類型是指Numeric數(shù)據(jù)類型(包括Integer、Long、Single、Double、Currency類型)、Byte數(shù)據(jù)類型和Boolean數(shù)據(jù)類型。它們的共同的特點(diǎn)是結(jié)構(gòu)簡單,操作系統(tǒng)在處理時不必進(jìn)行特殊的轉(zhuǎn)換。
簡單數(shù)據(jù)類型參數(shù)的傳遞比較簡單。我們知道,在VB中傳遞參數(shù)的方式有兩種:傳值(Byval) 和傳址(ByRef),缺省的方式是傳址。所謂傳值,就是對一個變量的具體值進(jìn)行傳遞;而傳址則 是傳遞變量的地址。例如,在VB程序中需要將一個整型變量m=10的值傳進(jìn)動態(tài)庫,如果用傳值 方式,那么傳進(jìn)動態(tài)庫的值就是10,而在傳址方式下,傳入的則是變量m的地址,相當(dāng)于C/C++ 中&m的值。需要注意的是,以傳值方式傳進(jìn)動態(tài)連接庫的變量,其值在動態(tài)庫中是不能 被改變的;如果需要在動態(tài)連接庫中修改傳入?yún)?shù)的值,則必須使用傳址方式。一般來說,在VB 和動態(tài)連接庫之間傳遞單個的簡單數(shù)據(jù)類型,只要注意了以上幾個方面就可以了。當(dāng)需要將 一個簡單數(shù)據(jù)類型的整個數(shù)組傳進(jìn)動態(tài)庫時,必須將相應(yīng)參數(shù)聲明為傳址方式,然后把數(shù)組 的第一個元素作為參數(shù)傳入,這樣在動態(tài)連接庫中就得到了數(shù)組的首地址,從而可以對整個 數(shù)組進(jìn)行訪問。例如,聲明了一個名為ReadArray的DLL過程,要求傳入一個整型數(shù)組aArray:
Declare Function ReadArray Lib "mydll.dll" _
(aArray As Integer) As Integer
在調(diào)用時可以采用如下方式:
Dim ret,I(5) as Integer
… …
ret = ReadArray(I(0)) '
將整個數(shù)組傳入動態(tài)連接庫
(2)、字符串參數(shù)的傳遞:
與簡單數(shù)據(jù)類型相比,字符串類型(String、String*n)的參數(shù)傳遞要復(fù)雜得多,這主要是Windows 95 API和VB使用的字符串類型不同的緣故。VB使用被稱為BSTR的String數(shù)據(jù)類型,它是由自動化(以前被稱為OLE Automation)定義的數(shù)據(jù)類型。一個BSTR由頭部和字符串組成,頭部包含了字符串的長度信息,字符串中可以包含嵌入的null值。大部分的BSTR是 Unicode的,即每個字符需要兩個字節(jié)。BSTR通常以兩字節(jié)的兩個null字符結(jié)束。下圖表示 了一個BSTR類型的字符串。
。ㄇ熬Y)aTest\0
頭部BSTR指向數(shù)據(jù)的第一個字節(jié)
另一方面,大部分的DLL過程(包括Windows 95 API中的所有過程)使用LPSTR類型字符串,這是指向標(biāo)準(zhǔn)的以null結(jié)束的C語言字符串的指針,它也被稱為ASCIIZ字符串。LPSTR 沒有前綴。下圖顯示了一個指向ASCIIZ字符串的LPSTR。
aTest\0
LPSTR指向一個以null結(jié)尾的字符串?dāng)?shù)據(jù)的第一個字節(jié)
如果DLL過程需要一個LPSTR(指向以null結(jié)束的字符串的指針)作為參數(shù),可以在VB 中將一個字符串以傳值的方式傳遞給它。因?yàn)橹赶駼STR的指針實(shí)際指向以null值結(jié)束的字符串的第一個數(shù)據(jù)字節(jié),所以對于DLL過程來說,它就是一個LPSTR。這樣傳入動態(tài)連接庫的字符串,DLL過程也可以對它進(jìn)行修改,盡管它是以傳值方式傳入的。只有當(dāng)DLL過程需要一個指向LPSTR的指針時,才以傳址的方式傳入字符串,這時DLL過程得到的是一個指向字符串指針的指針(相當(dāng)于C/C++中的char**),而不是通常所用的字符串的首地址(相當(dāng)于C/C++中的char*)。
當(dāng)需要把一個字符串?dāng)?shù)組整個傳入動態(tài)連接庫時,情況就變得復(fù)雜多了,用傳遞簡單數(shù)據(jù)類型數(shù)組的方式來傳遞字符串?dāng)?shù)組是行不通的。當(dāng)我們以傳值的方式將一個字符串?dāng)?shù)組的第一個元素傳進(jìn)動態(tài)連接庫時,DLL過程得到的實(shí)際上是該元素壓入堆棧段后的地址,而不是數(shù)據(jù)段中整個數(shù)組的首地址。也就是說,這時DLL過程只能得到數(shù)組的第一個元素,而無法訪問整個數(shù)組。而以傳址方式傳入第一個元素時,DLL過程只能得到指向該元素在堆棧段中地址的指針,同樣無法訪問整個數(shù)組。這不能不說是VB的一個不足。因此,在程序設(shè)計(jì)中,如果確實(shí)需要將整個字符串?dāng)?shù)組傳入動態(tài)庫,就必須采取其它方法。
我們知道,在VB中,有一種Byte數(shù)據(jù)類型。每個Byte型變量占一個字節(jié),不含符號位,因 此所能表示的范圍為0到255。這種數(shù)據(jù)類型是專門用于存放二進(jìn)制數(shù)據(jù)的。為了將整個字符 串?dāng)?shù)組傳進(jìn)動態(tài)庫,可以用字節(jié)數(shù)組來保存字符串。由于Byte是一種簡單數(shù)據(jù)類型,因此字節(jié) 數(shù)組的傳遞是非常簡單的。首先,需要把一個字符串正確地轉(zhuǎn)變成一個字節(jié)數(shù)組。這要涉及一 些字符集的知識。Windows 95和VB使用不同的字符集,Windows 95 API使用的是ANSI或DBCS 字符集,而VB使用的則是Unicode字符集。所謂ANSI字符集,是指每個字符都用一個字節(jié)表示, 因此最多只能有28=256個不同的字符,這對于英語來說已經(jīng)足夠了,但不能完全支持其它語 言。DBCS字符集支持很多不同的東亞語言,如漢語、日語和朝鮮語,它使用數(shù)字0-255表示ASCII 字符,其它大于255或小于0的數(shù)字表明該字符屬于非拉丁字符集;在DBCS中,ASCII字符的長 度是一個字節(jié),而漢語、日語和其它東亞字符的長度是2個字節(jié)。而Unicode字符集則完全用 兩個字節(jié)表示一個字符,因此最多可以表示216=65536個不同字符。也就是說,ANSI字符集中 所有的字符都只占一個字節(jié),DBCS字符集中ASCII字符占一個字節(jié),漢字占兩個字節(jié),Unicode 字符集中每個字符都占兩個字節(jié)。由于VB與WindowsAPI使用的字符集不同,因此在進(jìn)行字符 串到字節(jié)數(shù)組的轉(zhuǎn)換時,當(dāng)用Asc函數(shù)取得一個字符的字節(jié)碼后,需要判斷它是否是一個ASCII 字符;如果是ASCII字符,則在轉(zhuǎn)換后的字節(jié)數(shù)組中就只占一個字節(jié),否則要占兩個字節(jié)。
下面給出了轉(zhuǎn)換函數(shù):GetChar Byte得到一個字符的高字節(jié)或低字節(jié),它的第一個參數(shù) 是一個字符的ASCII碼,第二個參數(shù)是標(biāo)志取高字節(jié)還是低字節(jié);StrToByte按DBCS或ANSI格 式將一個字符串轉(zhuǎn)換成一個字節(jié)數(shù)組,第一個參數(shù)是待轉(zhuǎn)換的字符串,第二個參數(shù)是轉(zhuǎn)換后的一個定長字節(jié)數(shù)組,若該數(shù)組長度不足以存放整個字符串,則截去超長的部分;ChangeStrAryToByte 利用前兩個函數(shù)將字符串?dāng)?shù)組轉(zhuǎn)換成字節(jié)數(shù)組,第一個參數(shù)是定長的字符串?dāng)?shù)組,其中每個元素都是一個字符串(各個元素包含的字符數(shù)可以不同),第二個參數(shù)是一個變長的字節(jié)數(shù)組, 保存轉(zhuǎn)換后的結(jié)果。
Function GetCharByte(ByVal OneChar As Integer, ByVal IsHighByte As Boolean) As Byte ' 該函數(shù)獲得一個字符的高字節(jié)或低字節(jié)
If IsHighByte Then
If OneChar >= 0 Then
GetCharByte = CByte(OneChar \ 256)
'右移8位,得到高字節(jié)
Else
GetCharByte = CByte((OneChar
And &H7FFF) \ 256) Or &H80
End If
Exit Function
Else
GetCharByte = CByte(OneChar And &HFF)
'屏蔽掉高字節(jié),得到低字節(jié)
Exit Function
End If
End Function
Sub StrToByte(StrToChange As String, ByteArray() As Byte)
'該函數(shù)將一個字符串轉(zhuǎn)換成字節(jié)數(shù)組
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 '初始化字節(jié)數(shù)組
Next
For i = LowBound To UpBound
count = count + 1
If count <= length Then
OneChar = Asc(Mid(StrToChange, count, 1))
If (OneChar > 255) Or (OneChar < 0) Then
'該字符是非ASCII字符
ByteArray(i) = GetCharByte(OneChar, True) '得到高字節(jié)
i = i + 1
If i <= UpBound Then ByteArray(i)
= GetCharByte(OneChar, False)
'得到低字節(jié)
Else
'該字符是ASCII字符
ByteArray(i) = OneChar
End If
Else
Exit For
End If
Next
End Sub
Sub ChangeStrAryToByte(StrAry()
As String, ByteAry() As Byte)
'將字符串?dāng)?shù)組轉(zhuǎn)換成字節(jié)數(shù)組
Dim LowBound, UpBound As Integer
Dim i, count, 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) '轉(zhuǎn)換一個字符串
StartPos = count
Do
ByteAry(count) = TmpByte(count - StartPos)
count = count + 1
If ByteAry(count - 1) = 0 Then Exit Do
Loop '將每一個字符串對應(yīng)
的字節(jié)數(shù)組按順序填入結(jié)果數(shù)組中
ReDim Preserve ByteAry(count - 1)
Next i
End Sub
下面看一個轉(zhuǎn)換的例子:
DimResultAry()asByte
DimSomeStr(2)asString
SomeStr(0)="測試1"
SomeStr(1)="測試222"
SomeStr(2)="測試33"
CallChangeStrAryToByte
(SomeStr,ResultAry)'轉(zhuǎn)換字符串?dāng)?shù)組
當(dāng)轉(zhuǎn)換完成以后,查看字節(jié)數(shù)組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]是"測"的字節(jié)碼,[202,112]是"試"的字節(jié)碼,49,50,51 分別為字符1、2、3的ASCII碼?梢,經(jīng)過轉(zhuǎn)換后,字符串?dāng)?shù)組中的各個元素按順序放在了字節(jié)數(shù)組中,相互間以終止符0分隔。
這樣,字符串?dāng)?shù)組就全部轉(zhuǎn)換成了字節(jié)數(shù)組,然后只要將字節(jié)數(shù)組的第一個元素以傳址的方式傳入動態(tài)連接庫,DLL過程就可以正確地訪問數(shù)組中的所有字符串了。但是,使用這種方法,當(dāng)DLL過程處理結(jié)束返回VB時,VB得到的仍然是字節(jié)數(shù)組。如果需要在VB中再次得到該字節(jié)數(shù)組表示的字符串,還要把整個字節(jié)數(shù)組重新以0為分割符分成多個子數(shù)組(每個子數(shù)組都對應(yīng)原來字符串?dāng)?shù)組中的一個元素),然后使用VB函數(shù)StrConv將每個子數(shù)組轉(zhuǎn)換成字符串(轉(zhuǎn)換時第二個參數(shù)選vbUnicode),就可以顯示或進(jìn)行其它操作了。例如,其中一個子數(shù)組的名字是SubAry,則函數(shù)StrConv(SubAry,vbUnicode)就返回了它所對應(yīng)的字符串。
總之,VB應(yīng)用程序和動態(tài)庫間字符串參數(shù)的傳遞是一個比較復(fù)雜的過程,使用時要非常謹(jǐn)慎。同時應(yīng)盡可能避免傳遞字符串?dāng)?shù)組類型的參數(shù),因?yàn)檫@很容易引起下標(biāo)越界、堆棧溢出等嚴(yán)重錯誤。
(3)、用戶自定義類型(User-defined Type)參數(shù)的傳遞
用戶自定義類型在VB中是一種重要的數(shù)據(jù)類型,它為編程者提供了很大的靈活性,使開發(fā)人員可以根據(jù)需要構(gòu)造自己的數(shù)據(jù)結(jié)構(gòu)。它相當(dāng)于C/C++中的結(jié)構(gòu)類型(structure)。在VB中,允許程序員以傳址的方式將自定義數(shù)據(jù)類型參數(shù)傳入動態(tài)庫,DLL過程也可以將修改后的參數(shù)返回VB程序。但是,在VB中仍然不支持以傳值的方式傳遞用戶自定義類型參數(shù)。
傳遞用戶自定義類型參數(shù)時,必須確保VB中的數(shù)據(jù)類型的成員與動態(tài)庫中的結(jié)構(gòu)成員是一一對應(yīng)的,所占空間也必須嚴(yán)格一致。這里所說的一一對應(yīng),不僅是指VB 中的所有結(jié)構(gòu)成員在動態(tài)庫的結(jié)構(gòu)中都必須有對應(yīng)的元素,而且它們在數(shù)據(jù)結(jié)構(gòu)中定義的順序也必須嚴(yán)格一致,這是VB中使用的"數(shù)據(jù)結(jié)構(gòu)成員對齊方式"決定的。在VB 中,數(shù)據(jù)結(jié)構(gòu)使用雙字對齊方式(4-byte alignment),因此,在用戶自己生成用于VB 調(diào)用的動態(tài)連接庫時,也必須把編譯選項(xiàng)"structure member alignment" 設(shè)為4字節(jié)(如前文所述)。
所謂結(jié)構(gòu)成員對齊方式是指一個數(shù)據(jù)結(jié)構(gòu)內(nèi)部,其成員的排列方式。譬如,在VB中,其對齊方式是4字節(jié),這就好象在一個數(shù)據(jù)結(jié)構(gòu)內(nèi)部分成了很多個4字節(jié)大小的小單元,如果相鄰 兩個或多個數(shù)據(jù)成員的大小可以放在一個單元中,那么就放在一起;否則這些小單元中可能 會出現(xiàn)未用的空字節(jié)。我們來看下面一個數(shù)據(jù)類型:
Type TestType
m1 as Integer
m2 as Byte
m3 as Long
End Type
它的三個成員的大小加起來是2+1+4=7。但是,由于m1和m2的字節(jié)總長度是3,小于4,它 們就存放于一個單元中;但該單元剩下的一個字節(jié)不足以放下一個Long型的成員m3,于是m3 就被放在下一個單元中,它們之間就有了一個未用的空字節(jié);因此,整個結(jié)構(gòu)所占實(shí)際長度是8 字節(jié)。同理,如果將m3和m2的位置交換一下,它所占的尺寸就變成了9字節(jié)。可見,成員在結(jié)構(gòu) 中的聲明順序也是非常重要的。
通常,當(dāng)一個用戶自定義類型中不包含字符串時,向動態(tài)連接庫中傳遞該類型的參數(shù)是沒有什么問題的。如果只傳遞一個自定義類型變量,則既可以傳遞該變量名,也可以傳遞該變 量的第一個成員,它們的效果是一樣的,都是將該變量的地址傳進(jìn)了動態(tài)庫;同樣,如果要傳遞一個自定義類型的數(shù)組,則既可以傳遞該數(shù)組的第一個元素,也可以傳遞第一個元素的第一個成員。但是,如果用戶自定義類型中包含字符串類型時,又該如何與動態(tài)連接庫傳遞參數(shù)呢?答案是令人遺憾的:在VB中,你無法將一個包含字符串成員的用戶自定義類型變量或數(shù) 組安全、正確地傳入動態(tài)庫中。如果你這樣做了,即使某次僥幸得到了正確的結(jié)果,在其背后也隱藏著許多致命的危險。因此,如果一定要在用戶自定義類型中包含字符串變量,并且該類型的變量又要作為參數(shù)傳入動態(tài)庫時,你最好修改類型定義,把其中的字符串成員用相應(yīng)的字節(jié)數(shù)組類型替換掉(轉(zhuǎn)換方法可參見前文),這樣就可以在VB 和動態(tài)庫間傳遞這種類型的參數(shù)了。
另外,在VB 中還可以把一個函數(shù)的指針傳遞到動態(tài)庫中,方法也并不復(fù)雜。但筆者強(qiáng)烈建議最好不要這么做,因?yàn)檫@樣一來VB 應(yīng)用程序就幾乎完全喪失了它所應(yīng)有的安全性。如果 確實(shí)需要傳遞函數(shù)指針的話,那么還是編一個C/C++ 的程序來完成這項(xiàng)工作吧。
總之,在VB中調(diào)用DLL過程是一個比較復(fù)雜的問題,編程人員必須很好地把握,才能達(dá)到既提高了程序效率,開拓了程序功能,又不降低程序安全性的目的。另外需要特別指出的一點(diǎn)是,在本文中提到的所有動態(tài)連接庫,都是指沒有使用自動化(OLE Automation)技術(shù)的動態(tài)庫,Windows API和大多數(shù)用戶自編的動態(tài)連接庫都是這種類型的。對于使用了OLE Automation技術(shù)的動態(tài)連接庫,其參數(shù)傳遞的方式有所不同,讀者可以參閱有關(guān)OLE 技術(shù)的書籍,在此不再涉及。