VB 版 (精华区)
发信人: bloom (├┝┞┟┠┢┣), 信区: VB
标 题: VB API教程(王国荣版)(六)(转载)
发信站: 哈工大紫丁香 (2000年09月07日18:42:50 星期四), 转信
【 以下文字转载自 cnTemp 讨论区 】
【 原文由 bloom 所发表 】
发信人: Love1976 (狄飞惊), 信区: VisualBasic
发信站: BBS 水木清华站 (Thu Apr 6 04:21:52 2000)
发信人: coolknight (酷骑士~找工作中), 信区: VB
标 题: VB 與 Windows API 講座(六)
发信站: 武汉白云黄鹤站 (Tue Nov 9 20:11:39 1999), 站内信件
Windows API 鬼門關的安全之旅
------------------------------------------------------------------------
----
----
王國榮
開始使用 Windows API 之後, 想信您一定看過「這個程式執行的作業無效,即將
關閉
」的畫面(程式當掉也), 出現此一畫面後, 若按下「詳細資料」鈕,大致上可看
到「
VB5 caused a general protection fault in module XXXX at 9999:99999999」
的字樣
,而按下「關閉」鈕, VB 及程式會立刻被踢出系統。千萬別懷疑是 VB 或是
module
XXXX 出的錯, 問題很簡單, 只是因為我們沒有傳遞正確的參數給 API 函數。
筆者覺得出現以上的錯誤倒還好, 因為它至少讓我們知道程式呼叫 API 函數時所
傳遞
的參數不正確,但很多時候並不會出現以上的錯誤, 而呼叫 API 函數之後, 所
得到的
結果卻不是我們所預期的,舉個簡單的例子:(GetWindowText API 的用途是讀取
視窗的
標題)
Dim S
S = String( 80, Chr(0) )
S = String( 80, Chr(0) )
Call GetWindowText(Me.hwnd, S, 80)
' 呼叫之後, S 竟然不等於視窗標題
結果 API 順利呼叫完畢了, 但 S 卻不等於視窗標題。
除了避免當掉之外, 能夠將參數正確地傳入 API 函數, 而 API 函數執行之後也
能夠
正確地傳回 VB 程式, 才是我們期望的結果, 本期就讓筆者帶著您的參數, 安
全正確
地來回 Windows API 的鬼門關。
------------------------------------------------------------------------
----
----
從「防呆」功能看 VB 副程式與 API 函數
------------------------------------------------------------------------
----
----
如果您組裝過電腦, 可以發現大多數的硬體插座(頭)都具有「防呆」的功能,舉
例來說
, 記憶體的插槽被設計成兩邊不對稱, 如果安裝時方向錯了, 就無法將記憶體
插入插
槽中,所以即使是呆瓜, 也不會安裝錯誤, 而這樣的作法在 VB 裡面也是屢見不
鮮的
,舉例來說, 某一副程式的定義如下:
Sub SomeSub( ByVal X As String )
… 副程式內部
End Sub
End Sub
那麼以下幾種呼叫的敘述都可以得到正確的結果:
Call SomeSub ( "ABC" ) ' 標準的呼叫方式
Call SomeSub ( 123 ) ' 123 會先被轉成 "123"
Dim X ' X 為不定型型別
X = "ABC"
Cal SomeSub ( X ) ' 雖然 X 不是 String 型別, 但 VB 仍然接受
這是因為 VB 會先判別參數的型別, 然後進行適當的轉換。
接下來讓我們來比較 API 函數是否具有防呆設計, 閱讀過前面幾期的 Windows
API 專
欄, 大家都知道呼叫 API 函數以前, 必須利用 Declare 敘述宣告 API 函數,
例如
:
Declare Function RegQueryValue Lib "advapi32.dll" Alias "RegQueryValueA"
(By
Val hKey As Long, ByVal lpSubKey As String, ByVal lpValue As String,
lpcbVal
ue As Long) As Long
如果您檢視宣告式中的參數部分, 可以發現它與 VB 副程式的參數宣告方式並沒
有什麼
不同,那麼當我們利用以上的 Declare 敘述宣告了 API 函數之後, 呼叫 API 函
數時
,是否也具有防呆的功能?
,是否也具有防呆的功能?
答案不是 No, 也不是 Yes, 而與參數的資料型別有關, 除了 String 及 Any
型別之
外, 其他資料型別的參數都具有防呆的功能, 以上面的 RegQueryValue 函數為
例,
hKey 參數的宣告是 ByVal As Long, 所以以下的傳入值都不會造成錯誤:
&H80000000:長整數, 標準的傳遞方式。
"&H80000000":數值類型的字串, 傳遞時會被轉成長整數。
I%:整數型別, 也一樣在傳遞時會被轉成長整數。
再來比較 lpcbValue 參數的情況, 此一參數的宣告是 Long, 等於 ByRef As
Long,
表示傳遞參數的「位址」, 而 VB 規定傳遞 ByRef 參數時, 實際參數的型別一
定要
與形式參數的型別相同,所以這個參數只能傳入 Long 型別的變數, 但如果我們
傳入
Integer 或 String 型別的參數, 結果會如何呢?結果無法通過編譯, 所以也就
不會
呼叫進入 API 函數的內部, 這當然也是防呆的功能。
筆者剛才說過 API 函數中的 Any 型別及 String 型別不具備防呆的功能, 首先
讓我們
來比較以下兩種不同的呼叫例子:
' 呼叫例一:正確的呼叫方式
Dim S As String
S = String(80, Chr(0))
Call RegQueryValue(HKEY_CLASSES_ROOT, ".txt", S, 80)
Call RegQueryValue(HKEY_CLASSES_ROOT, ".txt", S, 80)
' 呼叫例二:錯誤的呼叫方式
' 變數 S 並未宣告成 String 型別
S = String(80, Chr(0))
Call RegQueryValue(HKEY_CLASSES_ROOT, ".txt", S, 80)
結果呼叫例二無法得到正確的結果。在呼叫例二之中, S 並未預先宣告, 所以會
被 V
B 視為 Variant(不定型)型別, 而由於 String 型別在 API 參數中不具有防呆的
功能
,以致這個呼叫例無法得到正確的結果。
除了 String 型別之外, Any 型別也不具有防呆的功能, 而且還更詭譎, 本期
筆者會
針對這兩種型別做深入的探討。
Option Explicit ─ 防呆 DIY
------------------------------------------------------------------------
----
----
由於很多人(包括筆者在內)已經習慣於 VB 的防呆功能, 經常在不宣告變數型別
的情況
下就直接使用變數,結果一旦開始使用Windows API, 就很容易忽略傳遞給 API
函數的
每一個變數都應該規規矩矩地宣告資料型別,而造成錯誤, 為了避免這一類的錯
誤,
筆者建議您在每一個模組(表單模組、一般模組、物件類別模組…)的最前面加上
Optio
n Explicit 敘述, 此一敘述的作用是要求編譯器對所有未宣告的變數進行檢查,
如果
n Explicit 敘述, 此一敘述的作用是要求編譯器對所有未宣告的變數進行檢查,
如果
發現未宣告的變數, 一律以錯誤視之, 如此一來, 可以提早在程式編譯階段就
挑出潛
在的錯誤,而不必等到程式執行結果錯誤之後, 才辛苦地檢測程式。
加上 Option Explicit 的習慣很真的重要, 讓筆者再婆婆媽媽地叮嚀您一次,勇
闖 A
PI 以前, 記得戴上這第一道護身符。
------------------------------------------------------------------------
----
----
字串資料是如何傳遞的
------------------------------------------------------------------------
----
----
由於 String 型別在API 裡面並不具有防呆的功能, 想要來一段安全的 API 之旅
, 必
須徹底瞭解它的傳遞方式。首先讓我們回顧它在 VB 副程式之間的傳遞方式,請比
較以
下兩個副程式:
Sub Sub1( ByVal X As String ) ' 傳「值」模式
…
End Sub
Sub Sub2( X As String ) ' 傳「位址」模式
…
End Sub
End Sub
其中 Sub2 「X As String」的意義等於「ByRef X As String」, 在術語上稱為
傳「位
址」呼叫(或簡稱傳「址」呼叫),假設呼叫端的敘述是「Call Sub2(S)」, 則不
管 S
字串有多長, 一概傳遞 S 字串的所在位址(位址的長度是固定的,對 32-bits
Window
s 而言, 等於32-bits), 至於「ByVal X As String」則表示傳「值」呼叫,假
設呼叫
的敘述是「Call Sub1(S)」, 則會複製一份 S 再傳遞給 Sub1 副程式。
基本上, 傳位址呼叫的執行效能優於傳值呼叫, 因為不管字串有多長, 只傳位
址,但
這並不表示傳值呼叫沒有存在的必要, 傳值呼叫由於是複製一份字串再傳遞給副
程式,
所以不管副程式如何使用這一份字串, 都不會破壞原有的字串, 而傳位址呼叫則
是直
接告訴副程式原有字串的位址,因此副程式對字串的任何操作都會反映到原有的字
串上
面。該使用傳值或傳位址呼叫,不能夠單純地以執行效能為考量。
API 的 ByVal As String 傳遞模式
------------------------------------------------------------------------
----
----
以上所說明的是 VB 副程式之間傳遞字串參數的方式, 至於 VB 程式與 API 函數
之間
的傳遞方式有什麼不同呢?首先請回顧較早所引用的 RegQueryValue API 函數,
其中
參數三的宣告方式是「ByVal lpValue As String」, 而呼叫時的所傳遞的參數是
S 變
數, 假設 RegQueryValue 是 VB 的副程式, 則 S 的內容在呼叫前後是不會被改
變的
(因為是 ByVal 傳值呼叫), 但實際上呼叫此一 API之後, S 變數卻會得到
"HKEY_CL
ASSED_ROOT\.txt" Subkey 的預設值, 顯然地與 VB 的副程式並不相同, 以下就
讓筆
ASSED_ROOT\.txt" Subkey 的預設值, 顯然地與 VB 的副程式並不相同, 以下就
讓筆
者來說明字串在 API 函數裡面的傳遞方式。
由於絕大部分的 API 是以 C 語言開發出來的, 而 C 語言在傳遞字串時, 並不
具備複
製字串的功能(也就是字串的傳值呼叫),原因是複製字串的執行效能較差, 為了
傳遞字
串, C 語言選擇了位址的傳遞,而此一特性也決定了 API 傳遞字串的方式。說明
至此
, 您一定覺得很奇怪, 既然是傳遞字串的位址,參數的宣告寫成「參數 As
String」
或「ByRef 參數 As String」不就好了, 但實際上,當我們檢視 Win32api.txt
(位於
VB 的 Winapi 目錄, 這是 Windows API 宣告式的大本營),卻發現所有字串型
別的參
數都是以 ByVal 方式來宣告的, 這到底是怎麼一回事?
這與 Unicode 有關, 稍後筆者在「Unicode 與字串」的段落中會有進一步的解說
,現
在讓筆者先說明「ByVal As String」在 VB 程式與 API 之間的傳遞過程, 請參
考圖-
1。當 VB 程式傳遞字串參數給 API 函數時, VB 會先複製一份 C 語言格式的字
串,
然後將此一字串的位址傳給 API, 於是 API 對字串的操作, 都會發生在此一
C 語言
字串上面, 而最後當 API 返回時, VB 又會把這一份 C 語言字串複製回 VB 程
式的字
串參數, 就結果來看,此一呼叫與傳位址呼叫是一樣的。
圖-1 字串參數的傳遞方式
但顯然地以上的參數傳遞方式執行效能較差(因為來回複製了兩次字串), 為什麼
Wind
ows 不讓API 直接使用 VB 的字串呢?有關這個問題, 請參見稍後的「Unicode
及字串
」。
」。
除了過程不一樣之外, 讓我們也來想一想以上的傳遞方式對程式設計會有怎樣的
影響,
首先回顧 VB 的副程式, 假設是不含 ByVal 的「S As String」傳位址方式, 則
副程
式內部除了可能改變 S 的內容之外, 甚至可以重新配置S 的長度, 例如:
Dim X As String
X = String(5, Chr(0)) ' 字串長度等於 5
Call SubX(X)
Sub SubX( S As String )
S = "abc" ' S 變成長度等於 3 的字串
' ...
S = "123456789" ' S 變成長度等於 9 的字串
End Sub
VB 的副程式可以重新配置字串, 但 API 函數卻不能這麼做, 請再參考圖-1,由
於 A
PI 所使用的是複製版的 C 語言字串, 所以它不像 VB 的副程式一樣可以動態地
配置字
串,因此呼叫 API 之前, VB 程式有義務配置足夠的字串空間供 API 使用, 這
是為什
麼我們在呼叫含有字串參數的 API 函數時, 要使用 String 函數來配置字串空間
的原
因。
------------------------------------------------------------------------
----
----
----
Any 型別是怎樣的資料型別
------------------------------------------------------------------------
----
----
瞭解 String 型別的參數傳遞方式之後, 接下來是 Any 型別, 當某一個 API 的
參數
被宣告成 Any 型別時, 就表示VB 程式可以傳入任何型別的資料, 但那並不表示
傳入
任何型別的資料都是正確的(態度就好像是「隨你怎麼做都行,但對錯我可不管」
), 顯
然地, 這是API 函數中最不具備防呆功能的參數, 既然它不具備防呆的功能,那
麼一
切就得靠自己了。
接下來請檢視筆者上一期所提供原始程式碼中的 Registry.bas, 結果可以找到
RtlMo
veMemory API 函數的宣告式, 如下:
Declare Sub RtlMoveMemory Lib "KERNEL32" (lpvDest As Any, lpvSource As
Any,
ByVal cbCopy As Long)
這個函數的用途是把參數 lpSource 位址的資料複製 lpvDest 參數的位址,而複
製的長
度取決於 cbCopy 參數, 以下是一個使用實例:
Dim L1 As Long, L2 As Long
L1 = 12345
Call RtlMoveMemory( L2, L1, 4 )
Call RtlMoveMemory( L2, L1, 4 )
在以上的呼叫敘述中, 筆者分別傳入 L2 及 L1 兩個 Long 型別的變數到兩個
Any 型
別的參數中, 這代表什麼意義呢?其實我們可以用一句話來詮釋 Any 型別的參數
:「
我沒有型別,如果你傳給我的資料是 Long 型別, 我就變成 Long 型別, 如果你
傳給
我的資料是 Double 型別, 我就變成 Double 型別…」, 以「RtlMoveMemory(
L2, L
1, 4 )」為例,其作用相當於將 RtlMoveMemory 宣告成:
Declare Sub RtlMoveMemory Lib "KERNEL32" (lpvDest As Long, lpvSource
As Long
, ByVal cbCopy As Long)
但為什麼我們不直接採以上的宣告方式呢?因為採用以上的宣告方式之後, 我們
就只能
傳入 Long 型別的資料, 而採用 Any 型別的宣告, 我們可以傳入 Long、
Integer、S
ingle、Double、Array、自訂型別…等各種不同型別的資料,例如以下是傳入
Integer
型別資料到 Any 型別參數的例子:
Dim I As Integer, J As Integer
I = 123
Call RtlMoveMemory( J, I, 2 )
而此時的 RtlMoveMemory 的宣告式相當於:
Declare Sub RtlMoveMemory Lib "KERNEL32" (lpvDest As Integer,
lpvSource As I
Declare Sub RtlMoveMemory Lib "KERNEL32" (lpvDest As Integer,
lpvSource As I
nteger, ByVal cbCopy As Long)
Any 型別中的 ByVal 參數傳遞方式
------------------------------------------------------------------------
----
----
相信以上關於 Any 型別的介紹還難不倒您, 但請注意雖然 Any 型別可以替代成
任何資
料型別,傳入怎樣的資料才是正確的, 卻與 API 函數本身的定義有關, 而這也
正是
Any 型別參數陷阱特多的原因。接下來讓筆者再來舉個參數被定義成 Any 型別的
API
函數─SendMessage。SendMessage 的宣告式如下, 其中參數四被宣告成 Any 型
別:
Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal
hwnd A
s Long, ByVal wMsg As Long, ByVal wParam As Long, lParam As Any) As
Long
SendMessage 的用途是傳送訊息給某一視窗, 所以參數一須指明 hWnd(handle of
win
dow)、參數二則須指明訊息編號 wMsg, 接著 wParam 及 lParam 參數的指定則與
訊息
編號有關,請參考下表, 分別是三種不同的訊息其 wParam 及 lParam 的指定方
式:
wMsg(訊息編號) 訊息用途 wParam lParam
EM_GETRECT 讀取 TextBox 文字的顯示區 沒有作用, 填入 0 即可 RECT 資料結
構的位
址
WM_SETTEXT 設定視窗標題 沒有作用, 填入 0 即可 視窗標題, 字串型別
WM_SETTEXT 設定視窗標題 沒有作用, 填入 0 即可 視窗標題, 字串型別
EM_LINESCROLL 控制 TextBox 的捲動 水平捲動的行數 垂直捲動的列數, 長整數
型別
比較令人放心的是 wParam 參數, 它一定是長整數型別, 但 lParam 參數的型別
卻與
訊息編號有關,就表格的順序, 分別是:(1) RECT 資料結構的位址:此時須以As
REC
T 的方式傳遞 (2) 字串型別:此時須以 ByVal As String 的方式傳遞 (3) 長整
數型別
:此時須以 ByVal As Long 的方式傳遞。以上三種訊息的 lParam 參數除了資料
型別不
同之外,也請注意它們依序是以:(1) 傳位址的方式來傳遞 (2) API String 的方
式來
傳遞 (3) 傳值的方式來傳遞, 而呼叫的實例則如下:(註:完整的範例請至筆者
的網站
下載)
' EM_GETRECT 的呼叫實例
Dim r As RECT
' lParam傳入RECT 資料結構的位址
Call SendMessage(Text1.hWnd, EM_GETRECT, 0, r)
' WM_SETTEXT 的呼叫實例
' lParam傳入字串
Call SendMessage(Me.hWnd, WM_SETTEXT, 0, ByVal "設定標題測試")
' EM_LINESCROLL 的呼叫實例
' lParam 傳入長整數
' lParam 傳入長整數
Call SendMessage(Text1.hWnd, EM_LINESCROLL, 3, ByVal 3&)
觀察以上實例以前, 也請注意 SendMessage 宣告式的 lParam 參數之前並未加上
ByV
al, 屬於傳位址呼叫, 但是對 WM_SETTEXT 訊息而言, 卻必須以 ByVal As
String
的方式傳遞參數, 所以在WM_SETTEXT 的呼叫實例中, 筆者在 "設定標題測試"
之前加
上 ByVal, 使得「As Any」變成「ByVal As String」, 而同樣的, 對
EM_LINESCRO
LL 訊息而言, 必須以 ByVal As Long 的方式傳遞參數, 所以在
EM_LINESCROLL 的呼
叫實例中, 筆者在 3 之前加上 ByVal, 並且在 3 之後加上 &(將 Integer 型別
的常
數強制轉換成 Long 型別), 而使得「As Any」變成「ByVal As Long」。
Any 型別固然詭譎, 但實際上 Any 型別參數的設定方法, 仍不脫以上三種:(1)
傳遞
位址, 此時參數可能是任何一種資料型別 (2) 傳遞字串, 此時參數必須是
String 型
別, 且之前要加上 ByVal (3) 傳遞長整數, 此時參數必須是 Long 型別,且之
前要加
上 ByVal。不過話雖如此, 筆者還是要舉出兩種常犯的錯誤, 同樣以
SendMessage 為
例:
Dim S
Dim I As Integer
S = "設定標題測試"
Call SendMessage(Me.hWnd, WM_SETTEXT, 0, ByVal S)
I = 3
I = 3
Call SendMessage(Text1.hWnd, EM_LINESCROLL, 3, ByVal I)
第一個 SendMessage 的錯誤是以「Dim S」宣告 S 變數, 如此宣告的變數其資料
型別
為 Variant(不定型), 雖然程式曾經指定 "設定標題測試" 字串給它,但它的型
別還是
Variant, 第二個 SendMessage 的錯誤則是傳入 Integer 型別的 I 給
lParam 參數
, 這兩個敘述的 lParam 參數可以分別修正為「ByVal CStr(S)」及「ByVal
CLng(I)」
(註:CStr 的作用是把資料轉換成 String 型別, CLng 則是把資料轉換成
Long 型別
), 當然, 在一開始就宣告正確的資料型別, 也就是「Dim S As String」及「
Dim I
As Long」, 則是更好的習慣。
------------------------------------------------------------------------
----
----
如何傳遞陣列給 API
------------------------------------------------------------------------
----
----
傳遞陣列?「我連如何傳遞陣列到 VB 的副程式都不知道, …」, 好吧!就先複
習 V
B 程式中傳遞陣列的方法。對副程式而言, 要接受陣列, 參數要如下宣告:
Sub 副程式名稱( 參數名稱() As 型別名稱, … )
與一般參數唯一的差別是參數名稱後面要加上 (), 至於呼叫程式則是直接傳入陣
列名
與一般參數唯一的差別是參數名稱後面要加上 (), 至於呼叫程式則是直接傳入陣
列名
稱,例如:
Dim Param(0 To 10) As Integer ' 宣告一個陣列
Call SubX(Param) ' 直接傳入陣列名稱
Sub SubX(arr() As Integer)
'...
End Sub
對 API 而言, 傳遞陣列不管在副程式定義端或是呼叫程式端, 都與 VB 的語法
不同,
以下直接以實例來說明, 首先請看 Polygon API 函數的宣告式:
Private Type POINTAPI
x As Long
y As Long
End Type
Private Declare Function Polygon Lib "gdi32" (ByVal hdc As Long, lpPoint
As
POINTAPI, ByVal nCount As Long) As Long
這個函數的用途是繪製多邊形, 參數二 lpPoint 須傳入形成多邊形的「點」(定
義成
POINTAPI 資料結構)陣列, 參數三則須指明「點」數, 您一定注意到了 lpPoint
參數
並不像 VB 的陣列參數一樣, 含有 (), 接著再來看呼叫的範例程式:(完整範例
請參
並不像 VB 的陣列參數一樣, 含有 (), 接著再來看呼叫的範例程式:(完整範例
請參
閱下載檔案中的 Polygon.vbp 專案)
Dim Point(0 To 10) As POINTAPI
Point(0).x = 100: Point(0).y = 0
Point(1).x = 0: Point(1).y = 100
Point(2).x = 66: Point(2).y = 200
Point(3).x = 133: Point(3).y = 200
Point(4).x = 200: Point(4).y = 100
Call Polygon(Me.hdc, Point(0), 5)
請看呼叫敘述的的參數二 Point(0), 看起來只傳入 Point 陣列中的第 0 個點,
這怎
麼會是傳遞陣列呢?
由於 lpPoint 參數的宣告是「lpPoint As POINTAPI」(傳位址也), 所以以上程
式傳遞
Point(0) 給這個參數時, 實際上是傳遞 Point(0) 的位址, 加上陣列是連續性
的資
料,所以 API 便可以只利用 Point(0) 的位址緊接著抓到 Point(1)、Point(2)…
的位
址了。(呼!原來傳遞陣列不過是傳遞位址的一種變形)
C 語言與傳位址呼叫
------------------------------------------------------------------------
----
----
慢慢地, 您會發現除了 1、2、4 bytes 的資料之外(例如 Byte、Integer、
Long), A
慢慢地, 您會發現除了 1、2、4 bytes 的資料之外(例如 Byte、Integer、
Long), A
PI 函數特別偏好位址的傳遞, 對於大於 32-bits 的資料一律以位址來傳遞(註:
取 3
2-bits 與作業系統有關, 因為 Windows 95/NT 都是 32-bits的作業系統), 如
果我們
使用「變數的四個組成元素」(請參閱 Run!PC 43、44 期或 Visual Basic 5.0 實
戰講
座第 5 章), 可以把位址的傳遞表示成圖-2:
圖-2 位址的傳遞與資料的使用
由於是傳遞位址, 所以不管多大的資料, 也不會影響執行效能。傳遞位址的另一
個優
點是各種資料型別的資料都可以傳遞(筆者必須先說明的是,有得必有失, 此一優
點也
使得 C 語言在很多地方欠缺防呆的功能), 在我們所介紹過的 API 函數中,
RtlMove
Memory 可以說是傳遞位址的代表性 API, 較早介紹的例子只是牛刀小試,接著我
們要
利用它來做兩個有趣的實驗。
(1) 複製陣列。
(2) 將一個長整數複製到 Byte 陣列中。
首先是陣列的複製, 請直接看例子:
Dim Arr1(100) As Long, Arr2(100) As Long
' 這是 VB 複製陣列的方法
For I = 0 To 100
Arr1(I) = Arr2(I)
Arr1(I) = Arr2(I)
Next
' 這是 RtlMoveMemory API 複製陣列的方法
Call RtlMoveMemory( Arr2(0), Arr1(0), 101*4 )
可以看出 VB 要執行 101 次的指定動作, 而 RtlMoveMemory 則是利用位址的特
性,依
序將 Arr1(0) 開始的資料複製到 Arr2(0) 的所在位置, 複製的資料長度是
101*4(請
注意不是 101, 因為 Long 的長度是 4)。RtlMoveMemory 也可以只複製部分資料
, 假
設我們要將 Arr1(11~20) 複製到 Arr2(51~60), 則呼叫的敘述如下:
Call RtlMoveMemory( Arr2(51), Arr1(11), 10*4 )
再來看將長整數複製到 Byte 陣列的例子:
Dim L As Long, bArr(0 To 3) As Byte
L = &H1020304
Call RtlMoveMemory(bArr(0), L, 4)
Print "bArr="; bArr(0); bArr(1); bArr(2); bArr(3)
在以上例子中, 長整數 L 的 4個位元組分別等於 1、2、3、4, 但是印出來的
bArr(
0~3) 卻是顛倒的(等於 4 3 2 1), 這是因為 1 的位置在高位元組而 4 的位置
在低位
元組,所以 bArr(0~3) 才會印出 4 3 2 1 的結果。
元組,所以 bArr(0~3) 才會印出 4 3 2 1 的結果。
------------------------------------------------------------------------
----
----
Unicode 與字串
------------------------------------------------------------------------
----
----
使用 Windows API 時, 還有一件十分惱人的事情─字元碼(或稱「內碼」)。對英
文來
說, 0-127 的字元碼就足以代表所有的字元, 但是對中文而言, 就必須使用兩
個位元
組來代表一個字元,習慣上稱為 DBCS(Double-Byte Character Set), 而相對之
下,
英文的字元碼就稱為 SBCS(Single-Byte Character Set), 在本段落中, 筆者要
討論
字元碼(尤其是 Unicode)對 Windows API 的影響。
什麼是 Unicode?
------------------------------------------------------------------------
----
----
雖然說 DBCS 的觀念足以解決中英文字元混合使用的問題, 但實際上它仍有許多
問題,
以國內早期的字元碼而言, 有 Big5 碼、國標碼、電信碼…, 對於使用不同字元
碼的
系統而言,為了互換資料, 必須經過字元碼的轉換, 十分麻煩, 如果再把簡體
字、日
文、韓文…等一併考量進來,為了能夠互相轉換字元碼, 麻煩的事就更多了。
為了解決這個問題, 老美的電腦公司(包括 Apple、Xerox(這兩家是最早的發起公
司)、
Microsoft、IBM、Novell、Borland…太多了,無法一一列舉)聯合起來制訂了一套
可以
適用於全世界所有國家的字元碼, 稱為 Unicode(有人將它翻譯成「國際」字元碼
, 筆
者則稱它為「一統」字元碼, 想想,字元碼都不能統一, 老美的軟體如何征服全
世界
呢?)。
Unicode 最大的特點是不管哪一國的字元碼都是以兩個 byte 表示, 舉例來說,
英文的
"A" 在 ASCII code 裡面, 原本使用的字元碼是41(16 進位),而在 Unicode 之
中則
是 4100(low byte 等於 41、high byte 則補 0)。由於 Unicode 使用了兩個
byte,
所以共可以表示 65536 個字元, 也許有人覺得才這麼多,夠嗎?實際上, 除了
中文字
有一萬多字以外, 大多數國家的字母都是寥寥可數的,以英文為例, 大小寫字母
加起
來不過 52 個, 以現況而言, 目前各國的字元全部加起來不過是三萬多個。
與 VB 相關的軟體中, 哪些使用 Unicode?哪些不是?
------------------------------------------------------------------------
----
----
本段落標題的答案, 請參考下表:
使用 DBCS + SBCS VB 3.0 版、VB 4.0 16-bits 版(只能使用於 Windows 3.1)、
Windo
ws 3.1 及其 API、Windows 95 及其 API。
使用 Unicode VB 4.0 32-bits 版、VB 5.0 版、Windows NT 及其 API
使用 Unicode VB 4.0 32-bits 版、VB 5.0 版、Windows NT 及其 API
由於本專欄一直都是以 32-bits Windows(95 或 NT) 為解說的對象, 所以 VB
3.0、V
B 4.0 16-bit 版本及 Windows 3.1 就略過不談, 在上表中, 最特殊的是
Windows 9
5 還是使用 DBCS+SBCS。
首先讓筆者舉個例子來說明字串在 Unicode 及 DBCS+SBCS 字元碼之中的差異,請
參考
下圖:
圖-3 Unicode 字串與 DBCS+SBCS 字串
同樣的字串 "中英Mixed" 在 VB4 32-bit 版、VB5、及 Windows NT 裡面佔用 7
個 Un
icode 字元(14 個 byte), 但是在 Windows 95 裡面, 卻是佔用 9 個 byte。
VB 程式與 Windows 95 API 之間的字串傳遞
------------------------------------------------------------------------
----
----
在「API 的 ByVal As String 傳遞模式」段落中, 筆者說明了 VB 程式與 API
之間的
字串傳遞方式, 可以看出執行效能實在遠低於位址的傳遞, 為什麼字串資料不直
接以
位址來傳遞呢,參考圖-4 便可知曉, 因為 Windows 95 API 使用的是 DBCS+SBCS
字串
, 而 VB 使用的是 Unicode 字串, 所以必須先將 Unicode 字串轉換(複製)成一
份 D
BCS+SBCS 字串, 然後再傳遞其位址給 API, 如果直接傳遞 VB 字串的位址給
API,
BCS+SBCS 字串, 然後再傳遞其位址給 API, 如果直接傳遞 VB 字串的位址給
API,
API 是無法正確解讀的。
圖-4 VB 程式傳遞字串給 Win95 API, 必須先經過字元碼的轉換
以上的字串傳遞方式看起來實在很驢, 為什麼 Windows 95 不直接採用 Unicode
呢?筆
者覺得原因有二:(1) 當初微軟承諾讓 Windows 95 可以在 4MB 記憶體的機器上
面執行
, 所以選擇比較節省記憶體的 DBCS+SBCS 字元碼 (2) 其實主要的原因還是應用
程式的
相容性, 想想在 Windows 95 還沒上市以前, 世界上有多少 DOS 及 Windows
3.1 的
應用程式, 為了讓這些程式能夠繼續在 Windows 95 底下使用, 採用與 Windows
3.1
相容的字元碼是最不會有問題的。
------------------------------------------------------------------------
----
----
我需要高效能的字串傳遞方式
------------------------------------------------------------------------
----
----
雖然字串傳遞的效能比較低, 但也別把它想得太糟糕, 對於偶而呼叫的 API 來
說,
複製字串所花費的時間實在少之又少, 大可不必理會, 筆者覺得需要高效能字串
傳遞
的情況大概有二:(1) 您是十分龜毛的人, (2) 在冗長的迴圈中, 需要連續呼叫
傳遞
字串的 API, 不管是哪一種,如果您希望字串的傳遞再更快一點, 請繼續閱讀本
段落
。
。
開始以前, 讓筆者先介紹一個重要的函數─StrConv, 這個函數可將 Unicode 字
串轉
換成 DBCS+SBCS 字串, 也可以將DBCS+SBCS 字串轉換成 Unicode 字串,範例如
下:(
請同時參考圖-5)
Dim S1 As String, S2 As String, S3 As String
S1 = "中英Mixed" ' VB 的字串, 所以是 Unicode 字串
S2 = StrConv( S1, vbFromUnicode ) ' 將 S1 轉成DBCS+SBCS 字串
S3 = StrConv( S2, vbUnicode ) ' 將 S2 轉成Unicode 字串
圖-5 利用 StrConv 轉換字元碼
執行以上程式之後, S1 及 S3 的「字元數」等於 7(使用 Len(S1) 來檢查)、「
位元組
數」(bytes)等於 14(使用 LenB(S1) 來檢查), 而 S2 的「位元組數」則等於
9(使用
LenB(S2) 來檢查),但請注意「字元數」對 S2 是沒有意義的, 因為 S2 不是
Unico
de 字串。
雖然 VB 不允許我們直接操作DBCS+SBCS 字串, 卻允許我們將 DBCS+SBCS 字串指
定給
Byte 陣列, 而透過 Byte 陣列的操作, 便可以達到操作DBCS+SBCS 字串的目的
,承
續上面的程式, 將 S2 指定給 Byte 陣列的方法如下:
Dim bArr() As Byte ' 宣告一個可變動長度的 Byte 陣列
Dim bArr() As Byte ' 宣告一個可變動長度的 Byte 陣列
bArr = S2
' 經過以上敘述之後
' bArr(0) = "中" 的第一個 byte、bArr(1) = "中" 的第二個 byte
' bArr(2) = "英" 的第一個 byte、bArr(3) = "英" 的第二個 byte
' bArr(4) = "M" 的ASCII code、bArr(5) = "i" 的ASCII code
' bArr(6) = "x" 的ASCII code、bArr(7) = "e" 的ASCII code
' bArr(8) = "d" 的ASCII code
為什麼要介紹 StrConv 函數及 Byte 陣列呢?因為筆者想利用StrConv 函數將字
串(也
就是 Unicode 字串)轉成 Byte 陣列(其實就是 DBCS+SBCS 字串), 然後傳遞
Byte 陣
列的位址給 API 函數, 如此一來 API 便可以省下複製兩次字串的時間, 而提高
API
的執行效能。這麼說好像有點抽象,舉例來說明吧!假設我們想利用
RtlMoveMemory
函數將 "TestString" 複製到字串變數 S 之中, 則標準的呼叫敘述如下:
Dim S As String
S = String( 80, Chr(0) )
Call RtlMoveMemory( ByVal S, ByVal "TestString", 10 )
但這麼做複製資料的次數有四次(S 及 "TestString" 各兩次), 所以有人就想,
不如去
掉 ByVal, 變成「Call RtlMoveMemory(S, "TestString", 10 )」, 結果並不能
得到
正確的結果, 接著讓我們來看以下的程式:
Dim bArr(79) As Byte, S As String
Call RtlMoveMemory( bArr(0), ByVal "TestString", 10 )
S = StrConv(bArr, vbUnicode)
由於參數一 bArr(0) 是以傳位址的方式來傳遞, 所以省下複製二次資料的時間,
而呼
叫之後, 我們利用 StrConv將 bArr 複製到 S 字串之中, 雖然也花了一次複製
的時間
,但還是賺了一次。如果又把程式改成:
Dim bArr(79) As Byte, S As String, bArr2() As Byte
bArr2 = StrConv("TestString", vbFromUnicode)
Call RtlMoveMemory( bArr(0), bArr2(0), 10 )
S = StrConv(bArr, vbUnicode)
那麼參數二的傳遞也賺了一次複製資料的時間, 加起來就賺了兩次。(補充說明,
第三
個例子其實比第二個例子多執行了一個「bArr2 = StrConv("TestString",
vbFromUnic
ode)」敘述, 所以未必會比較快, 就實際的測試數據來看, 反倒變慢了,這是
理論與
實務不同的地方)。
由於 RtlMoveMemory 函數宣告式中的參數一、二是 As Any, 所以我們除了傳遞
字串之
外,也可以傳入 Byte 陣列, 但並不是每一個 API 函數的宣告式對我們都一樣的
幸運
,以 RegQueryValue 為例, 它在 Win32api.txt 之中的宣告式如下:
Declare Function RegQueryValue Lib "advapi32.dll" Alias "RegQueryValueA"
(By
Val hKey As Long, ByVal lpSubKey As String, ByVal lpValue As String,
lpcbVal
ue As Long) As Long
其中參數二、三都是宣告成「ByVal As String」, 如果我們在呼叫的敘述中傳入
Byt
e 陣列, VB 編譯時就會產生錯誤, 遇到這種情況該怎麼辦呢?第一步是在程式
視窗中
複製原來的宣告,然後將原來的函數名稱改掉(假設改成 RegQueryValueByAny),
接著
再把「ByVal As String」的參數宣告改成「As Any」, 使得增加以下的 API 宣
告式:
Declare Function RegQueryValueByAny Lib "advapi32.dll" Alias
"RegQueryValueA
" (ByVal hKey As Long, lpSubKey As Any, lpValue As Any, lpcbValue As
Long) A
s Long
如此一來, 原本的呼叫敘述, 如下:
S = String(80, Chr(0)
ret = RegQueryValue(HKEY_CLASSES_ROOT, ".txt", S, 80)
就可以改成:
Dim bArr1() As Byte, bArr2(80) As Byte
Dim bArr1() As Byte, bArr2(80) As Byte
bArr1 = StrConv( ".txt" + Chr(0), vbFromUnicode )
ret = RegQueryValueByAny(HKEY_CLASSES_ROOT, bArr1(0), bArr2(0), 80)
S = StrConv(bArr2, vbUnicode )
而使得字串的傳遞節省兩次複製資料的時間。不可否認的, 這麼做比較麻煩,除
非連節
省一絲絲的時間都很重要, 否則多一事不如少一事。
VB 呼叫 NT API 又如何呢?
------------------------------------------------------------------------
----
----
由於 VB 與 Windows 95 使用不同的字元碼, 使得字串的傳遞必須經過兩次複製
的過程
,一定有讀者想問:「VB 與 NT 都是使用 Unicode, 那麼 VB 呼叫 NT 含有字串
的 A
PI 是不是就不會那麼驢了?」, 答案是肯定的, 但實務上, 卻有一些該注意的
地方
,對於含有字串參數的 API 而言, NT 提供了兩套 API 函數, 以
RegQueryValue 為
例, 一個函數叫做 RegQueryValueW(W 表示 Word), 另一個叫做
RegQueryValueA(A 表
示 ASCII), 如果 VB 呼叫的是以 W 結尾的函數, 則 VB 程式傳遞字串給 NT 時
, 不
必像 95 一樣, 要先經過字串的轉換, 如果 VB 想使用這一套函數, 在 API 的
宣告
式中, 要修改 Alias 保留字之後 API 名稱, 以 RegQueryValue 為例,原本的
宣告式
如下:
Declare Function RegQueryValue Lib "advapi32.dll" Alias "RegQueryValueA"
(By
Declare Function RegQueryValue Lib "advapi32.dll" Alias "RegQueryValueA"
(By
Val hKey As Long, ByVal lpSubKey As String, ByVal lpValue As String,
lpcbVal
ue As Long) As Long
修改之後的宣告式則如下:
Declare Function RegQueryValue Lib "advapi32.dll" Alias "RegQueryValueW"
(By
Val hKey As Long, ByVal lpSubKey As String, ByVal lpValue As String,
lpcbVal
ue As Long) As Long
這麼做確實可以提升字串在 NT API 的傳遞速度, 但問題是 95 並沒有提供以
W 結尾
的 API 函數, 也就是說, 編譯出來的執行檔只能夠在 NT 底下執行, 這麼一來
,如
果同一個程式想同時在95 及 NT 底下執行, 便必須呼叫不同的 API, 並且編譯
出不同
的執行檔。
反之, 如果程式呼叫的是以 A 結尾的 API 呢?不錯的是, NT 同時提供有以
W 及以
A 結尾的 API 函數, 因此以 A 結尾的 API 函數可以在 95 底下執行,也可以
在 NT
底下執行, 其實這就是為什麼 Win32api.txt 之中出現的都是以 A 結尾的
API 宣告
式, 因為它們可以同時適用於 95 及 NT。
但也先別高興太早, 以 A 結尾的 API 函數一定要先將 Unicode 字串轉成
DBCS+SBCD
字串, 才進入 API 函數內部, 但 NT 需要的不是 DBCS+SBCD 字串, 於是以 A
結尾
的 API 函數在 NT 底下執行的時候, 還要再把DBCS+SBCD 字串轉換回 Unicode
字串(
的 API 函數在 NT 底下執行的時候, 還要再把DBCS+SBCD 字串轉換回 Unicode
字串(
轉換了兩次, 結果轉回原來的格式, 驢啊!), 完整的過程如圖-6:
圖-6 以 A 結尾的 API 函數在 NT 底下傳遞字串的過程
所以結論是:呼叫以 A 結尾的 API 函數, 可使得同一套執行檔可以同時在 95
跟 NT
底下執行, 比較省事, 大部分的時候我們會採用這一種呼叫方式, 但如果效能
是很
重要的考量因素,則在 NT 底下執行的執行檔應該呼叫以 W 結尾的 API 函數。
------------------------------------------------------------------------
----
----
別讓您的程式碼喪生於API 的鬼門關
------------------------------------------------------------------------
----
----
筆者相信今天所介紹的東西, 一定有助於您勇闖 API 的世界, 但還是不能完全
保證「
這個程式執行的作業無效,即將關閉」不會再出現在您的螢幕上, 為了不要讓您
辛苦撰
寫的程式血本無歸,進入 API 鬼門關的最後一條法則是「啟動程式以前, 先存檔
」。
--
" The Matrix is everywhere, it's all around us, here even in this room.
You
底下執行, 比較省事, 大部分的時候我們會採用這一種呼叫方式, 但如果效能
是很
can see it out your window, or on your television. You feel it when
you
go to work, or go to church or pay your taxes. It is the world that has
been
pulled over your eyes to blind you from the truth... Unfortunately,
no one
can be told what the Matrix is. You have to see it for yourself."
Morphe
※ 来源:.武汉白云黄鹤站 bbs.whnet.edu.cn.[FROM: 202.114.3.124]
--
我并不是在等待奇迹,因为我知道没有奇迹的。
有的,也只是爱情、意志和勇气。
是这些东西的重叠后,而成为奇迹的。
所以,我从未曾想过放弃。
※ 修改:·Love1976 於 Apr 6 04:24:33 修改本文·[FROM: 202.112.140.138]
--
☆ 来源:.哈工大紫丁香 bbs.hit.edu.cn.[FROM: blo0m.bbs@smth.org]
--
※ 转载:.哈工大紫丁香 bbs.hit.edu.cn.[FROM: 202.118.247.254]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:206.083毫秒