1) COM組件有三個(gè)最基本的接口類(lèi),分別是IUnknown、IClassFactory、IDispatch。
COM規范規定任何組件、任何接口都必須從IUnknown繼承,IUnknown包含三個(gè)函數,分別是 QueryInterface、AddRef、Release。這三個(gè)函數是無(wú)比重要的,而且它們的排列順序也是不可改變的。QueryInterface用于查詢(xún)組件實(shí)現的其它接口,說(shuō)白了也就是看看這個(gè)組件的父類(lèi)中還有哪些接口類(lèi),AddRef用于增加引用計數,Release用于減少引用計數。引用計數也是COM中的一個(gè)非常重要的概念。大體上簡(jiǎn)單的說(shuō)來(lái)可以這么理解,COM組件是個(gè)DLL,當客戶(hù)程序要用它時(shí)就要把它裝到內存里。另一方面,一個(gè)組件也不是只給你一個(gè)人用的,可能會(huì )有很多個(gè)程序同時(shí)都要用到它。但實(shí)際上DLL只裝載了一次,即內存中只有一個(gè)COM組件,那COM組件由誰(shuí)來(lái)釋放?由客戶(hù)程序嗎?不可能,因為如果你釋放了組件,那別人怎么用,所以只能由COM組件自己來(lái)負責。所以出現了引用計數的概念,COM維持一個(gè)計數,記錄當前有多少人在用它,每多一次調用計數就加一,少一個(gè)客戶(hù)用它就減一,當最后一個(gè)客戶(hù)釋放它的時(shí)侯,COM知道已經(jīng)沒(méi)有人用它了,它的使用已經(jīng)結束了,那它就把它自己給釋放了。引用計數是COM編程里非常容易出錯的一個(gè)地方,但所幸VC的各種各樣的類(lèi)庫里已經(jīng)基本上把AddRef的調用給隱含了,在我的印象里,我編程的時(shí)侯還從來(lái)沒(méi)有調用過(guò)AddRef,我們只需在適當的時(shí)侯調用Release。至少有兩個(gè)時(shí)侯要記住調用Release,第一個(gè)是調用了 QueryInterface以后,第二個(gè)是調用了任何得到一個(gè)接口的指針的函數以后,記住多查MSDN 以確定某個(gè)函數內部是否調用了AddRef,如果是的話(huà)那調用Release的責任就要歸你了。 IUnknown的這三個(gè)函數的實(shí)現非常規范但也非常煩瑣,容易出錯,所幸的事我們可能永遠也不需要自己來(lái)實(shí)現它們。
IClassFactory的作用是創(chuàng )建COM組件。我們已經(jīng)知道COM組件實(shí)際上就是一個(gè)類(lèi),那我們平常是怎么實(shí)例化一個(gè)類(lèi)對象的?是用‘new’命令!很簡(jiǎn)單吧,COM組件也一樣如此。但是誰(shuí)來(lái)new它呢?不可能是客戶(hù)程序,因為客戶(hù)程序不可能知道組件的類(lèi)名字,如果客戶(hù)知道組件的類(lèi)名字那組件的可重用性就要打個(gè)大大的折扣了,事實(shí)上客戶(hù)程序只不過(guò)知道一個(gè)代表著(zhù)組件的128位的數字串而已,這個(gè)等會(huì )再介紹。所以客戶(hù)無(wú)法自己創(chuàng )建組件,而且考慮一下,如果組件是在遠程的機器上,你還能new出一個(gè)對象嗎?所以創(chuàng )建組件的責任交給了一個(gè)單獨的對象,這個(gè)對象就是類(lèi)廠(chǎng)。每個(gè)組件都必須有一個(gè)與之相關(guān)的類(lèi)廠(chǎng),這個(gè)類(lèi)廠(chǎng)知道怎么樣創(chuàng )建組件,當客戶(hù)請求一個(gè)組件對象的實(shí)例時(shí),實(shí)際上這個(gè)請求交給了類(lèi)廠(chǎng),由類(lèi)廠(chǎng)創(chuàng )建組件實(shí)例,然后把實(shí)例指針交給客戶(hù)程序。這個(gè)過(guò)程在跨進(jìn)程及遠程創(chuàng )建組件時(shí)特別有用,因為這時(shí)就不是一個(gè)簡(jiǎn)單的new操作就可以的了,它必須要經(jīng)過(guò)調度,而這些復雜的操作都交給類(lèi)廠(chǎng)對象去做了。IClassFactory最重要的一個(gè)函數就是CreateInstance,顧名思議就是創(chuàng )建組件實(shí)例,一般情況下我們不會(huì )直接調用它,API函數都為我們封裝好它了,只有某些特殊情況下才會(huì )由我們自己來(lái)調用它,這也是VC編寫(xiě)COM組件的好處,使我們有了更多的控制機會(huì ),而VB給我們這樣的機會(huì )則是太少太少了。
IDispatch叫做調度接口。它的作用何在呢?這個(gè)世上除了C++還有很多別的語(yǔ)言,比如VB、 VJ、VBScript、JavaScript等等??梢赃@么說(shuō),如果這世上沒(méi)有這么多亂七八糟的語(yǔ)言,那就不會(huì )有IDispatch。:-) 我們知道COM組件是C++類(lèi),是靠虛函數表來(lái)調用函數的,對于VC來(lái)說(shuō)毫無(wú)問(wèn)題,這本來(lái)就是針對C++而設計的,以前VB不行,現在VB也可以用指針了,也可以通過(guò)VTable來(lái)調用函數了,VJ也可以,但還是有些語(yǔ)言不行,那就是腳本語(yǔ)言,典型的如 VBScript、JavaScript。不行的原因在于它們并不支持指針,連指針都不能用還怎么用多態(tài)性啊,還怎么調這些虛函數啊。唉,沒(méi)辦法,也不能置這些腳本語(yǔ)言于不顧吧,現在網(wǎng)頁(yè)上用的都是這些腳本語(yǔ)言,而分布式應用也是COM組件的一個(gè)主要市場(chǎng),它不得不被這些腳本語(yǔ)言所調用,既然虛函數表的方式行不通,我們只能另尋他法了。時(shí)勢造英雄,IDispatch應運而生。:-) 調度接口把每一個(gè)函數每一個(gè)屬性都編上號,客戶(hù)程序要調用這些函數屬性的時(shí)侯就把這些編號傳給IDispatch接口就行了,IDispatch再根據這些編號調用相應的函數,僅此而已。當然實(shí)際的過(guò)程遠比這復雜,僅給一個(gè)編號就能讓別人知道怎么調用一個(gè)函數那不是天方夜潭嗎,你總得讓別人知道你要調用的函數要帶什么參數,參數類(lèi)型什么以及返回什么東西吧,而要以一種統一的方式來(lái)處理這些問(wèn)題是件很頭疼的事。IDispatch接口的主要函數是Invoke,客戶(hù)程序都調用它,然后Invoke再調用相應的函數,如果看一看MS的類(lèi)庫里實(shí)現 Invoke的代碼就會(huì )驚嘆它實(shí)現的復雜了,因為你必須考慮各種參數類(lèi)型的情況,所幸我們不需要自己來(lái)做這件事,而且可能永遠也沒(méi)這樣的機會(huì )。:-)
(2) dispinterface接口、Dual接口以及Custom接口
這一小節放在這里似乎不太合適,因為這是在A(yíng)TL編程時(shí)用到的術(shù)語(yǔ)。我在這里主要是想談一下自動(dòng)化接口的好處及缺點(diǎn),用這三個(gè)術(shù)語(yǔ)來(lái)解釋可能會(huì )更好一些,而且以后遲早會(huì )遇上它們,我將以一種通俗的方式來(lái)解釋它們,可能并非那么精確,就好象用偽代碼來(lái)描述算法一樣。-:)
所謂的自動(dòng)化接口就是用IDispatch實(shí)現的接口。我們已經(jīng)講解過(guò)IDispatch的作用了,它的好處就是腳本語(yǔ)言象VBScript、 JavaScript也能用COM組件了,從而基本上做到了與語(yǔ)言無(wú)關(guān)它的缺點(diǎn)主要有兩個(gè),第一個(gè)就是速度慢效率低。這是顯而易見(jiàn)的,通過(guò)虛函數表一下子就可以調用函數了,而通過(guò)Invoke則等于中間轉了道手續,尤其是需要把函數參數轉換成一種規范的格式才去調用函數,耽誤了很多時(shí)間。所以一般若非是迫不得已我們都想用VTable的方式調用函數以獲得高效率。第二個(gè)缺點(diǎn)就是只能使用規定好的所謂的自動(dòng)化數據類(lèi)型。如果不用IDispatch我們可以想用什么數據類(lèi)型就用什么類(lèi)型,VC會(huì )自動(dòng)給我們生成相應的調度代碼。而用自動(dòng)化接口就不行了,因為Invoke的實(shí)現代碼是VC事先寫(xiě)好的,而它不能事先預料到我們要用到的所有類(lèi)型,它只能根據一些常用的數據類(lèi)型來(lái)寫(xiě)它的處理代碼,而且它也要考慮不同語(yǔ)言之間的數據類(lèi)型轉換問(wèn)題。所以VC自動(dòng)化接口生成的調度代碼只適用于它所規定好的那些數據類(lèi)型,當然這些數據類(lèi)型已經(jīng)足夠豐富了,但不能滿(mǎn)足自定義數據結構的要求。你也可以自己寫(xiě)調度代碼來(lái)處理你的自定義數據結構,但這并不是一件容易的事??紤]到IDispatch的種種缺點(diǎn)(它還有一個(gè)缺點(diǎn),就是使用麻煩,:-) )現在一般都推薦寫(xiě)雙接口組件,稱(chēng)為dual接口,實(shí)際上就是從IDispatch繼承的接口。我們知道任何接口都必須從 IUnknown繼承,IDispatch接口也不例外。那從IDispatch繼承的接口實(shí)際上就等于有兩個(gè)基類(lèi),一個(gè)是IUnknown,一個(gè)是IDispatch,所以它可以以?xún)煞N方式來(lái)調用組件,可以通過(guò) IUnknown用虛函數表的方式調用接口方法,也可以通過(guò)IDispatch::Invoke自動(dòng)化調度來(lái)調用。這就有了很大的靈活性,這個(gè)組件既可以用于C++的環(huán)境也可以用于腳本語(yǔ)言中,同時(shí)滿(mǎn)足了各方面的需要。
相對比的,dispinterface是一種純粹的自動(dòng)化接口,可以簡(jiǎn)單的就把它看作是IDispatch接口 (雖然它實(shí)際上不是的),這種接口就只能通過(guò)自動(dòng)化的方式來(lái)調用,COM組件的事件一般都用的是這種形式的接口。
Custom接口就是從IUnknown接口派生的類(lèi),顯然它就只能用虛函數表的方式來(lái)調用接口了
(3) COM組件有三種,進(jìn)程內、本地、遠程。對于后兩者情況必須調度接口指針及函數參數。
COM是一個(gè)DLL,它有三種運行模式。它可以是進(jìn)程內的,即和調用者在同一個(gè)進(jìn)程內,也可以和調用者在同一個(gè)機器上但在不同的進(jìn)程內,還可以根本就和調用者在兩臺機器上。這里有一個(gè)根本點(diǎn)需要牢記,就是COM組件它只是一個(gè)DLL,它自己是運行不起來(lái)的,必須有一個(gè)進(jìn)程象父親般照顧它才行,即COM組件必須在一個(gè)進(jìn)程內.那誰(shuí)充當看護人的責任呢?先說(shuō)說(shuō)調度的問(wèn)題。調度是個(gè)復雜的問(wèn)題,以我的知識還講不清楚這個(gè)問(wèn)題,我只是一般性的談?wù)剮讉€(gè)最基本的概念。我們知道對于WIN32程序,每個(gè)進(jìn)程都擁有4GB的虛擬地址空間,每個(gè)進(jìn)程都有其各自的編址,同一個(gè)數據塊在不同的進(jìn)程里的編址很可能就是不一樣的,所以存在著(zhù)進(jìn)程間的地址轉換問(wèn)題。這就是調度問(wèn)題。對于本地和遠程進(jìn)程來(lái)說(shuō),DLL 和客戶(hù)程序在不同的編址空間,所以要傳遞接口指針到客戶(hù)程序必須要經(jīng)過(guò)調度。Windows 已經(jīng)提供了現成的調度函數,就不需要我們自己來(lái)做這個(gè)復雜的事情了。對遠程組件來(lái)說(shuō)函數的參數傳遞是另外一種調度。DCOM是以RPC為基礎的,要在網(wǎng)絡(luò )間傳遞數據必須遵守標準的網(wǎng)上數據傳輸協(xié)議,數據傳遞前要先打包,傳遞到目的地后要解包,這個(gè)過(guò)程就是調度,這個(gè)過(guò)程很復雜,不過(guò)Windows已經(jīng)把一切都給我們做好了,一般情況下我們不需要自己來(lái)編寫(xiě)調度DLL。
我們剛說(shuō)過(guò)一個(gè)COM組件必須在一個(gè)進(jìn)程內。對于本地模式的組件一般是以EXE的形式出現,所以它本身就已經(jīng)是一個(gè)進(jìn)程。對于遠程DLL,我們必須找一個(gè)進(jìn)程,這個(gè)進(jìn)程必須包含了調度代碼以實(shí)現基本的調度。這個(gè)進(jìn)程就是dllhost.exe。這是COM默認的DLL代理。實(shí)際上在分布式應用中,我們應該用MTS來(lái)作為DLL代理,因為MTS有著(zhù)很強大的功能,是專(zhuān)門(mén)的用于管理分布式DLL組件的工具。
調度離我們很近又似乎很遠,我們編程時(shí)很少關(guān)注到它,這也是COM的一個(gè)優(yōu)點(diǎn)之一,既平臺無(wú)關(guān)性,無(wú)論你是遠程的、本地的還是進(jìn)程內的,編程是一樣的,一切細節都由COM自己處理好了,所以我們也不用深究這個(gè)問(wèn)題,只要有個(gè)概念就可以了,當然如果你對調度有自己特殊的要求就需要深入了解調度的整個(gè)過(guò)程了,這里推薦一本《COM+技術(shù)內幕》,這絕對是一本講調度的好書(shū)。
(4) COM組件的核心是IDL。
我們希望軟件是一塊塊拼裝出來(lái)的,但不可能是沒(méi)有規定的胡亂拼接,總是要遵守一定的標準,各個(gè)模塊之間如何才能親密無(wú)間的合作,必須要事先共同制訂好它們之間交互的規范,這個(gè)規范就是接口。我們知道接口實(shí)際上都是純虛類(lèi),它里面定義好了很多的純虛函數,等著(zhù)某個(gè)組件去實(shí)現它,這個(gè)接口就是兩個(gè)完全不相關(guān)的模塊能夠組合在一起的關(guān)鍵試想一下如果我們是一個(gè)應用軟件廠(chǎng)商,我們的軟件中需要用到某個(gè)模塊,我們沒(méi)有時(shí)間自己開(kāi)發(fā),所以我們想到市場(chǎng)上找一找看有沒(méi)有這樣的模塊,我們怎么去找呢?也許我們需要的這個(gè)模塊在業(yè)界已經(jīng)有了標準,已經(jīng)有人制訂好了標準的接口,有很多組件工具廠(chǎng)商已經(jīng)在自己的組件中實(shí)現了這個(gè)接口,那我們尋找的目標就是這些已經(jīng)實(shí)現了接口的組件,我們不關(guān)心組件從哪來(lái),它有什么其它的功能,我們只關(guān)心它是否很好的實(shí)現了我們制訂好的接口。這種接口可能是業(yè)界的標準,也可能只是你和幾個(gè)廠(chǎng)商之間內部制訂的協(xié)議,但總之它是一個(gè)標準,是你的軟件和別人的模塊能夠組合在一起的基礎,是COM組件通信的標準。
COM具有語(yǔ)言無(wú)關(guān)性,它可以用任何語(yǔ)言編寫(xiě),也可以在任何語(yǔ)言平臺上被調用。但至今為止我們一直是以C++的環(huán)境中談COM,那它的語(yǔ)言無(wú)關(guān)性是怎么體現出來(lái)的呢?或者換句話(huà)說(shuō),我們怎樣才能以語(yǔ)言無(wú)關(guān)的方式來(lái)定義接口呢?前面我們是直接用純虛類(lèi)的方式定義的,但顯然是不行的,除了C++誰(shuí)還認它呢?正是出于這種考慮,微軟決定采用IDL來(lái)定義接口。說(shuō)白了,IDL實(shí)際上就是一種大家都認識的語(yǔ)言,用它來(lái)定義接口,不論放到哪個(gè)語(yǔ)言平臺上都認識它。我們可以想象一下理想的標準的組件模式,我們總是從IDL開(kāi)始,先用IDL制訂好各個(gè)接口,然后把實(shí)現接口的任務(wù)分配不同的人,有的人可能善長(cháng)用VC,有的人可能善長(cháng)用VB,這沒(méi)關(guān)系,作為項目負責人我不關(guān)心這些,我只關(guān)心你把最終的DLL 拿給我。這是一種多么好的開(kāi)發(fā)模式,可以用任何語(yǔ)言來(lái)開(kāi)發(fā),也可以用任何語(yǔ)言來(lái)欣賞你的開(kāi)發(fā)成果。
(5) COM組件的運行機制,即COM是怎么跑起來(lái)的。
這部分我們將構造一個(gè)創(chuàng )建COM組件的最小框架結構,然后看一看其內部處理流程是怎樣的
IUnknown *pUnk=NULL;
IObject *pObject=NULL;
CoInitialize(NULL);
CoCreateInstance(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IUnknown, (void**)&pUnk);
pUnk->QueryInterface(IID_IOjbect, (void**)&pObject);
pUnk->Release();
pObject->Func();
pObject->Release();
CoUninitialize();
這就是一個(gè)典型的創(chuàng )建COM組件的框架,不過(guò)我的興趣在CoCreateInstance身上,讓我們來(lái)看看它內部做了一些什么事情。以下是它內部實(shí)現的一個(gè)偽代碼:
CoCreateInstance(....)
{
.......
IClassFactory *pClassFactory=NULL;
CoGetClassObject(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void **)&pClassFactory);
pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnk);
pClassFactory->Release();
........
}
這段話(huà)的意思就是先得到類(lèi)廠(chǎng)對象,再通過(guò)類(lèi)廠(chǎng)創(chuàng )建組件從而得到IUnknown指針。繼續深入一步,看看CoGetClassObject的內部偽碼:
CoGetClassObject(.....)
{
//通過(guò)查注冊表CLSID_Object,得知組件DLL的位置、文件名
//裝入DLL庫
//使用函數GetProcAddress(...)得到DLL庫中函數DllGetClassObject的函數指針。
//調用DllGetClassObject
}
DllGetClassObject是干什么的,它是用來(lái)獲得類(lèi)廠(chǎng)對象的。只有先得到類(lèi)廠(chǎng)才能去創(chuàng )建組件.
下面是DllGetClassObject的偽碼:
DllGetClassObject(...)
{
......
CFactory* pFactory= new CFactory; //類(lèi)廠(chǎng)對象
pFactory->QueryInterface(IID_IClassFactory, (void**)&pClassFactory);
//查詢(xún)IClassFactory指針
pFactory->Release();
......
}
CoGetClassObject的流程已經(jīng)到此為止,現在返回CoCreateInstance,看看CreateInstance的偽碼:
CFactory::CreateInstance(.....)
{
...........
CObject *pObject = new CObject; //組件對象
pObject->QueryInterface(IID_IUnknown, (void**)&pUnk);
pObject->Release();
...........
}
(6) 一個(gè)典型的自注冊的COM DLL所必有的四個(gè)函數
DllGetClassObject:用于獲得類(lèi)廠(chǎng)指針
DllRegisterServer:注冊一些必要的信息到注冊表中
DllUnregisterServer:卸載注冊信息
DllCanUnloadNow:系統空閑時(shí)會(huì )調用這個(gè)函數,以確定是否可以卸載DLL
DLL還有一個(gè)函數是DllMain,這個(gè)函數在COM中并不要求一定要實(shí)現它,但是在VC生成的組件中自動(dòng)都包含了它,它的作用主要是得到一個(gè)全局的實(shí)例對象。
(7) 注冊表在COM中的重要作用
首先要知道GUID的概念,COM中所有的類(lèi)、接口、類(lèi)型庫都用GUID來(lái)唯一標識,GUID是一個(gè)128位的字串,根據特制算法生成的GUID可以保證是全世界唯一的。 COM組件的創(chuàng )建,查詢(xún)接口都是通過(guò)注冊表進(jìn)行的。有了注冊表,應用程序就不需要知道組件的DLL文件名、位置,只需要根據CLSID查就可以了。當版本升級的時(shí)侯,只要改一下注冊表信息就可以神不知鬼不覺(jué)的轉到新版本的DLL。
聯(lián)系客服