一、為什么要用COM
軟件工程發(fā)展到今天,從一開(kāi)始的結(jié)構(gòu)化編程,到面向?qū)ο缶幊蹋俚浆F(xiàn)在的COM編程,目標(biāo)只有一個(gè),就是希望軟件能象積方塊一樣是累起來(lái)的,是組裝起來(lái)的,而不是一點(diǎn)點(diǎn)編出來(lái)的。結(jié)構(gòu)化編程是函數(shù)塊的形式,通過(guò)把一個(gè)軟件劃分成許多模塊,每個(gè)模塊完成各自不同的功能,盡量做到高內(nèi)聚低藕合,這已經(jīng)是一個(gè)很好的開(kāi)始,我們可以把不同的模塊分給不同的人去做,然后合到一塊,這已經(jīng)有了組裝的概念了。軟件工程的核心就是要模塊化,最理想的情況就是100%內(nèi)聚0%藕合。整個(gè)軟件的發(fā)展也都是朝著這個(gè)方向走的。結(jié)構(gòu)化編程方式只是一個(gè)開(kāi)始。下一步就出現(xiàn)了面向?qū)ο缶幊蹋鄬?duì)于面向功能的結(jié)構(gòu)化方式是一個(gè)巨大的進(jìn)步。我們知道整個(gè)自然界都是由各種各樣不同的事物組成的,事物之間存在著復(fù)雜的千絲萬(wàn)縷的關(guān)系,而正是靠著事物之間的聯(lián)系、交互作用,我們的世界才是有生命力的才是活動(dòng)的。我們可以認(rèn)為在自然界中事物做為一個(gè)概念,它是穩(wěn)定的不變的,而事物之間的聯(lián)系是多變的、運(yùn)動(dòng)的。事物應(yīng)該是這個(gè)世界的本質(zhì)所在。面向?qū)ο蟮闹埸c(diǎn)就是事物,就是這種穩(wěn)定的概念。每個(gè)事物都有其固有的屬性,都有其固有的行為,這些都是事物本身所固有的東西,而面向?qū)ο蟮姆椒ň褪敲枋龀鲞@種穩(wěn)定的東西。而面向功能的模塊化方法它的著眼點(diǎn)是事物之間的聯(lián)系,它眼中看不到事物的概念它只注重功能,我們平常在劃分模塊的時(shí)侯有沒(méi)有想過(guò)這個(gè)函數(shù)與哪些對(duì)象有關(guān)呢?很少有人這么想,一個(gè)函數(shù)它實(shí)現(xiàn)一種功能,這個(gè)功能必定與某些事物想聯(lián)系,我們沒(méi)有去掌握事物本身而只考慮事物之間是怎么相互作用而完成一個(gè)功能的。說(shuō)白了,這叫本末倒置,也叫急功近利,因?yàn)椴皇俏覀冎腔鄄粔颍皇且驗(yàn)槲覀儧](méi)有多想一步。面向功能的結(jié)構(gòu)化方法因?yàn)樗⒁獾闹皇鞘挛镏g的聯(lián)系,而聯(lián)系是多變的,事物本身可能不會(huì)發(fā)生大的變化,而聯(lián)系則是很有可能發(fā)生改變的,聯(lián)系一變,那就是另一個(gè)世界了,那就是另一種功能了。如果我們用面向?qū)ο蟮姆椒ǎ覀兙涂梢砸圆蛔儜?yīng)萬(wàn)變,只要事先把事物用類描述好,我們要改變的只是把這些類聯(lián)系起來(lái)的方法,只是重新使用我們的類庫(kù),而面向過(guò)程的方法因?yàn)樗鼧?gòu)造的是一個(gè)不穩(wěn)定的世界,所以一點(diǎn)小小的變化也可能導(dǎo)致整個(gè)系統(tǒng)都要改變。然而面向?qū)ο蠓椒ㄈ匀挥袉?wèn)題,問(wèn)題在于重用的方法。搭積木式的軟件構(gòu)造方法的基礎(chǔ)是有許許多多各種各樣的可重用的部件、模塊。我們首先想到的是類庫(kù),因?yàn)槲覀冇妹嫦驅(qū)ο蟮姆椒óa(chǎn)生的直接結(jié)果就是許多的類。但類庫(kù)的重用是基于源碼的方式,這是它的重大缺陷。首先它限制了編程語(yǔ)言,你的類庫(kù)總是用一種語(yǔ)言寫的吧,那你就不能拿到別的語(yǔ)言里用了。其次你每次都必須重新編譯,只有編譯了才能與你自己的代碼結(jié)合在一起生成可執(zhí)行文件。在開(kāi)發(fā)時(shí)這倒沒(méi)什么,關(guān)鍵在于開(kāi)發(fā)完成后,你的EXE都已經(jīng)生成好了,如果這時(shí)侯你的類庫(kù)提供廠商告訴你他們又做好了一個(gè)新的類庫(kù),功能更強(qiáng)大速度更快,而你為之心動(dòng)又想把這新版的類庫(kù)用到你自己的程序中,那你就必須重新編譯、重新調(diào)試!這離我們理想的積木式軟件構(gòu)造方法還有一定差距,在我們的設(shè)想里希望把一個(gè)模塊拿出來(lái)再換一個(gè)新的模塊是非常方便的事,可是現(xiàn)在不但要重新編譯,還要冒著很大的風(fēng)險(xiǎn),因?yàn)槟憧赡芤匦赂淖兡阕约旱拇a。另一種重用方式很自然地就想到了是DLL的方式。Windows里到處是DLL,它是Windows 的基礎(chǔ),但DLL也有它自己的缺點(diǎn)。總結(jié)一下它至少有四點(diǎn)不足。(1)函數(shù)重名問(wèn)題。DLL里是一個(gè)一個(gè)的函數(shù),我們通過(guò)函數(shù)名來(lái)調(diào)用函數(shù),那如果兩個(gè)DLL里有重名的函數(shù)怎么辦?(2)各編譯器對(duì)C++函數(shù)的名稱修飾不兼容問(wèn)題。對(duì)于C++函數(shù),編譯器要根據(jù)函數(shù)的參數(shù)信息為它生成修飾名,DLL庫(kù)里存的就是這個(gè)修飾名,但是不同的編譯器產(chǎn)生修飾的方法不一樣,所以你在VC 里編寫的DLL在BC里就可以用不了。不過(guò)也可以用extern "C";來(lái)強(qiáng)調(diào)使用標(biāo)準(zhǔn)的C函數(shù)特性,關(guān)閉修飾功能,但這樣也喪失了C++的重載多態(tài)性功能。(3)路徑問(wèn)題。放在自己的目錄下面,別人的程序就找不到,放在系統(tǒng)目錄下,就可能有重名的問(wèn)題。而真正的組件應(yīng)該可以放在任何地方甚至可以不在本機(jī),用戶根本不需考慮這個(gè)問(wèn)題。(4)DLL與EXE的依賴問(wèn)題。我們一般都是用隱式連接的方式,就是編程的時(shí)侯指明用什么DLL,這種方式很簡(jiǎn)單,它在編譯時(shí)就把EXE與DLL綁在一起了。如果DLL發(fā)行了一個(gè)新版本,我們很有必要重新鏈接一次,因?yàn)?/span>DLL里面函數(shù)的地址可能已經(jīng)發(fā)生了改變。DLL的缺點(diǎn)就是COM的優(yōu)點(diǎn)。首先我們要先把握住一點(diǎn),COM和DLL一樣都是基于二進(jìn)制的代碼重用,所以它不存在類庫(kù)重用時(shí)的問(wèn)題。另一個(gè)關(guān)鍵點(diǎn)是,COM本身也是DLL,既使是ActiveX控件.ocx它實(shí)際上也是DLL,所以說(shuō)DLL在還是有重用上有很大的優(yōu)勢(shì),只不過(guò)我們通過(guò)制訂復(fù)雜的COM協(xié)議,通COM本身的機(jī)制改變了重用的方法,以一種新的方法來(lái)利用DLL,來(lái)克服DLL本身所固有的缺陷,從而實(shí)現(xiàn)更高一級(jí)的重用方法。COM沒(méi)有重名問(wèn)題,因?yàn)楦静皇峭ㄟ^(guò)函數(shù)名來(lái)調(diào)用函數(shù),而是通過(guò)虛函數(shù)表,自然也不會(huì)有函數(shù)名修飾的問(wèn)題。路徑問(wèn)題也不復(fù)存在,因?yàn)槭峭ㄟ^(guò)查注冊(cè)表來(lái)找組件的,放在什么地方都可以,即使在別的機(jī)器上也可以。也不用考慮和EXE的依賴關(guān)系了,它們二者之間是松散的結(jié)合在一起,可以輕松的換上組件的一個(gè)新版本,而應(yīng)用程序混然不覺(jué)
二、用VC進(jìn)行COM編程,必須要掌握哪些COM理論知識(shí)
我見(jiàn)過(guò)很多人學(xué)COM,看完一本書后覺(jué)得對(duì)COM的原理比較了解了,COM也不過(guò)如此,可是就是不知道該怎么編程序,我自己也有這種情況,我也是經(jīng)歷了這樣的階段走過(guò)來(lái)的。要學(xué)COM的基本原理,我推薦的書是《COM技術(shù)內(nèi)幕》。但僅看這樣的書是遠(yuǎn)遠(yuǎn)不夠的,我們最終的目的是要學(xué)會(huì)怎么用COM去編程序,而不是拼命的研究COM本身的機(jī)制。所以我個(gè)人覺(jué)得對(duì)COM的基本原理不需要花大量的時(shí)間去追根問(wèn)底,沒(méi)有必要,是吃力不討好的事。其實(shí)我們只需要掌握幾個(gè)關(guān)鍵概念就夠了。這里我列出了一些我自己認(rèn)為是用VC編程所必需掌握的幾個(gè)關(guān)鍵概念。(這里所說(shuō)的均是用C++語(yǔ)言條件下的COM編程方式)
(1) COM組件實(shí)際上是一個(gè)C++類,而接口都是純虛類。組件從接口派生而來(lái)。我們可以簡(jiǎn)單的用純粹的C++的語(yǔ)法形式來(lái)描述COM是個(gè)什么東西:
class IObject { public: virtual Function1(...) = 0; virtual Function2(...) = 0; .... }; class MyObject : public IObject { public: virtual Function1(...){...} virtual Function2(...){...} .... }; |
看清楚了嗎?IObject就是我們常說(shuō)的接口,MyObject就是所謂的COM組件。切記切記接口都是純虛類,它所包含的函數(shù)都是純虛函數(shù),而且它沒(méi)有成員變量。而COM組件就是從這些純虛類繼承下來(lái)的派生類,它實(shí)現(xiàn)了這些虛函數(shù),僅此而已。從上面也可以看出,COM組件是以 C++為基礎(chǔ)的,特別重要的是虛函數(shù)和多態(tài)性的概念,COM中所有函數(shù)都是虛函數(shù),都必須通過(guò)虛函數(shù)表VTable來(lái)調(diào)用,這一點(diǎn)是無(wú)比重要的,必需時(shí)刻牢記在心。為了讓大家確切了解一下虛函數(shù)表是什么樣子,從《COM+技術(shù)內(nèi)幕》中COPY了下面這個(gè)示例圖:
(2) COM組件有三個(gè)最基本的接口類,分別是IUnknown、IClassFactory、IDispatch。
COM規(guī)范規(guī)定任何組件、任何接口都必須從IUnknown繼承,IUnknown包含三個(gè)函數(shù),分別是 QueryInterface、AddRef、Release。這三個(gè)函數(shù)是無(wú)比重要的,而且它們的排列順序也是不可改變的。QueryInterface用于查詢組件實(shí)現(xiàn)的其它接口,說(shuō)白了也就是看看這個(gè)組件的父類中還有哪些接口類,AddRef用于增加引用計(jì)數(shù),Release用于減少引用計(jì)數(shù)。引用計(jì)數(shù)也是COM中的一個(gè)非常重要的概念。大體上簡(jiǎn)單的說(shuō)來(lái)可以這么理解,COM組件是個(gè)DLL,當(dāng)客戶程序要用它時(shí)就要把它裝到內(nèi)存里。另一方面,一個(gè)組件也不是只給你一個(gè)人用的,可能會(huì)有很多個(gè)程序同時(shí)都要用到它。但實(shí)際上DLL只裝載了一次,即內(nèi)存中只有一個(gè)COM組件,那COM組件由誰(shuí)來(lái)釋放?由客戶程序嗎?不可能,因?yàn)槿绻汜尫帕私M件,那別人怎么用,所以只能由COM組件自己來(lái)負(fù)責(zé)。所以出現(xiàn)了引用計(jì)數(shù)的概念,COM維持一個(gè)計(jì)數(shù),記錄當(dāng)前有多少人在用它,每多一次調(diào)用計(jì)數(shù)就加一,少一個(gè)客戶用它就減一,當(dāng)最后一個(gè)客戶釋放它的時(shí)侯,COM知道已經(jīng)沒(méi)有人用它了,它的使用已經(jīng)結(jié)束了,那它就把它自己給釋放了。引用計(jì)數(shù)是COM編程里非常容易出錯(cuò)的一個(gè)地方,但所幸VC的各種各樣的類庫(kù)里已經(jīng)基本上把AddRef的調(diào)用給隱含了,在我的印象里,我編程的時(shí)侯還從來(lái)沒(méi)有調(diào)用過(guò)AddRef,我們只需在適當(dāng)?shù)臅r(shí)侯調(diào)用Release。至少有兩個(gè)時(shí)侯要記住調(diào)用Release,第一個(gè)是調(diào)用了 QueryInterface以后,第二個(gè)是調(diào)用了任何得到一個(gè)接口的指針的函數(shù)以后,記住多查MSDN 以確定某個(gè)函數(shù)內(nèi)部是否調(diào)用了AddRef,如果是的話那調(diào)用Release的責(zé)任就要?dú)w你了。 IUnknown的這三個(gè)函數(shù)的實(shí)現(xiàn)非常規(guī)范但也非常煩瑣,容易出錯(cuò),所幸的事我們可能永遠(yuǎn)也不需要自己來(lái)實(shí)現(xiàn)它們。
IClassFactory的作用是創(chuàng)建COM組件。我們已經(jīng)知道COM組件實(shí)際上就是一個(gè)類,那我們平常是怎么實(shí)例化一個(gè)類對(duì)象的?是用‘new’命令!很簡(jiǎn)單吧,COM組件也一樣如此。但是誰(shuí)來(lái)new它呢?不可能是客戶程序,因?yàn)榭蛻舫绦虿豢赡苤澜M件的類名字,如果客戶知道組件的類名字那組件的可重用性就要打個(gè)大大的折扣了,事實(shí)上客戶程序只不過(guò)知道一個(gè)代表著組件的128位的數(shù)字串而已,這個(gè)等會(huì)再介紹。所以客戶無(wú)法自己創(chuàng)建組件,而且考慮一下,如果組件是在遠(yuǎn)程的機(jī)器上,你還能new出一個(gè)對(duì)象嗎?所以創(chuàng)建組件的責(zé)任交給了一個(gè)單獨(dú)的對(duì)象,這個(gè)對(duì)象就是類廠。每個(gè)組件都必須有一個(gè)與之相關(guān)的類廠,這個(gè)類廠知道怎么樣創(chuàng)建組件,當(dāng)客戶請(qǐng)求一個(gè)組件對(duì)象的實(shí)例時(shí),實(shí)際上這個(gè)請(qǐng)求交給了類廠,由類廠創(chuàng)建組件實(shí)例,然后把實(shí)例指針交給客戶程序。這個(gè)過(guò)程在跨進(jìn)程及遠(yuǎn)程創(chuàng)建組件時(shí)特別有用,因?yàn)檫@時(shí)就不是一個(gè)簡(jiǎn)單的new操作就可以的了,它必須要經(jīng)過(guò)調(diào)度,而這些復(fù)雜的操作都交給類廠對(duì)象去做了。IClassFactory最重要的一個(gè)函數(shù)就是CreateInstance,顧名思議就是創(chuàng)建組件實(shí)例,一般情況下我們不會(huì)直接調(diào)用它,API函數(shù)都為我們封裝好它了,只有某些特殊情況下才會(huì)由我們自己來(lái)調(diào)用它,這也是VC編寫COM組件的好處,使我們有了更多的控制機(jī)會(huì),而VB給我們這樣的機(jī)會(huì)則是太少太少了。
IDispatch叫做調(diào)度接口。它的作用何在呢?這個(gè)世上除了C++還有很多別的語(yǔ)言,比如VB、 VJ、VBScript、JavaScript等等。可以這么說(shuō),如果這世上沒(méi)有這么多亂七八糟的語(yǔ)言,那就不會(huì)有IDispatch。:-) 我們知道COM組件是C++類,是靠虛函數(shù)表來(lái)調(diào)用函數(shù)的,對(duì)于VC來(lái)說(shuō)毫無(wú)問(wèn)題,這本來(lái)就是針對(duì)C++而設(shè)計(jì)的,以前VB不行,現(xiàn)在VB也可以用指針了,也可以通過(guò)VTable來(lái)調(diào)用函數(shù)了,VJ也可以,但還是有些語(yǔ)言不行,那就是腳本語(yǔ)言,典型的如 VBScript、JavaScript。不行的原因在于它們并不支持指針,連指針都不能用還怎么用多態(tài)性啊,還怎么調(diào)這些虛函數(shù)啊。唉,沒(méi)辦法,也不能置這些腳本語(yǔ)言于不顧吧,現(xiàn)在網(wǎng)頁(yè)上用的都是這些腳本語(yǔ)言,而分布式應(yīng)用也是COM組件的一個(gè)主要市場(chǎng),它不得不被這些腳本語(yǔ)言所調(diào)用,既然虛函數(shù)表的方式行不通,我們只能另尋他法了。時(shí)勢(shì)造英雄,IDispatch應(yīng)運(yùn)而生。:-) 調(diào)度接口把每一個(gè)函數(shù)每一個(gè)屬性都編上號(hào),客戶程序要調(diào)用這些函數(shù)屬性的時(shí)侯就把這些編號(hào)傳給IDispatch接口就行了,IDispatch再根據(jù)這些編號(hào)調(diào)用相應(yīng)的函數(shù),僅此而已。當(dāng)然實(shí)際的過(guò)程遠(yuǎn)比這復(fù)雜,僅給一個(gè)編號(hào)就能讓別人知道怎么調(diào)用一個(gè)函數(shù)那不是天方夜潭嗎,你總得讓別人知道你要調(diào)用的函數(shù)要帶什么參數(shù),參數(shù)類型什么以及返回什么東西吧,而要以一種統(tǒng)一的方式來(lái)處理這些問(wèn)題是件很頭疼的事。IDispatch接口的主要函數(shù)是Invoke,客戶程序都調(diào)用它,然后Invoke再調(diào)用相應(yīng)的函數(shù),如果看一看MS的類庫(kù)里實(shí)現(xiàn) Invoke的代碼就會(huì)驚嘆它實(shí)現(xiàn)的復(fù)雜了,因?yàn)槟惚仨毧紤]各種參數(shù)類型的情況,所幸我們不需要自己來(lái)做這件事,而且可能永遠(yuǎn)也沒(méi)這樣的機(jī)會(huì)。:-)
(3) dispinterface接口、Dual接口以及Custom接口
這一小節(jié)放在這里似乎不太合適,因?yàn)檫@是在ATL編程時(shí)用到的術(shù)語(yǔ)。我在這里主要是想談一下自動(dòng)化接口的好處及缺點(diǎn),用這三個(gè)術(shù)語(yǔ)來(lái)解釋可能會(huì)更好一些,而且以后遲早會(huì)遇上它們,我將以一種通俗的方式來(lái)解釋它們,可能并非那么精確,就好象用偽代碼來(lái)描述算法一樣。-:)
所謂的自動(dòng)化接口就是用IDispatch實(shí)現(xiàn)的接口。我們已經(jīng)講解過(guò)IDispatch的作用了,它的好處就是腳本語(yǔ)言象VBScript、 JavaScript也能用COM組件了,從而基本上做到了與語(yǔ)言無(wú)關(guān)它的缺點(diǎn)主要有兩個(gè),第一個(gè)就是速度慢效率低。這是顯而易見(jiàn)的,通過(guò)虛函數(shù)表一下子就可以調(diào)用函數(shù)了,而通過(guò)Invoke則等于中間轉(zhuǎn)了道手續(xù),尤其是需要把函數(shù)參數(shù)轉(zhuǎn)換成一種規(guī)范的格式才去調(diào)用函數(shù),耽誤了很多時(shí)間。所以一般若非是迫不得已我們都想用VTable的方式調(diào)用函數(shù)以獲得高效率。第二個(gè)缺點(diǎn)就是只能使用規(guī)定好的所謂的自動(dòng)化數(shù)據(jù)類型。如果不用IDispatch我們可以想用什么數(shù)據(jù)類型就用什么類型,VC會(huì)自動(dòng)給我們生成相應(yīng)的調(diào)度代碼。而用自動(dòng)化接口就不行了,因?yàn)?/span>Invoke的實(shí)現(xiàn)代碼是VC事先寫好的,而它不能事先預(yù)料到我們要用到的所有類型,它只能根據(jù)一些常用的數(shù)據(jù)類型來(lái)寫它的處理代碼,而且它也要考慮不同語(yǔ)言之間的數(shù)據(jù)類型轉(zhuǎn)換問(wèn)題。所以VC自動(dòng)化接口生成的調(diào)度代碼只適用于它所規(guī)定好的那些數(shù)據(jù)類型,當(dāng)然這些數(shù)據(jù)類型已經(jīng)足夠豐富了,但不能滿足自定義數(shù)據(jù)結(jié)構(gòu)的要求。你也可以自己寫調(diào)度代碼來(lái)處理你的自定義數(shù)據(jù)結(jié)構(gòu),但這并不是一件容易的事。考慮到IDispatch的種種缺點(diǎn)(它還有一個(gè)缺點(diǎn),就是使用麻煩,:-) )現(xiàn)在一般都推薦寫雙接口組件,稱為dual接口,實(shí)際上就是從IDispatch繼承的接口。我們知道任何接口都必須從 IUnknown繼承,IDispatch接口也不例外。那從IDispatch繼承的接口實(shí)際上就等于有兩個(gè)基類,一個(gè)是IUnknown,一個(gè)是IDispatch,所以它可以以兩種方式來(lái)調(diào)用組件,可以通過(guò) IUnknown用虛函數(shù)表的方式調(diào)用接口方法,也可以通過(guò)IDispatch::Invoke自動(dòng)化調(diào)度來(lái)調(diào)用。這就有了很大的靈活性,這個(gè)組件既可以用于C++的環(huán)境也可以用于腳本語(yǔ)言中,同時(shí)滿足了各方面的需要。
相對(duì)比的,dispinterface是一種純粹的自動(dòng)化接口,可以簡(jiǎn)單的就把它看作是IDispatch接口 (雖然它實(shí)際上不是的),這種接口就只能通過(guò)自動(dòng)化的方式來(lái)調(diào)用,COM組件的事件一般都用的是這種形式的接口。
Custom接口就是從IUnknown接口派生的類,顯然它就只能用虛函數(shù)表的方式來(lái)調(diào)用接口了
(4) COM組件有三種,進(jìn)程內(nèi)、本地、遠(yuǎn)程。對(duì)于后兩者情況必須調(diào)度接口指針及函數(shù)參數(shù)。
COM是一個(gè)DLL,它有三種運(yùn)行模式。它可以是進(jìn)程內(nèi)的,即和調(diào)用者在同一個(gè)進(jìn)程內(nèi),也可以和調(diào)用者在同一個(gè)機(jī)器上但在不同的進(jìn)程內(nèi),還可以根本就和調(diào)用者在兩臺(tái)機(jī)器上。這里有一個(gè)根本點(diǎn)需要牢記,就是COM組件它只是一個(gè)DLL,它自己是運(yùn)行不起來(lái)的,必須有一個(gè)進(jìn)程象父親般照顧它才行,即COM組件必須在一個(gè)進(jìn)程內(nèi).那誰(shuí)充當(dāng)看護(hù)人的責(zé)任呢?先說(shuō)說(shuō)調(diào)度的問(wèn)題。調(diào)度是個(gè)復(fù)雜的問(wèn)題,以我的知識(shí)還講不清楚這個(gè)問(wèn)題,我只是一般性的談?wù)剮讉€(gè)最基本的概念。我們知道對(duì)于WIN32程序,每個(gè)進(jìn)程都擁有4GB的虛擬地址空間,每個(gè)進(jìn)程都有其各自的編址,同一個(gè)數(shù)據(jù)塊在不同的進(jìn)程里的編址很可能就是不一樣的,所以存在著進(jìn)程間的地址轉(zhuǎn)換問(wèn)題。這就是調(diào)度問(wèn)題。對(duì)于本地和遠(yuǎn)程進(jìn)程來(lái)說(shuō),DLL 和客戶程序在不同的編址空間,所以要傳遞接口指針到客戶程序必須要經(jīng)過(guò)調(diào)度。Windows 已經(jīng)提供了現(xiàn)成的調(diào)度函數(shù),就不需要我們自己來(lái)做這個(gè)復(fù)雜的事情了。對(duì)遠(yuǎn)程組件來(lái)說(shuō)函數(shù)的參數(shù)傳遞是另外一種調(diào)度。DCOM是以RPC為基礎(chǔ)的,要在網(wǎng)絡(luò)間傳遞數(shù)據(jù)必須遵守標(biāo)準(zhǔn)的網(wǎng)上數(shù)據(jù)傳輸協(xié)議,數(shù)據(jù)傳遞前要先打包,傳遞到目的地后要解包,這個(gè)過(guò)程就是調(diào)度,這個(gè)過(guò)程很復(fù)雜,不過(guò)Windows已經(jīng)把一切都給我們做好了,一般情況下我們不需要自己來(lái)編寫調(diào)度DLL。
我們剛說(shuō)過(guò)一個(gè)COM組件必須在一個(gè)進(jìn)程內(nèi)。對(duì)于本地模式的組件一般是以EXE的形式出現(xiàn),所以它本身就已經(jīng)是一個(gè)進(jìn)程。對(duì)于遠(yuǎn)程DLL,我們必須找一個(gè)進(jìn)程,這個(gè)進(jìn)程必須包含了調(diào)度代碼以實(shí)現(xiàn)基本的調(diào)度。這個(gè)進(jìn)程就是dllhost.exe。這是COM默認(rèn)的DLL代理。實(shí)際上在分布式應(yīng)用中,我們應(yīng)該用MTS來(lái)作為DLL代理,因?yàn)?/span>MTS有著很強(qiáng)大的功能,是專門的用于管理分布式DLL組件的工具。
調(diào)度離我們很近又似乎很遠(yuǎn),我們編程時(shí)很少關(guān)注到它,這也是COM的一個(gè)優(yōu)點(diǎn)之一,既平臺(tái)無(wú)關(guān)性,無(wú)論你是遠(yuǎn)程的、本地的還是進(jìn)程內(nèi)的,編程是一樣的,一切細(xì)節(jié)都由COM自己處理好了,所以我們也不用深究這個(gè)問(wèn)題,只要有個(gè)概念就可以了,當(dāng)然如果你對(duì)調(diào)度有自己特殊的要求就需要深入了解調(diào)度的整個(gè)過(guò)程了,這里推薦一本《COM+技術(shù)內(nèi)幕》,這絕對(duì)是一本講調(diào)度的好書。
(5) COM組件的核心是IDL。
我們希望軟件是一塊塊拼裝出來(lái)的,但不可能是沒(méi)有規(guī)定的胡亂拼接,總是要遵守一定的標(biāo)準(zhǔn),各個(gè)模塊之間如何才能親密無(wú)間的合作,必須要事先共同制訂好它們之間交互的規(guī)范,這個(gè)規(guī)范就是接口。我們知道接口實(shí)際上都是純虛類,它里面定義好了很多的純虛函數(shù),等著某個(gè)組件去實(shí)現(xiàn)它,這個(gè)接口就是兩個(gè)完全不相關(guān)的模塊能夠組合在一起的關(guān)鍵試想一下如果我們是一個(gè)應(yīng)用軟件廠商,我們的軟件中需要用到某個(gè)模塊,我們沒(méi)有時(shí)間自己開(kāi)發(fā),所以我們想到市場(chǎng)上找一找看有沒(méi)有這樣的模塊,我們?cè)趺慈フ夷兀恳苍S我們需要的這個(gè)模塊在業(yè)界已經(jīng)有了標(biāo)準(zhǔn),已經(jīng)有人制訂好了標(biāo)準(zhǔn)的接口,有很多組件工具廠商已經(jīng)在自己的組件中實(shí)現(xiàn)了這個(gè)接口,那我們尋找的目標(biāo)就是這些已經(jīng)實(shí)現(xiàn)了接口的組件,我們不關(guān)心組件從哪來(lái),它有什么其它的功能,我們只關(guān)心它是否很好的實(shí)現(xiàn)了我們制訂好的接口。這種接口可能是業(yè)界的標(biāo)準(zhǔn),也可能只是你和幾個(gè)廠商之間內(nèi)部制訂的協(xié)議,但總之它是一個(gè)標(biāo)準(zhǔn),是你的軟件和別人的模塊能夠組合在一起的基礎(chǔ),是COM組件通信的標(biāo)準(zhǔn)。
COM具有語(yǔ)言無(wú)關(guān)性,它可以用任何語(yǔ)言編寫,也可以在任何語(yǔ)言平臺(tái)上被調(diào)用。但至今為止我們一直是以C++的環(huán)境中談COM,那它的語(yǔ)言無(wú)關(guān)性是怎么體現(xiàn)出來(lái)的呢?或者換句話說(shuō),我們?cè)鯓硬拍芤哉Z(yǔ)言無(wú)關(guān)的方式來(lái)定義接口呢?前面我們是直接用純虛類的方式定義的,但顯然是不行的,除了C++誰(shuí)還認(rèn)它呢?正是出于這種考慮,微軟決定采用IDL來(lái)定義接口。說(shuō)白了,IDL實(shí)際上就是一種大家都認(rèn)識(shí)的語(yǔ)言,用它來(lái)定義接口,不論放到哪個(gè)語(yǔ)言平臺(tái)上都認(rèn)識(shí)它。我們可以想象一下理想的標(biāo)準(zhǔn)的組件模式,我們總是從IDL開(kāi)始,先用IDL制訂好各個(gè)接口,然后把實(shí)現(xiàn)接口的任務(wù)分配不同的人,有的人可能善長(zhǎng)用VC,有的人可能善長(zhǎng)用VB,這沒(méi)關(guān)系,作為項(xiàng)目負(fù)責(zé)人我不關(guān)心這些,我只關(guān)心你把最終的DLL 拿給我。這是一種多么好的開(kāi)發(fā)模式,可以用任何語(yǔ)言來(lái)開(kāi)發(fā),也可以用任何語(yǔ)言來(lái)欣賞你的開(kāi)發(fā)成果。
(6) COM組件的運(yùn)行機(jī)制,即COM是怎么跑起來(lái)的。
這部分我們將構(gòu)造一個(gè)創(chuàng)建COM組件的最小框架結(jié)構(gòu),然后看一看其內(nèi)部處理流程是怎樣的
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)看看它內(nèi)部做了一些什么事情。以下是它內(nèi)部實(shí)現(xiàn)的一個(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(); ........ } |
這段話的意思就是先得到類廠對(duì)象,再通過(guò)類廠創(chuàng)建組件從而得到IUnknown指針。繼續(xù)深入一步,看看CoGetClassObject的內(nèi)部偽碼:
CoGetClassObject(.....) { //通過(guò)查注冊(cè)表CLSID_Object,得知組件DLL的位置、文件名 //裝入DLL庫(kù) //使用函數(shù)GetProcAddress(...)得到DLL庫(kù)中函數(shù)DllGetClassObject的函數(shù)指針。 //調(diào)用DllGetClassObject } DllGetClassObject是干什么的,它是用來(lái)獲得類廠對(duì)象的。只有先得到類廠才能去創(chuàng)建組件. 下面是DllGetClassObject的偽碼: DllGetClassObject(...) { ...... CFactory* pFactory= new CFactory; //類廠對(duì)象 pFactory->QueryInterface(IID_IClassFactory, (void**)&pClassFactory); //查詢IClassFactory指針 pFactory->Release(); ...... } CoGetClassObject的流程已經(jīng)到此為止,現(xiàn)在返回CoCreateInstance,看看CreateInstance的偽碼: CFactory::CreateInstance(.....) { ........... CObject *pObject = new CObject; //組件對(duì)象 pObject->QueryInterface(IID_IUnknown, (void**)&pUnk); pObject->Release(); ........... } |
下圖是從COM+技術(shù)內(nèi)幕中COPY來(lái)的一個(gè)例圖,從圖中可以清楚的看到CoCreateInstance的整個(gè)流程。
(7) 一個(gè)典型的自注冊(cè)的COM DLL所必有的四個(gè)函數(shù)
DllGetClassObject:用于獲得類廠指針
DllRegisterServer:注冊(cè)一些必要的信息到注冊(cè)表中
DllUnregisterServer:卸載注冊(cè)信息
DllCanUnloadNow:系統(tǒng)空閑時(shí)會(huì)調(diào)用這個(gè)函數(shù),以確定是否可以卸載DLL
DLL還有一個(gè)函數(shù)是DllMain,這個(gè)函數(shù)在COM中并不要求一定要實(shí)現(xiàn)它,但是在VC生成的組件中自動(dòng)都包含了它,它的作用主要是得到一個(gè)全局的實(shí)例對(duì)象。
(8) 注冊(cè)表在COM中的重要作用
首先要知道GUID的概念,COM中所有的類、接口、類型庫(kù)都用GUID來(lái)唯一標(biāo)識(shí),GUID是一個(gè)128位的字串,根據(jù)特制算法生成的GUID可以保證是全世界唯一的。 COM組件的創(chuàng)建,查詢接口都是通過(guò)注冊(cè)表進(jìn)行的。有了注冊(cè)表,應(yīng)用程序就不需要知道組件的DLL文件名、位置,只需要根據(jù)CLSID查就可以了。當(dāng)版本升級(jí)的時(shí)侯,只要改一下注冊(cè)表信息就可以神不知鬼不覺(jué)的轉(zhuǎn)到新版本的DLL。