VisualBasic變態用法之函數指針
一、函數指針
AddressOf得到一個VB內部的函數指針,我們可以將這個函數指針傳遞給需要回調這個函數的API,它的作用就是讓外部的程序可以調用VB內部的函數。
但是VB里函數指針的應用,遠不象C里應用那么廣泛,因為VB文檔里僅介紹了如何將函數指針傳遞給API以實現回調,并沒指出函數指針諸多神奇的功能,因為VB是不鼓勵使用指針的,函數指針也不例外。
首先讓我們對函數指針的使用方式來分個類。
1、回調。這是最基本也是最重要的功能。比如VB文檔里介紹過的子類派生技術,它的核心就是兩個API:SetWindowLong和CallWindowProc。
我們可以使SetWindowLong這個API來將原來的窗口函數指針換成自己的函數指針,并將原來的窗口函數指針保存下來。這樣窗口消息就可以發到我們自己的函數里來,并且我們隨時可以用CallWindowProc來調用前面保存下來的窗口指針,以調用原來的窗口函數。這樣,我們可以在不破壞原有窗口功能的前提下處理鉤入的消息。
具體的處理,我們應該很熟悉了,VB文檔也講得很清楚了。這里需要注意的就是CallWindowProc這個API,在后面我們將看到它的妙用。
在這里我們稱回調為讓"外部調用內部的函數指針"。
2、程序內部使用。比如在C里我們可以將C函數指針作為參數傳遞給一個需要函數指針的C函數,如后面還要講到的C庫函數qsort,它的聲明如下:
它需要一個COMPARE類型函數指針,用來比較兩個變量大小的,這樣排序函數可以調用這個函數指針來比較不同類型的變量,所以qsort可以對不同類型的變量數組進行排序。
我們姑且稱這種應用為"從內部調用內部的函數指針"。
3、調用外部的函數
也許你會問,用API不就是調用外部的函數嗎?是的,但有時候我們還是需要直接獲取外部函數的指針。比如通過LoadLibrary動態加載DLL,然后再通過GetProcAddress得到我們需要的函數入口指針,然后再通過這個函數指針來調用外部的函數,這種動態載入DLL的技術可以讓我們更靈活的調用外部函數。
我們稱這種方式為"從內部調用外部的函數指針"
4、不用說,就是我們也可控制"從外部調用外部的函數指針"。不是沒有,比如我們可以加載多個DLL,將其中一個DLL中的函數指針傳到另一個DLL里的函數內。
上面所分的"內"和"外"都是相對而言(DLL實際上還是在進程內),這樣分類有助于以后我們談問題,請記住我上面的分類,因為以后的文章也會用到這個分類來分析問題。
函數指針的使用不外乎上面四種方式。但在實際使用中卻是靈活多變的。比如在C++里繼承和多態,在COM里的接口,都是一種叫vTable的函數指針表的巧妙應用。使用函數指針,可以使程序的處理方式更加高效、靈活。
VB文檔里除了介紹過第一方式外,對其它方式都沒有介紹,并且還明確指出不支持“Basic 到 Basic”的函數指針(也就是上面說的第二種方式),實際上,通過一定的HACK,上面四種方式均可以實現。今天,我們就來看看如何來實現第二種方式,因為實現它相對來說比較簡單,我們先從簡單的入手。至于如何在VB內調用外部的函數指針,如何在VB里通過處理vTable接口函數指針跳轉表來實現各種函數指針的巧妙應用,由于這將涉及COM內部原理,我將另文詳述。
其實VB的文檔并沒有說錯,VB的確不支持“Basic 到 Basic”的函數指針,但是我們可以繞個彎子來實現,那就是先從"Basic到API",然后再用第一種方式"外部調用內部的函數指針"來從"API到BASIC",這樣就達到了第二種方式從"Basic 到 Basic"的目的,這種技術我們可以稱之為"強制回調",只有VB里才會有這種古怪的技術。
說得有點繞口,但是仔細想想窗口子類派生技術里CallWindowProc,我們可以用CallWindowProc來強制外部的操作系統調用我們原來的保存的窗口函數指針,同樣我們也完全可以用它來強制調用我們內部的函數指針。
呵呵,前面說過要少講原理多講招式,現在我們就來開始學習招式吧!
考慮我們在VB里來實現和C里一樣支持多關鍵字比較的qsort。完整的源代碼見本文配套代碼,此處僅給出函數指針應用相關的代碼。
最后再看看我們來看看我們最終的qsort的聲明。
上面的ArrayPtr是需要排序數組的第一個元素的指針,nCount是數組的元素個數,nElemSize是每個元素大小,pfnCompare就是我們的比較函數指針。這個聲明和C庫函數里的qsort是極為相似的。
和C一樣,我們完全可以將Basic的函數指針傳遞給Basic的qsort函數。
使用方式如下:
聰明的朋友們,你們是不是已經看出這里的奧妙了呢?作為一個測驗,你能現在就給出在qsort里使用函數指針的方法嗎?比如現在我們要通過調用函數指針來比較數組的第i個元素和第j個元素的大小。
沒錯,當然要使用前面聲明的Compare(其實就是CallWindowProc)這個API來進行強制回調。
具體的實現如下:
招式介紹完了,明白了嗎?我再來簡單地講解一下上面Compare的意思,它非常巧妙地利用了CallWindowProc這個API。這個API需要五個參數,第一個參數就是一個普通的函數指針,這個API能夠強馬上回調這個函數指針,并將這個API的后四個Long型的參數傳遞給這個函數指針所指向的函數。這就是為什么我們的比較函數必須要有四個參數的原因,因為CallWindowProc這個API要求傳遞給的函數指針必須符合WndProc函數原形,WndProc的原形如下:
上面的LRESULT、HWND、UINT、WPARAM、LPARAM都可以對應于VB里的Long型,這真是太好了,因為Long型可以用來作指針嘛!
再來看看工作流程,當我們用AddressOf CompareSalaryName做為函數指針參數來調用qsort時,qsort的形參pfnCompare被賦值成了實參CompareSalaryName的函數指針。這時,調用Compare來強制回調pfnCompare,就相當于調用了如下的VB語句:
這不會引起參數類型不符錯誤嗎?CompareSalaryName的前兩個參數不是TEmployee類型嗎?的確,在VB里這樣調用是不行的,因為VB的類型檢查不會允許這樣的調用。但是,實際上這個調用是API進行的回調,而VB不可能去檢查API回調的函數的參數類型是一個普通的Long數值類型還是一個結構指針,所以也可以說我們繞過了VB對函數參數的類型檢查,我們可以將這個Long型參數聲明成任何類型的指針,我們聲明成什么,VB就認為是什么。所以,我們要小心地使用這種技術,如上面最終會傳遞給CompareSalaryName函數的參數"ArrayPtr + (i - 1) * nElemSize"只不過是一個地址,VB不會對這個地址進行檢查,它總是將這個地址當做一個TEmployee類型的指針,如果不小心用成了"ArrayPtr + i * nElemSize",那么當i是最后一個元素時,我們就會引起內存越權訪問錯誤,所以我們要和在C里處理指針一樣注意邊界問題。
函數指針的巧妙應用這里已經可見一斑了,但是這里介紹的方法還有很大的局限性,我們的函數必須要有四個參數,更干凈的做法還是在VC或Delphi里寫一個DLL,做出更加符合要求的API來實現和CallWindowProc相似的功能。我跟蹤過CallWindowProc的內部實現,它要做許多和窗口消息相關的工作,這些工作在我們這個應用中是多余的。其實實現強制回調API只需要將后幾個參數壓棧,再call第一個參數就行了,不過幾條匯編指令而已。
正是因為CallWindowProc的局限性,我們不能夠用它來調用外部的函數指針,以實現上面說的第三種函數指針調用方式。要實現第三種方式,Matt Curland大師提供了一個噩夢一般的HACK方式,我們要在VB里憑空構造一個IUnknown接口,在IUnknown接口的vTable原有的三個入口后再加入一個新入口,在新入口里插入機器代碼,這個機器代碼要處理掉this指針,最后才能調用到我們給的函數指針,這個函數指針無論是內部的還是外部的都一樣沒問題。在我們深入討論COM內部原理時我會再來談這個方法。
另外,排序算法是個見仁見智的問題,我本來想,在本文提供一個最通用性能最好的算法,這種想法雖好,但是不可能有在任何情況下都“最好”的算法。本文提供的用各種指針技術來實現的快速排序方法,應該比用對象技術來實現同樣功能快不少,內存占用也少得多。可是就是這個已經經過了我不少優化的快速排序算法,還是比不了ShellSort,因為ShellSort實現上簡單。從算法的理論上來講qsort應該比ShellSort平均性能好,但是在VB里這不一定(可見本文配套代碼,里面也提供了VBPJ一篇專欄的配套代碼ShellSort,非常得棒,本文的思想就取自這個ShellSort)。
但是應當指出無論是這里的快速排序還是ShellSort,都還可以大大改進,因為它們在實現上需要大量使用CopyMemroy來拷貝數據(這是VB里使用指針的缺點之一)。其實,我們還有更好的方法,那就是Hack一下VB的數組結構,也就是COM自動化里的SafeArray,我們可以一次性的將SafeArray里的各個數組元素的指針放到一個long型數組里,我們無需CopyMemroy,我們僅需交換Long型數組里的元素就可以達到實時地交換SafeArray數組元素指針的目的,數據并沒有移動,移動的僅僅是指針,可以想象這有快多。
AddressOf得到一個VB內部的函數指針,我們可以將這個函數指針傳遞給需要回調這個函數的API,它的作用就是讓外部的程序可以調用VB內部的函數。
但是VB里函數指針的應用,遠不象C里應用那么廣泛,因為VB文檔里僅介紹了如何將函數指針傳遞給API以實現回調,并沒指出函數指針諸多神奇的功能,因為VB是不鼓勵使用指針的,函數指針也不例外。
首先讓我們對函數指針的使用方式來分個類。
1、回調。這是最基本也是最重要的功能。比如VB文檔里介紹過的子類派生技術,它的核心就是兩個API:SetWindowLong和CallWindowProc。
我們可以使SetWindowLong這個API來將原來的窗口函數指針換成自己的函數指針,并將原來的窗口函數指針保存下來。這樣窗口消息就可以發到我們自己的函數里來,并且我們隨時可以用CallWindowProc來調用前面保存下來的窗口指針,以調用原來的窗口函數。這樣,我們可以在不破壞原有窗口功能的前提下處理鉤入的消息。
具體的處理,我們應該很熟悉了,VB文檔也講得很清楚了。這里需要注意的就是CallWindowProc這個API,在后面我們將看到它的妙用。
在這里我們稱回調為讓"外部調用內部的函數指針"。
2、程序內部使用。比如在C里我們可以將C函數指針作為參數傳遞給一個需要函數指針的C函數,如后面還要講到的C庫函數qsort,它的聲明如下:
| #define int (__cdecl *COMPARE)(const void *elem1, const void *elem2) void qsort(void *base, size_t num, size_t width, COMPARE pfnCompare); |
它需要一個COMPARE類型函數指針,用來比較兩個變量大小的,這樣排序函數可以調用這個函數指針來比較不同類型的變量,所以qsort可以對不同類型的變量數組進行排序。
我們姑且稱這種應用為"從內部調用內部的函數指針"。
3、調用外部的函數
也許你會問,用API不就是調用外部的函數嗎?是的,但有時候我們還是需要直接獲取外部函數的指針。比如通過LoadLibrary動態加載DLL,然后再通過GetProcAddress得到我們需要的函數入口指針,然后再通過這個函數指針來調用外部的函數,這種動態載入DLL的技術可以讓我們更靈活的調用外部函數。
我們稱這種方式為"從內部調用外部的函數指針"
4、不用說,就是我們也可控制"從外部調用外部的函數指針"。不是沒有,比如我們可以加載多個DLL,將其中一個DLL中的函數指針傳到另一個DLL里的函數內。
上面所分的"內"和"外"都是相對而言(DLL實際上還是在進程內),這樣分類有助于以后我們談問題,請記住我上面的分類,因為以后的文章也會用到這個分類來分析問題。
函數指針的使用不外乎上面四種方式。但在實際使用中卻是靈活多變的。比如在C++里繼承和多態,在COM里的接口,都是一種叫vTable的函數指針表的巧妙應用。使用函數指針,可以使程序的處理方式更加高效、靈活。
VB文檔里除了介紹過第一方式外,對其它方式都沒有介紹,并且還明確指出不支持“Basic 到 Basic”的函數指針(也就是上面說的第二種方式),實際上,通過一定的HACK,上面四種方式均可以實現。今天,我們就來看看如何來實現第二種方式,因為實現它相對來說比較簡單,我們先從簡單的入手。至于如何在VB內調用外部的函數指針,如何在VB里通過處理vTable接口函數指針跳轉表來實現各種函數指針的巧妙應用,由于這將涉及COM內部原理,我將另文詳述。
其實VB的文檔并沒有說錯,VB的確不支持“Basic 到 Basic”的函數指針,但是我們可以繞個彎子來實現,那就是先從"Basic到API",然后再用第一種方式"外部調用內部的函數指針"來從"API到BASIC",這樣就達到了第二種方式從"Basic 到 Basic"的目的,這種技術我們可以稱之為"強制回調",只有VB里才會有這種古怪的技術。
說得有點繞口,但是仔細想想窗口子類派生技術里CallWindowProc,我們可以用CallWindowProc來強制外部的操作系統調用我們原來的保存的窗口函數指針,同樣我們也完全可以用它來強制調用我們內部的函數指針。
呵呵,前面說過要少講原理多講招式,現在我們就來開始學習招式吧!
考慮我們在VB里來實現和C里一樣支持多關鍵字比較的qsort。完整的源代碼見本文配套代碼,此處僅給出函數指針應用相關的代碼。
| '當然少不了的CopyMemory,不用ANY的版本。 Declare Sub CopyMemory Lib "kernel32" Alias _ "RtlMoveMemory" (ByVal dest As Long, ByVal source As Long, _ ByVal numBytes As Long) '嘿嘿,看下面是如何將CallWindowProc的聲明做成Compare聲明的。 Declare Function Compare Lib "user32" Alias _ "CallWindowProcA" (ByVal pfnCompare As Long, ByVal pElem1 As Long, _ ByVal pElem2 As Long, ByVal unused1 As Long, _ ByVal unused2 As Long) As Integer '注:ByVal xxxxx As Long ,還記得吧!這是標準的指針聲明方法。 '聲明需要比較的數組元素的結構 Public Type TEmployee Name As String Salary As Currency End Type '再來看看我們的比較函數 '先按薪水比較,再按姓名比較 Function CompareSalaryName(Elem1 As TEmployee, _ Elem2 As TEmployee, _ unused1 As Long, _ unused2 As Long) As Integer Dim Ret As Integer Ret = Sgn(Elem1.Salary - Elem2.Salary) If Ret = 0 Then Ret = StrComp(Elem1.Name, Elem2.Name, vbTextCompare) End If CompareSalaryName = Ret End Function '先按姓名比較,再按薪水比較 Function CompareNameSalary(Elem1 As TEmployee, _ Elem2 As TEmployee, _ unused1 As Long, _ unused2 As Long) As Integer Dim Ret As Integer Ret = StrComp(Elem1.Name, Elem2.Name, vbTextCompare) If Ret = 0 Then Ret = Sgn(Elem1.Salary - Elem2.Salary) End If CompareNameSalary = Ret End Function |
最后再看看我們來看看我們最終的qsort的聲明。
| Sub qsort(ByVal ArrayPtr As Long, ByVal nCount As Long, _ ByVal nElemSize As Integer, ByVal pfnCompare As Long) |
上面的ArrayPtr是需要排序數組的第一個元素的指針,nCount是數組的元素個數,nElemSize是每個元素大小,pfnCompare就是我們的比較函數指針。這個聲明和C庫函數里的qsort是極為相似的。
和C一樣,我們完全可以將Basic的函數指針傳遞給Basic的qsort函數。
使用方式如下:
| Dim Employees(1 To 10000) As TEmployee '假設下面的調用對Employees數組進行了賦值初始化。 Call InitArray() '現在就可以調用我們的qsort來進行排序了。 Call qsort(VarPtr(Employees(1)), UBound(Employees), _ LenB(Employees(1)), AddressOf CompareSalaryName) '或者先按姓名排,再按薪水排 Call qsort(VarPtr(Employees(1)), UBound(Employees), _ LenB(Employees(1)), AddressOf CompareNameSalary) |
聰明的朋友們,你們是不是已經看出這里的奧妙了呢?作為一個測驗,你能現在就給出在qsort里使用函數指針的方法嗎?比如現在我們要通過調用函數指針來比較數組的第i個元素和第j個元素的大小。
沒錯,當然要使用前面聲明的Compare(其實就是CallWindowProc)這個API來進行強制回調。
具體的實現如下:
| Sub qsort(ByVal ArrayPtr As Long, ByVal nCount As Long, _ ByVal nElemSize As Integer, ByVal pfnCompare As Long) Dim i As Long, j As Long '這里省略快速排序算法的具體實現,僅給出比較兩個元素的方法。 If Compare(pfnCompare, ArrayPtr + (i - 1) * nElemSize, _ ArrayPtr + (j - 1) * nElemSize, 0, 0) > 0 Then '如果第i個元素比第j個元素大則用CopyMemory來交換這兩個元素。 End IF End Sub |
招式介紹完了,明白了嗎?我再來簡單地講解一下上面Compare的意思,它非常巧妙地利用了CallWindowProc這個API。這個API需要五個參數,第一個參數就是一個普通的函數指針,這個API能夠強馬上回調這個函數指針,并將這個API的后四個Long型的參數傳遞給這個函數指針所指向的函數。這就是為什么我們的比較函數必須要有四個參數的原因,因為CallWindowProc這個API要求傳遞給的函數指針必須符合WndProc函數原形,WndProc的原形如下:
| LRESULT (CALLBACK* WNDPROC) (HWND, UINT, WPARAM, LPARAM); |
上面的LRESULT、HWND、UINT、WPARAM、LPARAM都可以對應于VB里的Long型,這真是太好了,因為Long型可以用來作指針嘛!
再來看看工作流程,當我們用AddressOf CompareSalaryName做為函數指針參數來調用qsort時,qsort的形參pfnCompare被賦值成了實參CompareSalaryName的函數指針。這時,調用Compare來強制回調pfnCompare,就相當于調用了如下的VB語句:
| Call CompareSalaryName(ArrayPtr + (i - 1) * nElemSize, _ ArrayPtr + (j - 1) * nElemSize, 0, 0) |
這不會引起參數類型不符錯誤嗎?CompareSalaryName的前兩個參數不是TEmployee類型嗎?的確,在VB里這樣調用是不行的,因為VB的類型檢查不會允許這樣的調用。但是,實際上這個調用是API進行的回調,而VB不可能去檢查API回調的函數的參數類型是一個普通的Long數值類型還是一個結構指針,所以也可以說我們繞過了VB對函數參數的類型檢查,我們可以將這個Long型參數聲明成任何類型的指針,我們聲明成什么,VB就認為是什么。所以,我們要小心地使用這種技術,如上面最終會傳遞給CompareSalaryName函數的參數"ArrayPtr + (i - 1) * nElemSize"只不過是一個地址,VB不會對這個地址進行檢查,它總是將這個地址當做一個TEmployee類型的指針,如果不小心用成了"ArrayPtr + i * nElemSize",那么當i是最后一個元素時,我們就會引起內存越權訪問錯誤,所以我們要和在C里處理指針一樣注意邊界問題。
函數指針的巧妙應用這里已經可見一斑了,但是這里介紹的方法還有很大的局限性,我們的函數必須要有四個參數,更干凈的做法還是在VC或Delphi里寫一個DLL,做出更加符合要求的API來實現和CallWindowProc相似的功能。我跟蹤過CallWindowProc的內部實現,它要做許多和窗口消息相關的工作,這些工作在我們這個應用中是多余的。其實實現強制回調API只需要將后幾個參數壓棧,再call第一個參數就行了,不過幾條匯編指令而已。
正是因為CallWindowProc的局限性,我們不能夠用它來調用外部的函數指針,以實現上面說的第三種函數指針調用方式。要實現第三種方式,Matt Curland大師提供了一個噩夢一般的HACK方式,我們要在VB里憑空構造一個IUnknown接口,在IUnknown接口的vTable原有的三個入口后再加入一個新入口,在新入口里插入機器代碼,這個機器代碼要處理掉this指針,最后才能調用到我們給的函數指針,這個函數指針無論是內部的還是外部的都一樣沒問題。在我們深入討論COM內部原理時我會再來談這個方法。
另外,排序算法是個見仁見智的問題,我本來想,在本文提供一個最通用性能最好的算法,這種想法雖好,但是不可能有在任何情況下都“最好”的算法。本文提供的用各種指針技術來實現的快速排序方法,應該比用對象技術來實現同樣功能快不少,內存占用也少得多。可是就是這個已經經過了我不少優化的快速排序算法,還是比不了ShellSort,因為ShellSort實現上簡單。從算法的理論上來講qsort應該比ShellSort平均性能好,但是在VB里這不一定(可見本文配套代碼,里面也提供了VBPJ一篇專欄的配套代碼ShellSort,非常得棒,本文的思想就取自這個ShellSort)。
但是應當指出無論是這里的快速排序還是ShellSort,都還可以大大改進,因為它們在實現上需要大量使用CopyMemroy來拷貝數據(這是VB里使用指針的缺點之一)。其實,我們還有更好的方法,那就是Hack一下VB的數組結構,也就是COM自動化里的SafeArray,我們可以一次性的將SafeArray里的各個數組元素的指針放到一個long型數組里,我們無需CopyMemroy,我們僅需交換Long型數組里的元素就可以達到實時地交換SafeArray數組元素指針的目的,數據并沒有移動,移動的僅僅是指針,可以想象這有快多。