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應用程序調用的動態鏈接庫時,應注意如下幾個方面的問題:
  一、生成動態庫時要使用__stdcall調用約定,而不能使用缺省的__cdecl調用約定;__stdcall 約定一般用於32位API函數的調用。
  二、在VC5中的定義文件(.def)中,必須列出輸出函數的函數名,以強制VC5系統將輸出函數的裝飾名(decoratedname)改爲普通函數名;所謂裝飾名是VC的編譯器在編譯過程當中生成的輸出函數名,它包含了用戶定義的函數名、函數參數及函數所在的類等多方面的信息。因爲在VC5中定義文件不是必需的,所以工程不包含定義文件時VC5就按本身的約定將用戶定義的輸出函數名修改爲裝飾名後放到輸出函數列表中,這樣的輸出函數在VB生成的應用程序中是不能正確調用的(除非聲明時使用Alias子句)。所以須要增長一個.def文件,其中列出用戶須要的函數名,以強制VC5不按裝飾名進行輸出。
  三、VC5中的編譯選項"結構成員對齊方式(structure member alignment)" 應設成4字節,其緣由將在後文詳細介紹。
  四、因爲在C中整型變量是4個字節,而VB中的整型變量依然只有2個字節,所以在C中聲 明的整型(int)變量在VB中調用時要聲明爲長整型(long),而C中的短整型(short)在VB中則 要聲明成整型(integer);下表針對最經常使用的C語言數據類型列出了與之等價的Visual Basic 類型(用於32位版本的Windows)。
  C語言數據類型在VisualBasic中聲明爲調用時使用的表達式
   ATOM ByVal variable As Integer 結果爲Integer 類型的表達式
   BOOL ByVal variable As Long 結果爲 Long 類型的表達式
   BYTE ByVal variable As Byte 結果爲 Byte 類型的表達式
   CHAR ByVal variable As Byte 結果爲 Byte 類型的表達式
   COLORREF ByVal variable As Long 結果爲 Long 類型的表達式
   DWORD ByVal variable As Long 結果爲 Long 類型的表達式
   HWND, HDC, HMENU ByVal variable As Long 結果爲 Long 類型的表達式等Windows 句柄
   INT, UINT ByVal variable As Long 結果爲 Long 類型的表達式
   LONG ByVal variable As Long 結果爲 Long 類型的表達式
   LPARAM ByVal variable As Long 結果爲 Long 類型的表達式
   LPDWORD variable As Long 結果爲 Long 類型的表達式
   LPINT, LPUINT variable As Long 結果爲 Long 類型的表達式
   LPRECT variable As type 自定義類型的任意變量
   LPSTR, LPCSTR ByVal variable As String 結果爲 String 類型的表達式
   LPVOID variable As Any 任何變量(在傳遞字符串的時候使用ByVal)
   LPWORD variable As Integer 結果爲Integer 類型的表達式
   LRESULT ByVal variable As Long 結果爲 Long 類型的表達式
   NULL As Any 或 ByVal Nothing 或
   ByVal variable As Long ByVal 0& 或 VBNullString
   SHORT ByVal variable As Integer 結果爲Integer 類型的表達式
   VOID Sub procedure 不可用
   WORD ByVal variable As Integer 結果爲Integer 類型的表達式
   WPARAM ByVal variable As Long 結果爲 Long 類型的表達式
  五、VB中進行32位動態庫的聲明時,函數名是大小寫敏感的。在得到了須要的動態鏈接 庫以後,就能夠在VB中進行調用了。可是,因爲VB不能驗證應用程序傳遞到動態鏈接庫中的參 數值是否正確,所以VB程序中大量的API調用可能會下降整個應用程序的穩定性,也會增長以 後維護的難度。因此,決定在VB程序中直接調用API函數時要慎重,但適當的使用API調用確實 可以有效地提升VB程序的性能。這之間的平衡須要編程人員根據實際狀況來掌握。下面就具體介紹一下在VB中調用API函數時須要作的工做。
  要聲明一個DLL過程,首先須要在代碼窗口的"通用(General)"部分增長一個Declare語句。若是該過程返回一個值,應將其聲明爲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]]...])]
  缺省狀況下,在標準模塊中聲明的DLL過程,能夠在應用程序的任何地方調用它。在其它類型的模塊中定義的DLL過程則是模塊私有的,必須在它們前面聲明Private關鍵字,以示區分。下面分別介紹聲明語句的各個組成部分。
編程

(一)、指定動態庫:windows

  Declare語句中的Lib子句用來告訴Visual Basic如何找到包含過程的.dll文件。 若是引用的過程屬於Windows核心庫(User3二、Kernel32或GDI32),則能夠不包含文件擴展名,如:api


   Declare Function GetTickCount Lib "kernel32" Alias "GetTickCount" () As Long
數組

   對於其它動態鏈接庫,能夠在Lib子句指定文件的路徑: 安全

   Declare Function lzCopy Lib "c:windowslzexpand.dll" _ 網絡

   (ByVal S As Integer, ByVal D As Integer) As Long
  若是未指定libname的路徑,Visual Basic將按照下列順序查找該文件:
數據結構

  ①.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子句:
  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中的函數列表,它們的序號以及與代碼有關的其它信息。

(三)、使用值或引用傳遞

  在缺省的狀況下,VB以引用方式傳遞全部參數(ByRef)。這意味着並無傳遞實際的參 數值,VB只傳遞了數據的32位地址。另外有許多DLL過程要求參數以值方式傳遞(ByVal)。這意味着它們須要實際的數據,而不是數據的內存地址。
若是過程須要一個傳值參數,而傳遞給它的參數是一個指針,那麼因爲獲得了錯誤的數據,該過程將不能正確地工做。
  要使參數以使用值方式傳遞,在Declare語句中須要在參數聲明的前面加上ByVal關鍵字。例如InvertRect過程要求第一個參數用傳值方式傳遞,而第二個用引用方式傳遞:

Declare Function InvertRect Lib "user32" Alias _
"InvertRectA" (ByVal hdc As Long, lpRect As RECT) As Long

  動態鏈接庫的參數傳遞是一個複雜的問題,也是VB中調用動態鏈接庫時最容易出現錯誤的地方。參數類型或傳遞方式的聲明錯誤均可能致使應用程序出現GPF(通用保護錯誤),甚至使操做系統崩潰,所以咱們將在後面專門詳細地討論這個問題。

  (四)、靈活的參數類型

  某些DLL過程的同一個參數可以接受多種數據類型。若是須要傳遞多種類型的數據,可 以將參數聲明爲AsAny,從而取消類型限制。例如,下面的聲明中的第三個參數(lpptAsAny) 既能夠傳遞一個POINT結構的數組,也能夠傳遞一個RECT結構:

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子句提供了必定的靈活性,可是,因爲它不進行任何的類型檢查,風險也隨之增 加。所以在使用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 和動態鏈接庫之間傳遞單個的簡單數據類型,只要注意了以上幾個方面就能夠了。當須要將 一個簡單數據類型的整個數組傳進動態庫時,必須將相應參數聲明爲傳址方式,而後把數組 的第一個元素做爲參數傳入,這樣在動態鏈接庫中就獲得了數組的首地址,從而能夠對整個 數組進行訪問。例如,聲明瞭一個名爲ReadArray的DLL過程,要求傳入一個整型數組aArray:

Declare Function ReadArray Lib "mydll.dll" _
(aArray As Integer) As Integer

在調用時能夠採用以下方式:

Dim ret,I(5) as Integer
… …
ret = ReadArray(I(0)) 註釋:

將整個數組傳入動態鏈接庫
  (2)、字符串參數的傳遞:

  與簡單數據類型相比,字符串類型(String、 String*n)的參數傳遞要複雜得多,這主要是Windows 98 API和VB使用的字符串類型不一樣的緣故。VB使用被稱爲BSTR的String數據類型,它是由自動化(之前被稱爲OLE Automation)定義的數據類型。一個BSTR由頭部和字符串組成,頭部包含了字符串的長度信息,字符串中能夠包含嵌入的null值。大部分的 BSTR是 Unicode的,即每一個字符須要兩個字節。BSTR一般以兩字節的兩個null字符結束。下圖表示 了一個BSTR類型的字符串。

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, count, 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, 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) 註釋:轉換一個字符串
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 分別爲字符一、二、3的ASCII碼。可見,通過轉換後,字符串數組中的各個元素按順序放在了字節數組中,相互間以終止符0分隔。

  這樣,字符串數組就所有轉換成了字節數組,而後只要將字節數組的第一個元素以傳址的方式傳入動態鏈接庫,DLL過程就能夠正確地訪問數組中的全部字符串了。可是,使用這種方法,當DLL過程處理結束返回VB 時,VB獲得的仍然是字節數組。若是須要在VB中再次獲得該字節數組表示的字符串,還要把整個字節數組從新以0爲分割符分紅多個子數組(每一個子數組都對應原來字符串數組中的一個元素),而後使用VB函數StrConv將每一個子數組轉換成字符串(轉換時第二個參數選vbUnicode),就能夠顯示或進行其它操做了。例如,其中一個子數組的名字是SubAry,則函數StrConv(SubAry,vbUnicode)就返回了它所對應的字符串。

  總之,VB應用程序和動態庫間字符串參數的傳遞是一個比較複雜的過程,使用時要很是謹慎。同時應儘量避免傳遞字符串數組類型的參數,由於這很容易引發下標越界、堆棧溢出等嚴重錯誤。
  (3)、用戶自定義類型(User-defined Type)參數的傳遞

  用戶自定義類型在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
m1 as Integer
m2 as Byte
m3 as Long
End Type

  它的三個成員的大小加起來是2+1+4=7。可是,因爲m1和m2的字節總長度是3,小於4,它 們就存放於一個單元中;但該單元剩下的一個字節不足以放下一個Long型的成員m3,因而m3 就被放在下一個單元中,它們之間就有了一個未用的空字節;所以,整個結構所佔實際長度是8 字節。同理,若是將m3和m2的位置交換一下,它所佔的尺寸就變成了9字節。可見,成員在結構 中的聲明順序也是很是重要的。

  一般,當一個用戶自定義類型中不包含字符串時,向動態鏈接庫中傳遞該類型的參數是沒有什麼問題的。若是隻傳遞一個自定義類型變量,則既能夠傳遞該變量名,也能夠傳遞該變 量的第一個成員,它們的效果是同樣的,都是將該變量的地址傳進了動態庫;一樣,若是要傳遞一個自定義類型的數組,則既能夠傳遞該數組的第一個元素,也能夠傳遞第一個元素的第一個成員。可是,若是用戶自定義類型中包含字符串類型時,又該如何與動態鏈接庫傳遞參數呢?答案是使人遺憾的:在VB中,你沒法將一個包含字符串成員的用戶自定義類型變量或數 組安全、正確地傳入動態庫中。若是你這樣作了,即便某次僥倖獲得了正確的結果,在其背後也隱藏着許多致命的危險。所以,若是必定要在用戶自定義類型中包含字符串變量,而且該類型的變量又要做爲參數傳入動態庫時,你最好修改類型定義,把其中的字符串成員用相應的字節數組類型替換掉(轉換方法可參見前文),這樣就能夠在VB 和動態庫間傳遞這種類型的參數了。

  另外,在VB 中還能夠把一個函數的指針傳遞到動態庫中,方法也並不複雜。但筆者強烈建議最好不要這麼作,由於這樣一來VB 應用程序就幾乎徹底喪失了它所應有的安全性。若是 確實須要傳遞函數指針的話,那麼仍是編一個C/C++ 的程序來完成這項工做吧。

  總之,在VB中調用DLL過程是一個比較複雜的問題,編程人員必須很好地把握,才能達到既提升了程序效率,開拓了程序功能,又不下降程序安全性的目的。另外須要特別指出的一點是,在本文中提到的全部動態鏈接庫,都是指沒有使用自動化(OLE Automation)技術的動態庫,Windows API和大多數用戶自編的動態鏈接庫都是這種類型的。對於使用了OLE Automation技術的動態鏈接庫,其參數傳遞的方式有所不一樣,讀者能夠參閱有關OLE 技術的書籍,在此再也不涉及。

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, count, 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, 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) 註釋:轉換一個字符串
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 分別爲字符一、二、3的ASCII碼。可見,通過轉換後,字符串數組中的各個元素按順序放在了字節數組中,相互間以終止符0分隔。

  這樣,字符串數組就所有轉換成了字節數組,而後只要將字節數組的第一個元素以傳址的方式傳入動態鏈接庫,DLL過程就能夠正確地訪問數組中的全部字符串了。可是,使用這種方法,當DLL過程處理結束返回VB 時,VB獲得的仍然是字節數組。若是須要在VB中再次獲得該字節數組表示的字符串,還要把整個字節數組從新以0爲分割符分紅多個子數組(每一個子數組都對應原來字符串數組中的一個元素),而後使用VB函數StrConv將每一個子數組轉換成字符串(轉換時第二個參數選vbUnicode),就能夠顯示或進行其它操做了。例如,其中一個子數組的名字是SubAry,則函數StrConv(SubAry,vbUnicode)就返回了它所對應的字符串。

  總之,VB應用程序和動態庫間字符串參數的傳遞是一個比較複雜的過程,使用時要很是謹慎。同時應儘量避免傳遞字符串數組類型的參數,由於這很容易引發下標越界、堆棧溢出等嚴重錯誤。
  (3)、用戶自定義類型(User-defined Type)參數的傳遞

  用戶自定義類型在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
m1 as Integer
m2 as Byte
m3 as Long
End Type

  它的三個成員的大小加起來是2+1+4=7。可是,因爲m1和m2的字節總長度是3,小於4,它 們就存放於一個單元中;但該單元剩下的一個字節不足以放下一個Long型的成員m3,因而m3 就被放在下一個單元中,它們之間就有了一個未用的空字節;所以,整個結構所佔實際長度是8 字節。同理,若是將m3和m2的位置交換一下,它所佔的尺寸就變成了9字節。可見,成員在結構 中的聲明順序也是很是重要的。

  一般,當一個用戶自定義類型中不包含字符串時,向動態鏈接庫中傳遞該類型的參數是沒有什麼問題的。若是隻傳遞一個自定義類型變量,則既能夠傳遞該變量名,也能夠傳遞該變 量的第一個成員,它們的效果是同樣的,都是將該變量的地址傳進了動態庫;一樣,若是要傳遞一個自定義類型的數組,則既能夠傳遞該數組的第一個元素,也能夠傳遞第一個元素的第一個成員。可是,若是用戶自定義類型中包含字符串類型時,又該如何與動態鏈接庫傳遞參數呢?答案是使人遺憾的:在VB中,你沒法將一個包含字符串成員的用戶自定義類型變量或數 組安全、正確地傳入動態庫中。若是你這樣作了,即便某次僥倖獲得了正確的結果,在其背後也隱藏着許多致命的危險。所以,若是必定要在用戶自定義類型中包含字符串變量,而且該類型的變量又要做爲參數傳入動態庫時,你最好修改類型定義,把其中的字符串成員用相應的字節數組類型替換掉(轉換方法可參見前文),這樣就能夠在VB 和動態庫間傳遞這種類型的參數了。

  另外,在VB 中還能夠把一個函數的指針傳遞到動態庫中,方法也並不複雜。但筆者強烈建議最好不要這麼作,由於這樣一來VB 應用程序就幾乎徹底喪失了它所應有的安全性。若是 確實須要傳遞函數指針的話,那麼仍是編一個C/C++ 的程序來完成這項工做吧。

  總之,在VB中調用DLL過程是一個比較複雜的問題,編程人員必須很好地把握,才能達到既提升了程序效率,開拓了程序功能,又不下降程序安全性的目的。另外須要特別指出的一點是,在本文中提到的全部動態鏈接庫,都是指沒有使用自動化(OLE Automation)技術的動態庫,Windows API和大多數用戶自編的動態鏈接庫都是這種類型的。對於使用了OLE Automation技術的動態鏈接庫,其參數傳遞的方式有所不一樣,讀者能夠參閱有關OLE 技術的書籍,在此再也不涉及。

相關文章
相關標籤/搜索