Matthew Curland的VB函數指針調用
發表時間:2024-02-24 來源:明輝站整理相關軟件相關文章人氣:
[摘要]Matthew Curland簡介: Visual Studio開發小組成員,參與開發了VB的IntelliSense和Object Browser。他是VB資深專家,對VB有非常深入的研究,堪稱VB大師。所著《Advanced Visual Basice》是闡述VB高級編程技巧的一本好書。...
Matthew Curland簡介:
Visual Studio開發小組成員,參與開發了VB的IntelliSense和Object Browser。他是VB資深專家,對VB有非常深入的研究,堪稱VB大師。所著《Advanced Visual Basice》是闡述VB高級編程技巧的一本好書。
本文英文原著可見2000年2月份《Visual Basic Programmer's Journal》(VB程序員月刊)里的《Call Function Pointers》,這是他發表的妙文之一,他的書里的第11章和本文同名,本文應該是這一章節的精華。
之所以推薦此文,是因為它綜合運用了VB里的不少技術。我們可從中看到Matt大師對VB的深刻理解,而各位技術的綜合運用正體現了他深厚的功力。
本文原文:http://www.devx.com/premier/mgznarch/vbpj/2000/02feb00/mc0200/mc0200.asp
(要先注冊成premier用戶)
本文配套代碼:
http://www.devx.com/free/mgznarch/vbpj/code/2000/02feb00/vb0002mc_p.zip
關鍵字:函數指針,COM、對象、接口,vTalbe,VB匯編,動態DLL調用。
級別:高級
要求:了解VB對象編程,了解匯編。
調用函數指針
通過使用函數指針,我們能夠動態地在代碼中插入不同行為的函數,從而使代碼擁有動態改變自身行為的能力。
作者:Matther Curland
要求:使用本文的示例代碼,你需要VB5或VB6的專業版或企業版。
從Visual Basic 5.0開始Basic語言引入了一個重要的特性:AddressOf運算符。這個運算符能夠讓VB程序員直接體會到將自己的函數指針送出去的快感。比如我們在VB里就能夠得到系統字體的列表,我們能夠通過標準的API調用來進行子類化。一句話,我們終于可以象文檔里所說的那樣來使用Win32 API了。
不過,這個新玩具只能給我們帶來短暫的快感,因為這個禮物并不完整。我們可以送出函數指針,但卻沒人能將函數指針送給我們。事實上,我們甚至不能給我們自己送函數指針,這使我們不能夠體驗送禮的真正樂趣(譯者:呵呵,光送禮卻不能收禮的確沒趣)。AddressOf讓我們看到了廣袤天地的一角,但是VB卻不讓我們全面地探索它,因為VB根本就不讓我們調用函數指針,我們只能提供函數指針(譯者:可以先將函數指針送給API,然后讓API回調自已的函數指針來完成函數指針調用的功能,但這還是要先把禮物送給別人)。其實,我們能夠自己來實現調用函數指針的功能,我們可以手工將一個對COM接口的vTable綁定調用變成一個函數指針調用。最妙的是:我們能夠在純VB里寫出調用函數指針的代碼,不需要任何輔助的DLL。
告訴編譯器函數指針是什么樣子,是使VB能夠調用任何函數的關鍵。將參數類型和返回值類型交給VB編譯器,讓編譯器將我們的函數調用編譯到我們的程序里,這樣程序才能在運行時知道怎樣去定位函數。在程序被編譯后,一個函數就是內存里一串匯編字節流,通過CPU解釋執行而形成我們的程序。調用一個函數指針,首先需要程序獲得指向這個函數字節流的指針,再通過x86匯編指令call將當前指令指針(譯注:即x86匯編里的IP寄存器)轉到函數所在的字節流上。在函數完成后,再用ret指令返回給調用此函數的程序來繼續操作。
我下面將要提到的方法,利用了VB自己的函數調用方式,所以我先來解釋一下VB是怎樣來實現函數調用的。VB內部使用三種函數指針,但是,在本質上,不論VB是如何來定位這幾類函數指針,調用它們的方法卻是一樣的。VB編譯器必須知道準確的函數原型才能生成調用函數的代碼。
第一類,最常見的函數指針類型,就是VB用來調用函數的普通指針,這樣的函數定義在標準模塊內(或類模塊里的友元函數和私有函數)。調用友元函數和私有函數時,調用指令定位在當前指令指針的一個偏移地址處,或者先跳到一個記錄著函數位置的查找表里,再跳到函數內(譯者:即先"Call 絕對地址"跳到一個跳轉表內,表里的每個入口都是一個"Jmp"到函數)。這些函數都在同一個工程內,聯結器總是將所有的模塊聯結在一起,所以總是知道在內存何處能夠找到VB內部函數,因此轉移控制到內部函數時,其運行時開銷是很少的。
VB對某些函數指針的調用卻困難得多
對于另兩類函數指針,VB必須在運行時進行額外的工作才能夠找出它們。
第二類,VB調用一個COM對象接口里的方法。我們可能認為建立COM對象的工作是相當復雜的,如果完全用VB來為我們建造COM的所有組成部分的話,但事實上并不是這樣。按照COM的二進制標準,一個COM對象是一個指針,這個指針指向一個結構,這個特定結構的第一個元素是一個指向函數指針數組的指針。這個函數指針數組(又叫虛擬函數表,簡稱vTable)里的前三個指針,一定是標準QueryInterface,AddRef,Release函數。vTable里接下來的函數符合給定的COM對象接口定義里的函數定義(見圖一)
圖一:
函數指針代理是怎么工作的?click here
當VB通過一個對象類型的變量來調用一個COM對象的方法或屬性時,這個變量里存放著對這個COM對象接口的引用。VB要定位函數時,首先要通過COM引用的第一個元素來獲得指向vTalbe的指針,然后才能在vTable里定位函數指針。對一個vTable調用來說,編譯器提供了COM引用和函數指針在vTable里的偏移量。這樣函數指針才能在運行時被動態地選出來。這種雙向間接的方式——兩種指針都必須被計算(譯注:指向vTalbe的指針和vTable里的函數指針都必須在運行時才確定)——使得vTable調用比同一個工程內的直接調用慢得多,因為直接調用不需要任何在運行時才能進行的指針間接指定。
VB對待同一個工程里的類的公有方法和對待外部COM對象里方法完全一樣,都需要查找vTable,這就是為什么在同一個對象內調用一個友元函數會比調用一個公有函數快得多的原因。但是,查找vTable是COM的基礎,它使得VB能夠使用從外部庫里載入的COM對象,也是象Implements這樣的編程概念的實現基礎。動態載入不可能通過靜態聯結來實現,查找vTable的花費是使用動態載入必須付出的代價。
通過Object型變量來進行的后期綁定調用不同于vTable綁定調用。當然,這種差別不在于VB用沒用vTable,這種差別是因為對后期綁定調用VB使用了不同的vTable。當進行后期綁定調用時,編譯器會調用IDispatch接口的GetIDsOfNemes和Invoke。這需要兩次vTable調用和相當多的參數傳遞,所以這樣的處理非常慢,而且必須不斷地定位Invoke,才能通過類型信息調用到真正的函數指針(譯者:真正慢的原因還是Invoke所進行的參數調整。當擁有相應對象的接口類型庫信息時,VB會進行另一種后期綁定——DispID綁定,它只需要在第一次訪問對象時調用GetIDsOfNemes,來獲得所有屬性和方法的DispID,以后的調用只需要對Invoke進行一次vTalbe調用,但由于Invoke才是慢的原因,所以DispID綁定比一般后期綁定快不了多少)。毋庸置疑,當在同一個線程里調用COM對象時,后期綁定將比vTalbe綁定慢幾個數量級(譯者:同線程內要慢數百倍。由于跨邊界的調配開銷,隨跨線程、跨進程、跨機器,兩種綁定方式在速度上的差別將越來越�。�
第三類,通過Declare語句來使用函數指針。Declare使得VB能夠動通過LoadLibraray API來動態載入特定的DLL,并通過GetProcAddress API和函數名(或函數別名)來得到DLL里特定的函數指針。聲明在類型庫里的函數指針是在程序裝入時通過import table(輸入表)來載入的,而通過Declare語句聲明的函數指針是在此函數第一次被調用時裝入(譯者:這兩種方式各有優缺點。使用Declare在調用時載入,一來VB運行時直接支持,使用簡單,二來當需要載入的DLL不存在時可以在運行時通過錯誤捕獲來處理。而使用類型庫一次性載入,一是會增加載入時間,二是當相應的DLL找不到時程序根本就無法起動,但是通過類型庫調用API可以繞過VB運行時動態的DLL載入過程,這在某些時候很有必要)。
動態指定函數指針
無論是Declare還是庫型庫,當函數載入后,VB調用函數指針的方式是一樣的。指針已經因為先前的調用而被載入了,所以第二次調用會更快,并且速度接近調用靜態聯結的函數。Declare語句是VB調用動態載入的函數指針的最自然的方法。但是,函數指針由VB決定而不是由我們來指定(譯者:此為原文直譯,意思應該是:函數指針只能在編譯前指定,由VB來載入,而不能在運行時指定由我們自己動態載入的函數指針),所以我們不能用Declare語句來調用任意的函數指針。Declare語句的限制使我們只能載入在設計時通過Lib和Alias字句指定的函數。
到這里,我已經解釋了VB是怎么樣來調用自己的函數指針的。對VB本身沒有的功能進行擴展都應該通過VB本身提供的工具來實現(譯者:看來作者Matt是一位VB純粹論支持者)。靜態聯結不用考慮——如果你喜歡自己修改PE文件頭的話,請自便(譯者:關于修改PE頭來Hook輸入函數的方法,在1998年2月MSJ專欄Bugslayer里,John Robbins大師就用純VB實現了HookImportedFunctionsByName,不過用來調用函數指針那是殺雞用牛刀)。我們不可能靜態地指定函數指針,所以Declare語句也不用考慮。但是,我們能夠在VB里自己用LoadLibaray和GetProcAddress這兩個API來從外部DLL里獲取函數指針,就象Declare為我們做的那樣。vTable調用是唯一一種讓VB自已綁定函數的調用方式。我們的任務是建一個符合COM二進制標準的結構,再將這個手工建立的COM對象的引用放到一個對象類型的變量里,然后調用手工建立的vTable入口。通過調用這個vTable里的函數,就能夠直接代理到要調用的函數指針。我稱這個對象為FunctionDelegotor(函數代理者)。
這個方法需要我們解決三個特有的問題。第一,vTalbe調用有額外的參數(this指針),我們不想將它也傳給我們的函數指針。所以我們需要一個通用的代理函數來將這個額外的this指針處理掉,然后才能進行調用。第二,我們需要建立一個vTable里有這個代理函數的COM對象。第三,我們需要一個接口定義才能讓VB編譯器知道我們的函數指針的樣子。接口定義應該將函數原型也包括在vTable里,并且和代理函數在對象vTable里的位置一樣(譯者:當通過接口調用函數指針時,只有這樣才能夠讓代理函數處理掉做為函數參數壓在棧里的this指針)。
我們可以用匯編代碼很容易地的寫出代理函數(譯者:對作者Matt來說的確很容易,因為他對在VB里插入線內匯編代碼有相當深入的研究。其實作者這里的容易也是相對于Alpha平臺來說的)。在Intel平臺,所有傳遞給COM對象或標準API調用的參數都是通過堆棧來傳的。不幸的是,對Alpha平臺的VB來說不是這樣,它不能提供一種簡單的方法來寫出同樣功能的匯編代碼(譯注:Alpha平臺是一個RISC精簡指令集系統,其參數傳遞多直接使用寄存器,要在這個平臺上手工寫匯編代碼要難得,從他的書的目錄里知道他在書里專門拿出一節介紹Alpha平臺下的匯編代碼)。
壓棧
只要我們知道棧是什么樣子,我們就可以很清楚的知道匯編代碼需要做什么。VB僅僅支持符合stdcall調用規范的函數。這種調用規范,參數總是從右向左壓入棧中,并且是由調用者來負責棧的清理。清理的義務跟本文沒什么關系,但是壓棧的順序卻很重要。尤其要注意的是COM類里的this指針(在VB類里稱為Me),它總是作為最左邊的參數壓棧的。當函數被調用時,函數返回地址(函數返回后程序繼續執行的地方)也被call指令本身壓入棧中。在任何COM接口輸出函數被執行前,棧的樣子如下:
parameter n (第n個參數,最右邊的參數)
...
parameter 2
parameter 1 (第1個參數)
this pointer(暗藏的this指針才是最左前的參數)
return address (返回地址)
但是,我們只想調用函數指針,并不需要暗藏的相關聯的this指針。調用一個符合vTable調用卻沒有額外參數的函數,需要我們將this指針從棧里擠出來,然后才能將控制轉移到目標函數指針。讓this指針在棧里放著的好處是因為它指向結構�?紤]我們定義了一個結構,它的第二個成員是一個函數指針。這個成員距結構開始位置的偏移是4個字節。那么將這個函數指出擠出來并通過代理函數調用它的匯編代碼如下:
;彈出返回地址到臨時的ecx寄存器,
;后面還要將它恢復。
pop ecx
;從棧里彈掉this指針(譯注:做為后面跳轉的基址)
pop eax
;重新將ecx寄存器里保存的返回地址壓棧
;以使得函數指針調用后知道返回到哪兒
push ecx
;將控制轉移到函數指針,
;它在this指針后偏移4個字節處。
jmp DWORD PTR [eax + 4]
這四條指令的連在一起需要6個字節:59 58 51 FF 60 04。我們在后面補兩個Int3指令(CC CC)以湊足8個字節,這正好可以一個VB的Currency變量內。這樣一個Currency變量的地址里會放著如下的magic number(幻數)——368956918007638.6215@ ——這個Currency變量是指向代理函數的函數指針。這個代理函數擠掉this指針,并可跳到任何函數,而不用考慮函數的參數。這就是說,我們可以用同樣的匯編代碼來代理任何函數指針。我們現在需要一個vTable來包含這個指向字節流的指針,它實際上是一個函數。(譯者:即用vTable的某個入口包含代理函數指針)。
使用代理函數需要用到一個結構,它偏移4字節處是我們要調用的函數指針。我們還需要它偏移0個字節處是一個指向vTable的指針,這樣才能讓這個結構和一個COM對象一樣,只有這樣VB才能調用到vTable里的函數。我們并沒必要為了一個簡單的函數指針調用而在堆里分配內存;相反,我們僅需在調用代碼的某個地方聲明一個FunctionDelegator結構的變量。雖然我們提供了AddRef和Release函數,但它們不做任何事,只不過是遷就一下VB(譯者:VB她對我們的對象引用進行嚴格的跟蹤。每當我們新增一個對我們對象的引用,她就會調用一次我們對象里的AddRef,以準確計錄對象被引用的次數;每當我們的一個引用和對象分手,她又會調用Release來通知我們的對象減少引用計數。VB她這樣做是為了當我們所有的引用都和對象分手后,對象能夠在內存里被干凈地拋棄。為了遷就VB她的這個習慣,哪怕我們手工建立的對象并不動態分配內存,我們的對象也必須提供AddRef和Release)。所以第四個vTable入口是一個指向代理函數匯編代碼的指針。函數代理的代碼里聲明了一個UDT來包含一個vTalbe數組指針。(代碼見Listing1)
將結構轉換成COM對象
當我們將一個指向合法vTable的指針傳給FunctionDelegator結構,并將這個結構拷貝到一個對象變量里,這個結構就成為合法的COM對象了。這個對象的QueryInterface(譯者:以下簡稱QI)函數相信我們所要求的接口vTalbe的第四個入口的函數原型總是和函數指針相符的。如果不支持所要求的接口,QI函數通常返回E_NOINTERFACE錯誤。這個錯誤狀態在VB里表現出來就是在停在Set語句上的類型不符錯誤。FunctionDelegator對象的這種信任的設計要求我們必須自己來保證類型安全,我們永遠不要向這個對象請求一個不符合函數指針原型的接口。如果我們破壞了這個規則,對我們的懲罰就將是崩潰而不是類型不匹配錯誤了(譯者:要體會這種懲罰,可以試著將Listing1代碼里的InitDelegator返回的接口用VB里的任意接口來引用,比如用Shape,由于其第四個接口定義不符,崩潰)。
FunctionDelegator的vTalbe不進行任何引用計數,所以我們不用編寫任何tear-down(嚴重錯誤處理)或內存釋放代碼。當棧越出它的scope時(譯者:此處的scope是指FunctionDelegator對象變量的變量范圍,即聲明和使用它的過程級或模塊級范圍),COM對象所使用的內存會自動從棧里清除,這意味著InitDelegator所返回的COM對象必然在結構自己銷毀之前(或同時)被銷毀。
在VB能夠調用到代理函數之前,還有一個步驟:我們必須為我們想要調用的函數指針定義一個接口。通過使用mktylib工具來生成對象定義語言(ODL)文件,我們能夠非常容易地做到這一點。盡管mktylib.exe是midl.exe的一個官方的功能簡化版本,但當我們要生成給VB使用的嚴格的類型庫時,mktylib.exe相對更容易使用。而且,不同于midl.exe,mktylib.exe它是和單獨的VB產品一起銷售的。我們的接口定義必須繼承自IUnknown并且有一個附加的函數。當我們僅僅使用ODL待性而不使用oleautomation特性時,我們能夠避免OLE自動化在注冊表里的HKCR\Interface主鍵下寫入不必要的注冊鍵值。雖然我們的QI函數忽略uuid,但是它還是需要我們建立類型庫。(譯者:雖然可以通過ActiveX工程來生成包含類型庫的組件,這樣可以不用外部工具就能生成類型庫,但是VB里所有的組件都是支持OLE自動化的,它們必須在注冊表里注冊鍵值。更重要的是,VB所生成的接口都繼承自IDispatch,其vTable并不符合本文的要求。如果不想使用對象定義語言,而想用更純的VB地來做,就必須修改代理函數的實現,因為繼承至IDispatch后,我們只能在vTable的第八個入口里放代理函數指針。雖然這種做法可行,但是實現起來很復雜,因為需要手工建立能遷就VB的IDispatch,而這決不象本文手工建立 IUnknown接口這么簡單。雖然可能,但這個彎子繞得太大了)
作為例子,這里定義了三種函數。第一種是在排序算法中回調的標準的比較函數原型。第二種函數指針調用能夠返回COM HRESULT錯誤代碼,比如DllRegisterServer。第三種是一個即沒有參數也沒有返回值的函數。我們可以按照自己的需要來加入函數聲明。保存經過我們修改的FuncDecl.odl文件,并且執行mktylib FuncDecl.odl,然后再將FuncDecl.tlb的引用加入我們的工程。(見Listing2里的ODL)
我們能夠看到,通過調用下面的一對函數,我們的確是可以實時調用函數指針了,而很長時間以來,對VB程序員來說,想使用這對函數是不可能的,這對函數就是DllRegisterServer和DllUnregisterServer。通過訪問這兩個標準的ActiveX DLL和OCX入口函數,可以讓我們的EXE按照自已的需要來定位和注冊自己的組件(譯者:這個技術還是有相當價值的。雖然能夠通過Shell語句調用RegSvr32.exe來注冊組件,但是它僅支持標準的入口:DllRegisterServer和DllUnregisterServer。而使用這里的技術,我們就能夠調用非標準的入口,在ATL工程里將兩個兩個輸出函數換個名字,我們在VB里依然可以注冊,這樣簡單的操作就能起到一定的保護組件的作用)。對這樣的外部函數來說,我們是通過LoadLibrary和GetProcAddress調用來從外部DLL獲取函數指針,并將這個函數指針移到FunctionDelegator結構里以使我們能夠調用這個函數指針本身。(見Listing3)
使用函數指針來排序
(譯者:這里原文用了幾段來演示如何通過函數指針回調的方法來進行數組排序。僅就本文要談的函數指針調用來說,這和Listing3里的處理方式類似,因為此處省略這幾段。)
我們能夠在很多方面使用這種調用函數指針技術。比如,我們可以通過在運行時插入具有不同行為的函數來動態改變某段代碼的行為。我們也可以通過這種技術在VB里實現type casting(強制類型轉換)(譯者:通過VarPtr得到一個變量的無類型指針,然后將這個指針做為參數,將這個指針傳給不同的類型轉換函數指針,并調用之,即可實現強制類型轉換)。我不可能把所有可能的應用都列出來,但是這里我再來演示一段小程序。
我們經常想在調試已編譯的VB組件時,能在捕獲一個錯誤的同時跳到調試器內。標準的方法就是運行Int3命令,這時會出現一個系統異常對話框來讓我們選擇是起動調試器還是直接結束崩潰的程序。我們需要運行的函數有兩條匯編指令:break(Int3)和return(ret)。相應的ASM指令為CC和C3。用下面來代碼來實現一個這樣的FunctionDelegator:
Dim FDVoid As FunctionDelegator
Dim CallVoid As ICallVoid
Dim Int3Ret As Integer
Int3Ret = &HC3CC
Set CallVoid = InitDelegator( _
FDVoid, VarPtr(Int3Ret))
'中斷并進入調試器
CallVoid.Void
在VB里的In-line assembly(線內匯編)代碼給VB的表達能力提供了無限的可能性(譯者:實際上這和C里的線內匯編有很大不同,我們只能插入機器代碼,我覺得此處稱為In-Line Machine Code線內機器代碼更合適)。 我們這里演示的函數實際上和DebugBreak這個API的功能是一樣的(譯者:僅就這個函數的功能來說還不如直接用DebugBreak),但是實現別的功能就不是這么簡單了。如果我們需要更多的字節,可以用一個Long或Currency數組來填字節流,并用VarPtr取得指向數組第0個元素的指針來作為函數指針。
(全文完)
Listing 1 這段代碼將一個FunctionDelegator轉換成一個支持特定函數指針的COM對象。這是一個特殊的COM對象,因為它不要求任何內存分配并且對我們的接口請求總是盲目合作。請求僅有的正確接口是我們的責任。
'The magic number
Private Const cDelegateASM _
As Currency = -368956918007638.6215@
'到處到用的輔助函數
Private Declare Sub CopyMemory _
Lib "kernel32" Alias "RtlMoveMemory" _
(pDest As Any, pSrc As Any, ByVal ByteLen As Long)
Private m_DelegateASM As Currency
'vTable的類型聲明
Private Type DelegatorVTables
'OKQI vtable in 0 to 3, FailQI vtable in 4 to 7
VTable(7) As Long
End Type
Private m_VTables As DelegatorVTables
'指向vtable的指針, 成功QI
Private m_pVTableOKQI As Long
'指向vtable的指針, 失敗QI
Private m_pVTableFailQI As Long
'函數指針代理的結構聲明
Public Type FunctionDelegator
pVTable As Long 'This has to stay at offset 0
pfn As Long 'This has to stay at offset 4
End Type
'初始化FunctionDelegator結構,并將指向它的指針
' 作為一個COM對象返回.
Public Function InitDelegator( _
Delegator As FunctionDelegator, _
Optional ByVal pfn As Long) As IUnknown
'第一次訪問時初始化vTable
If m_pVTableOKQI = 0 Then InitVTables
With Delegator
.pVTable = m_pVTableOKQI
.pfn = pfn
End With
CopyMemory InitDelegator, VarPtr(Delegator), 4
End Function
'初始化vTable
Private Sub InitVTables()
Dim pAddRefRelease As Long
With m_VTables
.VTable(0) = _
FuncAddr(AddressOf QueryInterfaceOK)
.VTable(4) = _
FuncAddr(AddressOf QueryInterfaceFail)
pAddRefRelease = FuncAddr(AddressOf AddRefRelease)
.VTable(1) = pAddRefRelease
.VTable(5) = pAddRefRelease
.VTable(2) = pAddRefRelease
.VTable(6) = pAddRefRelease
m_DelegateASM = cDelegateASM
.VTable(3) = VarPtr(m_DelegateASM)
.VTable(7) = .VTable(3)
m_pVTableOKQI = VarPtr(.VTable(0))
m_pVTableFailQI = VarPtr(.VTable(4))
End With
End Sub
'成功QI
Private Function QueryInterfaceOK( _
This As FunctionDelegator, _
riid As Long, pvObj As Long) As Long
'對第一次請求總是盲目合作
pvObj = VarPtr(This)
'交換成失敗時vTable,僅在調用函數指針會返回HRESULT錯誤代碼
' 時才需要這么做,當然這么做總是更安全。
This.pVTable = m_pVTableFailQI
End Function
Private Function AddRefRelease( _
ByVal This As Long) As Long
'什么都不做,無需要引用計數。
End Function
'失敗QI
Private Function QueryInterfaceFail( _
ByVal This As Long, _
riid As Long, pvObj As Long) As Long
'對任何請求都說:"不"
pvObj = 0
QueryInterfaceFail = &H80004002 'E_NOINTERFACE
End Function
'返回函數指針的輔助函數
Private Function FuncAddr (ByVal pfn As Long) As Long
FuncAddr = pfn
End Function
譯者:上面的代碼在原文已經發表后經過了修改,因此原文沒有提到為什么上面的代碼需要兩個不同的vTable。Matt在更新的示例代碼的Readme文件里解釋這個原因。我下面將這個原因簡單的敘述如下:
這是因為當調用的函數指針需要返回HRESULT錯誤代碼時,VB會用再次調用QI來向對象請求一個ISupportErrorInfo接口的引用。但是,由于原來代碼里的QI完全采用盲目合作的信任方式,它總是返回對象自身的接口指針,哪怕它并不支持所要求的接口。由于返回的接口引用并不支持ISupportErrorInfo,所以當VB試圖用ISupportErrorInfo的方法來搜集錯誤信息時程序就會崩潰。解決的辦法,就是提供兩個vTable。當第一次調用初始化后的vTable里的QI時,它采取信任方式返回接口指針,并在返回之前將包含失敗QI的vTable交換進來。這樣下一次訪問的QI將是失敗QI,而失敗QI拒絕所有接口請求,這樣就有效的阻塞了后繼的QI請求,包括VB對ISupportErrorInfo的請求。在后面的Listing3的代碼中我們可以看到,一旦我們增加引用就會有類型不匹配錯誤。
還有VB在對Err對象的處理上有BUG,那就是當VB用QI向某個對象請求ISupportErrorInfo接口失敗后,Err對象內總是保留著對這個對象的引用。由于我們的vTalbe會先于Err對象釋放,所以Err對象里有一個掛起的引用,當釋放Err對象時程序會崩潰。解決的方法是:在程序結束前自己用Err.Raise來引發一個新錯誤。具體做法,見源代碼。
Listing 2 用來告訴VB編譯怎樣調用我們的函數指針的外部ODL文件。沒有對這個接口的描述,我們雖仍能生成代理到正確函數指針的COM對象,但卻沒有辦法來調用vTable里的函數。
[
uuid(57EC3F60-5425-11d3-AB5C-D41203C10000),
helpstring("Function pointer declarations"),
lcid(0x0),
version(1.0)
]
library FuncDeclLib
{
importlib("stdole2.tlb");
[uuid(57EC3F61-5425-11d3-AB5C-D41203C10000), odl]
interface ICallCompare : IUnknown
{
long Compare(
[in] long Elem1,
[in] long Elem2);
}
[uuid(57EC3F62-5425-11d3-AB5C-D41203C10000), odl]
interface ICallHRESULTNoParams : IUnknown
{
HRESULT Call();
}
[uuid(57EC3F63-5425-11d3-AB5C-D41203C10000), odl]
interface ICallVoid : IUnknown
{
void Void();
}
}
Listing 3 為了實現標準的ActiveX DLL和OCX的注冊,我們需要將DLL裝入內存,找到用來注冊的入口函數指針,然后再調用這個指針。通過使用FunctionDelegator對象,我們能對任意的DLL進行同樣的操作。
Private Declare Function LoadLibrary _
Lib "kernel32" Alias "LoadLibraryA" _
(ByVal lpFileName As String) As Long
Private Declare Function FreeLibrary Lib "kernel32" _
(ByVal hModule As Long) As Long
Private Declare Function GetProcAddress Lib "kernel32" _
(ByVal hModule As Long, _
ByVal lpProcName As String) As Long
Public Sub DllRegisterServer(DllName As String)
CallDllRegEntry DllName, "DllRegisterServer"
End Sub
Public Sub DllUnregisterServer(DllName As String)
CallDllRegEntry DllName, "DllUnregisterServer"
End Sub
Private Sub CallDllRegEntry (DllName As String, _
EntryPoint As String)
Dim pCall As ICallHRESULTNoParams
Dim Delegator As FunctionDelegator
Dim hMod As Long
Dim pfn As Long
'Load the dll
hMod = LoadLibrary(DllName)
If hMod = 0 Then Err.Raise 5
'Error trap to make sure we free the library
On Error GoTo Error
'找到函數指針
pfn = GetProcAddress(hMod, EntryPoint)
If pfn = 0 Then Err.Raise 5
'初始化并得到代理COM對象的引用。
Set pCall = InitDelegator(Delegator, pfn)
'調用函數指針
pCall.Call
''*****譯者:取消注釋下面部分可以來體驗文中所說錯誤和崩潰
' Set pCall = Nothing
' Dim pIUn As IUnknown, pShape2 As Shape
' Set pIUn = InitDelegator(Delegator, pfn)
' Dim pCallVoid As ICallVoid
' Set pCallVoid = pIUn
'
''類型不匹配錯誤,因為此時QI已經被換成了失敗QI。
' 'Set pShape2 = pIUn
' Set pIUn = Nothing
' Set pCallVoid = Nothing
'
''崩潰,因為接口定義和函數指針不符
' 'Set pShape2 = InitDelegator(Delegator, pfn)
''**********************************************************
Error:
'Free the library handle
FreeLibrary hMod
'Propagate any error
With Err
If .Number Then .Raise .Number
End With
End Sub