明輝手游網(wǎng)中心:是一個(gè)免費(fèi)提供流行視頻軟件教程、在線學(xué)習(xí)分享的學(xué)習(xí)平臺(tái)!

Matthew Curland的VB函數(shù)指針調(diào)用

[摘要]Matthew Curland簡(jiǎn)介: Visual Studio開發(fā)小組成員,參與開發(fā)了VB的IntelliSense和Object Browser。他是VB資深專家,對(duì)VB有非常深入的研究,堪稱VB大師。所著《Advanced Visual Basice》是闡述VB高級(jí)編程技巧的一本好書。...
Matthew Curland簡(jiǎn)介:
    Visual Studio開發(fā)小組成員,參與開發(fā)了VB的IntelliSense和Object Browser。他是VB資深專家,對(duì)VB有非常深入的研究,堪稱VB大師。所著《Advanced Visual Basice》是闡述VB高級(jí)編程技巧的一本好書。
    本文英文原著可見2000年2月份《Visual Basic Programmer's Journal》(VB程序員月刊)里的《Call Function Pointers》,這是他發(fā)表的妙文之一,他的書里的第11章和本文同名,本文應(yīng)該是這一章節(jié)的精華。

    之所以推薦此文,是因?yàn)樗C合運(yùn)用了VB里的不少技術(shù)。我們可從中看到Matt大師對(duì)VB的深刻理解,而各位技術(shù)的綜合運(yùn)用正體現(xiàn)了他深厚的功力。

本文原文:http://www.devx.com/premier/mgznarch/vbpj/2000/02feb00/mc0200/mc0200.asp
(要先注冊(cè)成premier用戶)
本文配套代碼:
http://www.devx.com/free/mgznarch/vbpj/code/2000/02feb00/vb0002mc_p.zip


關(guān)鍵字:函數(shù)指針,COM、對(duì)象、接口,vTalbe,VB匯編,動(dòng)態(tài)DLL調(diào)用。
級(jí)別:高級(jí)
要求:了解VB對(duì)象編程,了解匯編。

                          調(diào)用函數(shù)指針
    通過使用函數(shù)指針,我們能夠動(dòng)態(tài)地在代碼中插入不同行為的函數(shù),從而使代碼擁有動(dòng)態(tài)改變自身行為的能力。

作者:Matther Curland
要求:使用本文的示例代碼,你需要VB5或VB6的專業(yè)版或企業(yè)版。
    從Visual Basic 5.0開始Basic語言引入了一個(gè)重要的特性:AddressOf運(yùn)算符。這個(gè)運(yùn)算符能夠讓VB程序員直接體會(huì)到將自己的函數(shù)指針?biāo)统鋈サ目旄。比如我們(cè)赩B里就能夠得到系統(tǒng)字體的列表,我們能夠通過標(biāo)準(zhǔn)的API調(diào)用來進(jìn)行子類化。一句話,我們終于可以象文檔里所說的那樣來使用Win32 API了。
    不過,這個(gè)新玩具只能給我們帶來短暫的快感,因?yàn)檫@個(gè)禮物并不完整。我們可以送出函數(shù)指針,但卻沒人能將函數(shù)指針?biāo)徒o我們。事實(shí)上,我們甚至不能給我們自己送函數(shù)指針,這使我們不能夠體驗(yàn)送禮的真正樂趣(譯者:呵呵,光送禮卻不能收禮的確沒趣)。AddressOf讓我們看到了廣袤天地的一角,但是VB卻不讓我們?nèi)娴靥剿魉驗(yàn)閂B根本就不讓我們調(diào)用函數(shù)指針,我們只能提供函數(shù)指針(譯者:可以先將函數(shù)指針?biāo)徒oAPI,然后讓API回調(diào)自已的函數(shù)指針來完成函數(shù)指針調(diào)用的功能,但這還是要先把禮物送給別人)。其實(shí),我們能夠自己來實(shí)現(xiàn)調(diào)用函數(shù)指針的功能,我們可以手工將一個(gè)對(duì)COM接口的vTable綁定調(diào)用變成一個(gè)函數(shù)指針調(diào)用。最妙的是:我們能夠在純VB里寫出調(diào)用函數(shù)指針的代碼,不需要任何輔助的DLL。

    告訴編譯器函數(shù)指針是什么樣子,是使VB能夠調(diào)用任何函數(shù)的關(guān)鍵。將參數(shù)類型和返回值類型交給VB編譯器,讓編譯器將我們的函數(shù)調(diào)用編譯到我們的程序里,這樣程序才能在運(yùn)行時(shí)知道怎樣去定位函數(shù)。在程序被編譯后,一個(gè)函數(shù)就是內(nèi)存里一串匯編字節(jié)流,通過CPU解釋執(zhí)行而形成我們的程序。調(diào)用一個(gè)函數(shù)指針,首先需要程序獲得指向這個(gè)函數(shù)字節(jié)流的指針,再通過x86匯編指令call將當(dāng)前指令指針(譯注:即x86匯編里的IP寄存器)轉(zhuǎn)到函數(shù)所在的字節(jié)流上。在函數(shù)完成后,再用ret指令返回給調(diào)用此函數(shù)的程序來繼續(xù)操作。

    我下面將要提到的方法,利用了VB自己的函數(shù)調(diào)用方式,所以我先來解釋一下VB是怎樣來實(shí)現(xiàn)函數(shù)調(diào)用的。VB內(nèi)部使用三種函數(shù)指針,但是,在本質(zhì)上,不論VB是如何來定位這幾類函數(shù)指針,調(diào)用它們的方法卻是一樣的。VB編譯器必須知道準(zhǔn)確的函數(shù)原型才能生成調(diào)用函數(shù)的代碼。

    第一類,最常見的函數(shù)指針類型,就是VB用來調(diào)用函數(shù)的普通指針,這樣的函數(shù)定義在標(biāo)準(zhǔn)模塊內(nèi)(或類模塊里的友元函數(shù)和私有函數(shù))。調(diào)用友元函數(shù)和私有函數(shù)時(shí),調(diào)用指令定位在當(dāng)前指令指針的一個(gè)偏移地址處,或者先跳到一個(gè)記錄著函數(shù)位置的查找表里,再跳到函數(shù)內(nèi)(譯者:即先"Call 絕對(duì)地址"跳到一個(gè)跳轉(zhuǎn)表內(nèi),表里的每個(gè)入口都是一個(gè)"Jmp"到函數(shù))。這些函數(shù)都在同一個(gè)工程內(nèi),聯(lián)結(jié)器總是將所有的模塊聯(lián)結(jié)在一起,所以總是知道在內(nèi)存何處能夠找到VB內(nèi)部函數(shù),因此轉(zhuǎn)移控制到內(nèi)部函數(shù)時(shí),其運(yùn)行時(shí)開銷是很少的。

VB對(duì)某些函數(shù)指針的調(diào)用卻困難得多
    對(duì)于另兩類函數(shù)指針,VB必須在運(yùn)行時(shí)進(jìn)行額外的工作才能夠找出它們。
    第二類,VB調(diào)用一個(gè)COM對(duì)象接口里的方法。我們可能認(rèn)為建立COM對(duì)象的工作是相當(dāng)復(fù)雜的,如果完全用VB來為我們建造COM的所有組成部分的話,但事實(shí)上并不是這樣。按照COM的二進(jìn)制標(biāo)準(zhǔn),一個(gè)COM對(duì)象是一個(gè)指針,這個(gè)指針指向一個(gè)結(jié)構(gòu),這個(gè)特定結(jié)構(gòu)的第一個(gè)元素是一個(gè)指向函數(shù)指針數(shù)組的指針。這個(gè)函數(shù)指針數(shù)組(又叫虛擬函數(shù)表,簡(jiǎn)稱vTable)里的前三個(gè)指針,一定是標(biāo)準(zhǔn)QueryInterface,AddRef,Release函數(shù)。vTable里接下來的函數(shù)符合給定的COM對(duì)象接口定義里的函數(shù)定義(見圖一)




圖一:
函數(shù)指針代理是怎么工作的?click here

    當(dāng)VB通過一個(gè)對(duì)象類型的變量來調(diào)用一個(gè)COM對(duì)象的方法或?qū)傩詴r(shí),這個(gè)變量里存放著對(duì)這個(gè)COM對(duì)象接口的引用。VB要定位函數(shù)時(shí),首先要通過COM引用的第一個(gè)元素來獲得指向vTalbe的指針,然后才能在vTable里定位函數(shù)指針。對(duì)一個(gè)vTable調(diào)用來說,編譯器提供了COM引用和函數(shù)指針在vTable里的偏移量。這樣函數(shù)指針才能在運(yùn)行時(shí)被動(dòng)態(tài)地選出來。這種雙向間接的方式——兩種指針都必須被計(jì)算(譯注:指向vTalbe的指針和vTable里的函數(shù)指針都必須在運(yùn)行時(shí)才確定)——使得vTable調(diào)用比同一個(gè)工程內(nèi)的直接調(diào)用慢得多,因?yàn)橹苯诱{(diào)用不需要任何在運(yùn)行時(shí)才能進(jìn)行的指針間接指定。

    VB對(duì)待同一個(gè)工程里的類的公有方法和對(duì)待外部COM對(duì)象里方法完全一樣,都需要查找vTable,這就是為什么在同一個(gè)對(duì)象內(nèi)調(diào)用一個(gè)友元函數(shù)會(huì)比調(diào)用一個(gè)公有函數(shù)快得多的原因。但是,查找vTable是COM的基礎(chǔ),它使得VB能夠使用從外部庫里載入的COM對(duì)象,也是象Implements這樣的編程概念的實(shí)現(xiàn)基礎(chǔ)。動(dòng)態(tài)載入不可能通過靜態(tài)聯(lián)結(jié)來實(shí)現(xiàn),查找vTable的花費(fèi)是使用動(dòng)態(tài)載入必須付出的代價(jià)。

    通過Object型變量來進(jìn)行的后期綁定調(diào)用不同于vTable綁定調(diào)用。當(dāng)然,這種差別不在于VB用沒用vTable,這種差別是因?yàn)閷?duì)后期綁定調(diào)用VB使用了不同的vTable。當(dāng)進(jìn)行后期綁定調(diào)用時(shí),編譯器會(huì)調(diào)用IDispatch接口的GetIDsOfNemes和Invoke。這需要兩次vTable調(diào)用和相當(dāng)多的參數(shù)傳遞,所以這樣的處理非常慢,而且必須不斷地定位Invoke,才能通過類型信息調(diào)用到真正的函數(shù)指針(譯者:真正慢的原因還是Invoke所進(jìn)行的參數(shù)調(diào)整。當(dāng)擁有相應(yīng)對(duì)象的接口類型庫信息時(shí),VB會(huì)進(jìn)行另一種后期綁定——DispID綁定,它只需要在第一次訪問對(duì)象時(shí)調(diào)用GetIDsOfNemes,來獲得所有屬性和方法的DispID,以后的調(diào)用只需要對(duì)Invoke進(jìn)行一次vTalbe調(diào)用,但由于Invoke才是慢的原因,所以DispID綁定比一般后期綁定快不了多少)。毋庸置疑,當(dāng)在同一個(gè)線程里調(diào)用COM對(duì)象時(shí),后期綁定將比vTalbe綁定慢幾個(gè)數(shù)量級(jí)(譯者:同線程內(nèi)要慢數(shù)百倍。由于跨邊界的調(diào)配開銷,隨跨線程、跨進(jìn)程、跨機(jī)器,兩種綁定方式在速度上的差別將越來越。

    第三類,通過Declare語句來使用函數(shù)指針。Declare使得VB能夠動(dòng)通過LoadLibraray API來動(dòng)態(tài)載入特定的DLL,并通過GetProcAddress API和函數(shù)名(或函數(shù)別名)來得到DLL里特定的函數(shù)指針。聲明在類型庫里的函數(shù)指針是在程序裝入時(shí)通過import table(輸入表)來載入的,而通過Declare語句聲明的函數(shù)指針是在此函數(shù)第一次被調(diào)用時(shí)裝入(譯者:這兩種方式各有優(yōu)缺點(diǎn)。使用Declare在調(diào)用時(shí)載入,一來VB運(yùn)行時(shí)直接支持,使用簡(jiǎn)單,二來當(dāng)需要載入的DLL不存在時(shí)可以在運(yùn)行時(shí)通過錯(cuò)誤捕獲來處理。而使用類型庫一次性載入,一是會(huì)增加載入時(shí)間,二是當(dāng)相應(yīng)的DLL找不到時(shí)程序根本就無法起動(dòng),但是通過類型庫調(diào)用API可以繞過VB運(yùn)行時(shí)動(dòng)態(tài)的DLL載入過程,這在某些時(shí)候很有必要)。

動(dòng)態(tài)指定函數(shù)指針
    無論是Declare還是庫型庫,當(dāng)函數(shù)載入后,VB調(diào)用函數(shù)指針的方式是一樣的。指針已經(jīng)因?yàn)橄惹暗恼{(diào)用而被載入了,所以第二次調(diào)用會(huì)更快,并且速度接近調(diào)用靜態(tài)聯(lián)結(jié)的函數(shù)。Declare語句是VB調(diào)用動(dòng)態(tài)載入的函數(shù)指針的最自然的方法。但是,函數(shù)指針由VB決定而不是由我們來指定(譯者:此為原文直譯,意思應(yīng)該是:函數(shù)指針只能在編譯前指定,由VB來載入,而不能在運(yùn)行時(shí)指定由我們自己動(dòng)態(tài)載入的函數(shù)指針),所以我們不能用Declare語句來調(diào)用任意的函數(shù)指針。Declare語句的限制使我們只能載入在設(shè)計(jì)時(shí)通過Lib和Alias字句指定的函數(shù)。

    到這里,我已經(jīng)解釋了VB是怎么樣來調(diào)用自己的函數(shù)指針的。對(duì)VB本身沒有的功能進(jìn)行擴(kuò)展都應(yīng)該通過VB本身提供的工具來實(shí)現(xiàn)(譯者:看來作者M(jìn)att是一位VB純粹論支持者)。靜態(tài)聯(lián)結(jié)不用考慮——如果你喜歡自己修改PE文件頭的話,請(qǐng)自便(譯者:關(guān)于修改PE頭來Hook輸入函數(shù)的方法,在1998年2月MSJ專欄Bugslayer里,John Robbins大師就用純VB實(shí)現(xiàn)了HookImportedFunctionsByName,不過用來調(diào)用函數(shù)指針那是殺雞用牛刀)。我們不可能靜態(tài)地指定函數(shù)指針,所以Declare語句也不用考慮。但是,我們能夠在VB里自己用LoadLibaray和GetProcAddress這兩個(gè)API來從外部DLL里獲取函數(shù)指針,就象Declare為我們做的那樣。vTable調(diào)用是唯一一種讓VB自已綁定函數(shù)的調(diào)用方式。我們的任務(wù)是建一個(gè)符合COM二進(jìn)制標(biāo)準(zhǔn)的結(jié)構(gòu),再將這個(gè)手工建立的COM對(duì)象的引用放到一個(gè)對(duì)象類型的變量里,然后調(diào)用手工建立的vTable入口。通過調(diào)用這個(gè)vTable里的函數(shù),就能夠直接代理到要調(diào)用的函數(shù)指針。我稱這個(gè)對(duì)象為FunctionDelegotor(函數(shù)代理者)。

    這個(gè)方法需要我們解決三個(gè)特有的問題。第一,vTalbe調(diào)用有額外的參數(shù)(this指針),我們不想將它也傳給我們的函數(shù)指針。所以我們需要一個(gè)通用的代理函數(shù)來將這個(gè)額外的this指針處理掉,然后才能進(jìn)行調(diào)用。第二,我們需要建立一個(gè)vTable里有這個(gè)代理函數(shù)的COM對(duì)象。第三,我們需要一個(gè)接口定義才能讓VB編譯器知道我們的函數(shù)指針的樣子。接口定義應(yīng)該將函數(shù)原型也包括在vTable里,并且和代理函數(shù)在對(duì)象vTable里的位置一樣(譯者:當(dāng)通過接口調(diào)用函數(shù)指針時(shí),只有這樣才能夠讓代理函數(shù)處理掉做為函數(shù)參數(shù)壓在棧里的this指針)。

    我們可以用匯編代碼很容易地的寫出代理函數(shù)(譯者:對(duì)作者M(jìn)att來說的確很容易,因?yàn)樗麑?duì)在VB里插入線內(nèi)匯編代碼有相當(dāng)深入的研究。其實(shí)作者這里的容易也是相對(duì)于Alpha平臺(tái)來說的)。在Intel平臺(tái),所有傳遞給COM對(duì)象或標(biāo)準(zhǔn)API調(diào)用的參數(shù)都是通過堆棧來傳的。不幸的是,對(duì)Alpha平臺(tái)的VB來說不是這樣,它不能提供一種簡(jiǎn)單的方法來寫出同樣功能的匯編代碼(譯注:Alpha平臺(tái)是一個(gè)RISC精簡(jiǎn)指令集系統(tǒng),其參數(shù)傳遞多直接使用寄存器,要在這個(gè)平臺(tái)上手工寫匯編代碼要難得,從他的書的目錄里知道他在書里專門拿出一節(jié)介紹Alpha平臺(tái)下的匯編代碼)。

壓棧
    只要我們知道棧是什么樣子,我們就可以很清楚的知道匯編代碼需要做什么。VB僅僅支持符合stdcall調(diào)用規(guī)范的函數(shù)。這種調(diào)用規(guī)范,參數(shù)總是從右向左壓入棧中,并且是由調(diào)用者來負(fù)責(zé)棧的清理。清理的義務(wù)跟本文沒什么關(guān)系,但是壓棧的順序卻很重要。尤其要注意的是COM類里的this指針(在VB類里稱為Me),它總是作為最左邊的參數(shù)壓棧的。當(dāng)函數(shù)被調(diào)用時(shí),函數(shù)返回地址(函數(shù)返回后程序繼續(xù)執(zhí)行的地方)也被call指令本身壓入棧中。在任何COM接口輸出函數(shù)被執(zhí)行前,棧的樣子如下:

parameter n (第n個(gè)參數(shù),最右邊的參數(shù))
...
parameter 2
parameter 1 (第1個(gè)參數(shù))
this pointer(暗藏的this指針才是最左前的參數(shù))
return address (返回地址)

    但是,我們只想調(diào)用函數(shù)指針,并不需要暗藏的相關(guān)聯(lián)的this指針。調(diào)用一個(gè)符合vTable調(diào)用卻沒有額外參數(shù)的函數(shù),需要我們將this指針從棧里擠出來,然后才能將控制轉(zhuǎn)移到目標(biāo)函數(shù)指針。讓this指針在棧里放著的好處是因?yàn)樗赶蚪Y(jié)構(gòu)。考慮我們定義了一個(gè)結(jié)構(gòu),它的第二個(gè)成員是一個(gè)函數(shù)指針。這個(gè)成員距結(jié)構(gòu)開始位置的偏移是4個(gè)字節(jié)。那么將這個(gè)函數(shù)指出擠出來并通過代理函數(shù)調(diào)用它的匯編代碼如下:

;彈出返回地址到臨時(shí)的ecx寄存器,
;后面還要將它恢復(fù)。
pop ecx

;從棧里彈掉this指針(譯注:做為后面跳轉(zhuǎn)的基址)
pop eax

;重新將ecx寄存器里保存的返回地址壓棧
;以使得函數(shù)指針調(diào)用后知道返回到哪兒
push ecx

;將控制轉(zhuǎn)移到函數(shù)指針,
;它在this指針后偏移4個(gè)字節(jié)處。
jmp DWORD PTR [eax + 4]

這四條指令的連在一起需要6個(gè)字節(jié):59 58 51 FF 60 04。我們?cè)诤竺嫜a(bǔ)兩個(gè)Int3指令(CC CC)以湊足8個(gè)字節(jié),這正好可以一個(gè)VB的Currency變量?jī)?nèi)。這樣一個(gè)Currency變量的地址里會(huì)放著如下的magic number(幻數(shù))——368956918007638.6215@ ——這個(gè)Currency變量是指向代理函數(shù)的函數(shù)指針。這個(gè)代理函數(shù)擠掉this指針,并可跳到任何函數(shù),而不用考慮函數(shù)的參數(shù)。這就是說,我們可以用同樣的匯編代碼來代理任何函數(shù)指針。我們現(xiàn)在需要一個(gè)vTable來包含這個(gè)指向字節(jié)流的指針,它實(shí)際上是一個(gè)函數(shù)。(譯者:即用vTable的某個(gè)入口包含代理函數(shù)指針)。
    
    使用代理函數(shù)需要用到一個(gè)結(jié)構(gòu),它偏移4字節(jié)處是我們要調(diào)用的函數(shù)指針。我們還需要它偏移0個(gè)字節(jié)處是一個(gè)指向vTable的指針,這樣才能讓這個(gè)結(jié)構(gòu)和一個(gè)COM對(duì)象一樣,只有這樣VB才能調(diào)用到vTable里的函數(shù)。我們并沒必要為了一個(gè)簡(jiǎn)單的函數(shù)指針調(diào)用而在堆里分配內(nèi)存;相反,我們僅需在調(diào)用代碼的某個(gè)地方聲明一個(gè)FunctionDelegator結(jié)構(gòu)的變量。雖然我們提供了AddRef和Release函數(shù),但它們不做任何事,只不過是遷就一下VB(譯者:VB她對(duì)我們的對(duì)象引用進(jìn)行嚴(yán)格的跟蹤。每當(dāng)我們新增一個(gè)對(duì)我們對(duì)象的引用,她就會(huì)調(diào)用一次我們對(duì)象里的AddRef,以準(zhǔn)確計(jì)錄對(duì)象被引用的次數(shù);每當(dāng)我們的一個(gè)引用和對(duì)象分手,她又會(huì)調(diào)用Release來通知我們的對(duì)象減少引用計(jì)數(shù)。VB她這樣做是為了當(dāng)我們所有的引用都和對(duì)象分手后,對(duì)象能夠在內(nèi)存里被干凈地拋棄。為了遷就VB她的這個(gè)習(xí)慣,哪怕我們手工建立的對(duì)象并不動(dòng)態(tài)分配內(nèi)存,我們的對(duì)象也必須提供AddRef和Release)。所以第四個(gè)vTable入口是一個(gè)指向代理函數(shù)匯編代碼的指針。函數(shù)代理的代碼里聲明了一個(gè)UDT來包含一個(gè)vTalbe數(shù)組指針。(代碼見Listing1)

將結(jié)構(gòu)轉(zhuǎn)換成COM對(duì)象
    當(dāng)我們將一個(gè)指向合法vTable的指針傳給FunctionDelegator結(jié)構(gòu),并將這個(gè)結(jié)構(gòu)拷貝到一個(gè)對(duì)象變量里,這個(gè)結(jié)構(gòu)就成為合法的COM對(duì)象了。這個(gè)對(duì)象的QueryInterface(譯者:以下簡(jiǎn)稱QI)函數(shù)相信我們所要求的接口vTalbe的第四個(gè)入口的函數(shù)原型總是和函數(shù)指針相符的。如果不支持所要求的接口,QI函數(shù)通常返回E_NOINTERFACE錯(cuò)誤。這個(gè)錯(cuò)誤狀態(tài)在VB里表現(xiàn)出來就是在停在Set語句上的類型不符錯(cuò)誤。FunctionDelegator對(duì)象的這種信任的設(shè)計(jì)要求我們必須自己來保證類型安全,我們永遠(yuǎn)不要向這個(gè)對(duì)象請(qǐng)求一個(gè)不符合函數(shù)指針原型的接口。如果我們破壞了這個(gè)規(guī)則,對(duì)我們的懲罰就將是崩潰而不是類型不匹配錯(cuò)誤了(譯者:要體會(huì)這種懲罰,可以試著將Listing1代碼里的InitDelegator返回的接口用VB里的任意接口來引用,比如用Shape,由于其第四個(gè)接口定義不符,崩潰)。

    FunctionDelegator的vTalbe不進(jìn)行任何引用計(jì)數(shù),所以我們不用編寫任何tear-down(嚴(yán)重錯(cuò)誤處理)或內(nèi)存釋放代碼。當(dāng)棧越出它的scope時(shí)(譯者:此處的scope是指FunctionDelegator對(duì)象變量的變量范圍,即聲明和使用它的過程級(jí)或模塊級(jí)范圍),COM對(duì)象所使用的內(nèi)存會(huì)自動(dòng)從棧里清除,這意味著InitDelegator所返回的COM對(duì)象必然在結(jié)構(gòu)自己銷毀之前(或同時(shí))被銷毀。
    在VB能夠調(diào)用到代理函數(shù)之前,還有一個(gè)步驟:我們必須為我們想要調(diào)用的函數(shù)指針定義一個(gè)接口。通過使用mktylib工具來生成對(duì)象定義語言(ODL)文件,我們能夠非常容易地做到這一點(diǎn)。盡管mktylib.exe是midl.exe的一個(gè)官方的功能簡(jiǎn)化版本,但當(dāng)我們要生成給VB使用的嚴(yán)格的類型庫時(shí),mktylib.exe相對(duì)更容易使用。而且,不同于midl.exe,mktylib.exe它是和單獨(dú)的VB產(chǎn)品一起銷售的。我們的接口定義必須繼承自IUnknown并且有一個(gè)附加的函數(shù)。當(dāng)我們僅僅使用ODL待性而不使用oleautomation特性時(shí),我們能夠避免OLE自動(dòng)化在注冊(cè)表里的HKCR\Interface主鍵下寫入不必要的注冊(cè)鍵值。雖然我們的QI函數(shù)忽略u(píng)uid,但是它還是需要我們建立類型庫。(譯者:雖然可以通過ActiveX工程來生成包含類型庫的組件,這樣可以不用外部工具就能生成類型庫,但是VB里所有的組件都是支持OLE自動(dòng)化的,它們必須在注冊(cè)表里注冊(cè)鍵值。更重要的是,VB所生成的接口都繼承自IDispatch,其vTable并不符合本文的要求。如果不想使用對(duì)象定義語言,而想用更純的VB地來做,就必須修改代理函數(shù)的實(shí)現(xiàn),因?yàn)槔^承至IDispatch后,我們只能在vTable的第八個(gè)入口里放代理函數(shù)指針。雖然這種做法可行,但是實(shí)現(xiàn)起來很復(fù)雜,因?yàn)樾枰止そ⒛苓w就VB的IDispatch,而這決不象本文手工建立 IUnknown接口這么簡(jiǎn)單。雖然可能,但這個(gè)彎子繞得太大了)

    作為例子,這里定義了三種函數(shù)。第一種是在排序算法中回調(diào)的標(biāo)準(zhǔn)的比較函數(shù)原型。第二種函數(shù)指針調(diào)用能夠返回COM HRESULT錯(cuò)誤代碼,比如DllRegisterServer。第三種是一個(gè)即沒有參數(shù)也沒有返回值的函數(shù)。我們可以按照自己的需要來加入函數(shù)聲明。保存經(jīng)過我們修改的FuncDecl.odl文件,并且執(zhí)行mktylib FuncDecl.odl,然后再將FuncDecl.tlb的引用加入我們的工程。(見Listing2里的ODL)

    我們能夠看到,通過調(diào)用下面的一對(duì)函數(shù),我們的確是可以實(shí)時(shí)調(diào)用函數(shù)指針了,而很長(zhǎng)時(shí)間以來,對(duì)VB程序員來說,想使用這對(duì)函數(shù)是不可能的,這對(duì)函數(shù)就是DllRegisterServer和DllUnregisterServer。通過訪問這兩個(gè)標(biāo)準(zhǔn)的ActiveX DLL和OCX入口函數(shù),可以讓我們的EXE按照自已的需要來定位和注冊(cè)自己的組件(譯者:這個(gè)技術(shù)還是有相當(dāng)價(jià)值的。雖然能夠通過Shell語句調(diào)用RegSvr32.exe來注冊(cè)組件,但是它僅支持標(biāo)準(zhǔn)的入口:DllRegisterServer和DllUnregisterServer。而使用這里的技術(shù),我們就能夠調(diào)用非標(biāo)準(zhǔn)的入口,在ATL工程里將兩個(gè)兩個(gè)輸出函數(shù)換個(gè)名字,我們?cè)赩B里依然可以注冊(cè),這樣簡(jiǎn)單的操作就能起到一定的保護(hù)組件的作用)。對(duì)這樣的外部函數(shù)來說,我們是通過LoadLibrary和GetProcAddress調(diào)用來從外部DLL獲取函數(shù)指針,并將這個(gè)函數(shù)指針移到FunctionDelegator結(jié)構(gòu)里以使我們能夠調(diào)用這個(gè)函數(shù)指針本身。(見Listing3)

使用函數(shù)指針來排序
    (譯者:這里原文用了幾段來演示如何通過函數(shù)指針回調(diào)的方法來進(jìn)行數(shù)組排序。僅就本文要談的函數(shù)指針調(diào)用來說,這和Listing3里的處理方式類似,因?yàn)榇颂幨÷赃@幾段。)

    我們能夠在很多方面使用這種調(diào)用函數(shù)指針技術(shù)。比如,我們可以通過在運(yùn)行時(shí)插入具有不同行為的函數(shù)來動(dòng)態(tài)改變某段代碼的行為。我們也可以通過這種技術(shù)在VB里實(shí)現(xiàn)type casting(強(qiáng)制類型轉(zhuǎn)換)(譯者:通過VarPtr得到一個(gè)變量的無類型指針,然后將這個(gè)指針做為參數(shù),將這個(gè)指針傳給不同的類型轉(zhuǎn)換函數(shù)指針,并調(diào)用之,即可實(shí)現(xiàn)強(qiáng)制類型轉(zhuǎn)換)。我不可能把所有可能的應(yīng)用都列出來,但是這里我再來演示一段小程序。

    我們經(jīng)常想在調(diào)試已編譯的VB組件時(shí),能在捕獲一個(gè)錯(cuò)誤的同時(shí)跳到調(diào)試器內(nèi)。標(biāo)準(zhǔn)的方法就是運(yùn)行Int3命令,這時(shí)會(huì)出現(xiàn)一個(gè)系統(tǒng)異常對(duì)話框來讓我們選擇是起動(dòng)調(diào)試器還是直接結(jié)束崩潰的程序。我們需要運(yùn)行的函數(shù)有兩條匯編指令:break(Int3)和return(ret)。相應(yīng)的ASM指令為CC和C3。用下面來代碼來實(shí)現(xiàn)一個(gè)這樣的FunctionDelegator:
    Dim FDVoid As FunctionDelegator
    Dim CallVoid As ICallVoid
    Dim Int3Ret As Integer
    Int3Ret = &HC3CC
    Set CallVoid = InitDelegator( _
      FDVoid, VarPtr(Int3Ret))
    '中斷并進(jìn)入調(diào)試器
    CallVoid.Void

    在VB里的In-line assembly(線內(nèi)匯編)代碼給VB的表達(dá)能力提供了無限的可能性(譯者:實(shí)際上這和C里的線內(nèi)匯編有很大不同,我們只能插入機(jī)器代碼,我覺得此處稱為In-Line Machine Code線內(nèi)機(jī)器代碼更合適)。 我們這里演示的函數(shù)實(shí)際上和DebugBreak這個(gè)API的功能是一樣的(譯者:僅就這個(gè)函數(shù)的功能來說還不如直接用DebugBreak),但是實(shí)現(xiàn)別的功能就不是這么簡(jiǎn)單了。如果我們需要更多的字節(jié),可以用一個(gè)Long或Currency數(shù)組來填字節(jié)流,并用VarPtr取得指向數(shù)組第0個(gè)元素的指針來作為函數(shù)指針。
(全文完)





Listing 1 這段代碼將一個(gè)FunctionDelegator轉(zhuǎn)換成一個(gè)支持特定函數(shù)指針的COM對(duì)象。這是一個(gè)特殊的COM對(duì)象,因?yàn)樗灰笕魏蝺?nèi)存分配并且對(duì)我們的接口請(qǐng)求總是盲目合作。請(qǐng)求僅有的正確接口是我們的責(zé)任。

'The magic number
Private Const cDelegateASM _
   As Currency = -368956918007638.6215@

'到處到用的輔助函數(shù)
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

'函數(shù)指針代理的結(jié)構(gòu)聲明
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結(jié)構(gòu),并將指向它的指針
'    作為一個(gè)COM對(duì)象返回.
Public Function InitDelegator( _
   Delegator As FunctionDelegator, _
   Optional ByVal pfn As Long) As IUnknown
   '第一次訪問時(shí)初始化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
   '對(duì)第一次請(qǐng)求總是盲目合作
   pvObj = VarPtr(This)
   '交換成失敗時(shí)vTable,僅在調(diào)用函數(shù)指針會(huì)返回HRESULT錯(cuò)誤代碼
   '    時(shí)才需要這么做,當(dāng)然這么做總是更安全。
   This.pVTable = m_pVTableFailQI
End Function

Private Function AddRefRelease( _
   ByVal This As Long) As Long
   '什么都不做,無需要引用計(jì)數(shù)。
End Function

'失敗QI
Private Function QueryInterfaceFail( _
   ByVal This As Long, _
   riid As Long, pvObj As Long) As Long
   '對(duì)任何請(qǐng)求都說:"不"
   pvObj = 0
   QueryInterfaceFail = &H80004002 'E_NOINTERFACE
End Function

'返回函數(shù)指針的輔助函數(shù)
Private Function FuncAddr (ByVal pfn As Long) As Long
   FuncAddr = pfn
End Function

譯者:上面的代碼在原文已經(jīng)發(fā)表后經(jīng)過了修改,因此原文沒有提到為什么上面的代碼需要兩個(gè)不同的vTable。Matt在更新的示例代碼的Readme文件里解釋這個(gè)原因。我下面將這個(gè)原因簡(jiǎn)單的敘述如下:
    這是因?yàn)楫?dāng)調(diào)用的函數(shù)指針需要返回HRESULT錯(cuò)誤代碼時(shí),VB會(huì)用再次調(diào)用QI來向?qū)ο笳?qǐng)求一個(gè)ISupportErrorInfo接口的引用。但是,由于原來代碼里的QI完全采用盲目合作的信任方式,它總是返回對(duì)象自身的接口指針,哪怕它并不支持所要求的接口。由于返回的接口引用并不支持ISupportErrorInfo,所以當(dāng)VB試圖用ISupportErrorInfo的方法來搜集錯(cuò)誤信息時(shí)程序就會(huì)崩潰。解決的辦法,就是提供兩個(gè)vTable。當(dāng)?shù)谝淮握{(diào)用初始化后的vTable里的QI時(shí),它采取信任方式返回接口指針,并在返回之前將包含失敗QI的vTable交換進(jìn)來。這樣下一次訪問的QI將是失敗QI,而失敗QI拒絕所有接口請(qǐng)求,這樣就有效的阻塞了后繼的QI請(qǐng)求,包括VB對(duì)ISupportErrorInfo的請(qǐng)求。在后面的Listing3的代碼中我們可以看到,一旦我們?cè)黾右镁蜁?huì)有類型不匹配錯(cuò)誤。
    還有VB在對(duì)Err對(duì)象的處理上有BUG,那就是當(dāng)VB用QI向某個(gè)對(duì)象請(qǐng)求ISupportErrorInfo接口失敗后,Err對(duì)象內(nèi)總是保留著對(duì)這個(gè)對(duì)象的引用。由于我們的vTalbe會(huì)先于Err對(duì)象釋放,所以Err對(duì)象里有一個(gè)掛起的引用,當(dāng)釋放Err對(duì)象時(shí)程序會(huì)崩潰。解決的方法是:在程序結(jié)束前自己用Err.Raise來引發(fā)一個(gè)新錯(cuò)誤。具體做法,見源代碼。

    
Listing 2 用來告訴VB編譯怎樣調(diào)用我們的函數(shù)指針的外部ODL文件。沒有對(duì)這個(gè)接口的描述,我們雖仍能生成代理到正確函數(shù)指針的COM對(duì)象,但卻沒有辦法來調(diào)用vTable里的函數(shù)。

[
   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 為了實(shí)現(xiàn)標(biāo)準(zhǔn)的ActiveX DLL和OCX的注冊(cè),我們需要將DLL裝入內(nèi)存,找到用來注冊(cè)的入口函數(shù)指針,然后再調(diào)用這個(gè)指針。通過使用FunctionDelegator對(duì)象,我們能對(duì)任意的DLL進(jìn)行同樣的操作。

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

   '找到函數(shù)指針
   pfn = GetProcAddress(hMod, EntryPoint)
   If pfn = 0 Then Err.Raise 5

   '初始化并得到代理COM對(duì)象的引用。
   Set pCall = InitDelegator(Delegator, pfn)

   '調(diào)用函數(shù)指針
   pCall.Call

''*****譯者:取消注釋下面部分可以來體驗(yàn)文中所說錯(cuò)誤和崩潰
'   Set pCall = Nothing
'   Dim pIUn As IUnknown, pShape2 As Shape
'   Set pIUn = InitDelegator(Delegator, pfn)
'   Dim pCallVoid As ICallVoid
'   Set pCallVoid = pIUn
'
''類型不匹配錯(cuò)誤,因?yàn)榇藭r(shí)QI已經(jīng)被換成了失敗QI。
'   'Set pShape2 = pIUn
'   Set pIUn = Nothing
'   Set pCallVoid = Nothing
'
''崩潰,因?yàn)榻涌诙x和函數(shù)指針不符
'   '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