top
Loading...
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,它的聲明如下:

#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數組元素指針的目的,數據并沒有移動,移動的僅僅是指針,可以想象這有快多。
作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗