接触VC之四:COM组件模型基础
From:?http://daimajishu.iteye.com/blog/1081292
一年又一年,已經(jīng)又過了一年了。我VC的生涯已經(jīng)兩歲了。可以相當(dāng)?shù)貞c賀一下喲。回顧這一年的學(xué)習(xí)(唉,還沒有工作實(shí)踐呢。這年頭,工作不好找哇。),還學(xué)了不少的好東西。其中,最重要的就是COM組件模型,我個(gè)人覺得這個(gè)幾乎是Windows的核心。許多先進(jìn)的技術(shù)(比如微軟著名的DirectX,ADO,沒有人會不知道吧)都以COM組件的形式發(fā)布的。現(xiàn)在,我瞄上了另一個(gè)好東東,就是泛型編程技術(shù)。它能夠編寫出清晰、靈活、高度可重用的代碼,在ATL中就可以依稀看出它的影子(現(xiàn)在網(wǎng)上ATL文章有很多,我以后也會談到它)。好了,關(guān)于泛型編程的事今后再談。
按照我以前的計(jì)劃,我應(yīng)該談?wù)勎覍OM組件模型的認(rèn)識了。一來可以對自己的學(xué)習(xí)狀況進(jìn)行總結(jié)。二來,請教高手,可以幫忙指出錯(cuò)漏之處。三來,說不定會對初學(xué)者們有所幫助。請各位高手多多指正啊.在這里先謝了。
?一、動態(tài)鏈接庫:
動態(tài)鏈態(tài)庫是大部分COM組件的承載對象(不要在意ocx,它同樣也是dll,只不過改了一下后綴而已)。當(dāng)然Exe同樣也是可以的(TTS中的TextToSpeech對象就是一個(gè)例證),只不過在事實(shí)上要少得多。
在Windows初期,動態(tài)鏈態(tài)庫的出現(xiàn)是一場革命。它改變了Windows的一生,也為當(dāng)今Windows操作系統(tǒng)的霸主地位打下一塊堅(jiān)實(shí)的基石。(關(guān)于Windows的歷史問題,我一直沒有弄得太清楚。請VCKBASE的有關(guān)史學(xué)家們盡快寫出一篇文章來吧^_^)。
微軟對動態(tài)鏈接庫就是這樣解釋的:
動態(tài)鏈接庫 (DLL) 是作為共享函數(shù)庫的可執(zhí)行文件。動態(tài)鏈接提供了一種方法,使進(jìn)程可以調(diào)用不屬于其可執(zhí)行代碼的函數(shù)。函數(shù)的可執(zhí)行代碼位于一個(gè) DLL 中,該 DLL 包含一個(gè)或多個(gè)已被編譯、鏈接并與使用它們的進(jìn)程分開存儲的函數(shù)。DLL 還有助于共享數(shù)據(jù)和資源。多個(gè)應(yīng)用程序可同時(shí)訪問內(nèi)存中單個(gè) DLL 副本的內(nèi)容。
嗯,講得很清楚。動態(tài)鏈接庫首先是一個(gè)可執(zhí)行文件(微軟解釋說,exe叫做起直接可執(zhí)行文件),它里面包含著一組需要共享的函數(shù)。當(dāng)使用時(shí),動態(tài)鏈接庫(和Windows系統(tǒng))會提供一個(gè)方法來使我們的應(yīng)用程序可以調(diào)用其中的函數(shù)。此外,動態(tài)鏈接庫還會包含一些資源(如:圖標(biāo)、對話框模板等等)。在MFC中,微軟在現(xiàn)有動態(tài)鏈接庫的基礎(chǔ)上施用了一些技巧來提供一些另外功能,如MFC類的導(dǎo)出。
動態(tài)鏈接庫的鏈接方式大致分為兩類: 靜態(tài)鏈接和動態(tài)鏈接.
靜態(tài)鏈接又叫隱式鏈接,這種鏈接方式使我們在代碼中不用語句來指示系統(tǒng)中,我們的應(yīng)用程序要加載哪些動態(tài)鏈接庫。其靜態(tài)鏈接聲明是放在工程屬性中的(或者使用#pragma comment(lib,"XXX.lib"),這個(gè)可以和#include放在一起)。在指定時(shí),只需要輸入其動態(tài)鏈接庫相應(yīng)的導(dǎo)入庫文件(.lib)。然后,你就可以在程序的任何地方像調(diào)用普通函數(shù)一樣調(diào)用該動態(tài)鏈接庫中存在的函數(shù)了(當(dāng)然,你需要包含其相應(yīng)的頭文件。一般情況下,頭文件會和LIB文件一塊給出)。通過這種方法生成的程序在運(yùn)行初始化的時(shí)候(具體到什么時(shí)候不太清楚。但我可以肯定是在WinMain函數(shù)之前了^_^),會自動將動態(tài)鏈接庫加載在系統(tǒng)環(huán)境中,并將其映射到我們應(yīng)用程序的進(jìn)程當(dāng)中去。當(dāng)我們調(diào)用一個(gè)我們進(jìn)程沒有定義的函數(shù)時(shí),VC運(yùn)行庫會通過查找LIB文件的相關(guān)信息找到相應(yīng)動態(tài)鏈接庫的函數(shù)并調(diào)用它。進(jìn)程結(jié)束時(shí),系統(tǒng)會缷載動態(tài)鏈接庫。
動態(tài)鏈接又叫顯式鏈接,顧名思義這種方式讓我們必需在代碼通過調(diào)用API來顯式地加載動態(tài)鏈接庫。COM組件模型全部都是采用這種方式來加載進(jìn)程內(nèi)組件模塊(就是Dll)的。(我覺得微軟的專業(yè)術(shù)語有些混亂耶)。這個(gè)方式有許多好處,它可以在運(yùn)行時(shí)決定具體要加載哪個(gè)鏈接庫,要調(diào)用哪個(gè)函數(shù)…這才叫動態(tài)鏈接呢。
要使用動態(tài)鏈接庫并不難,首先要調(diào)用LoadLibrary,其原型如下:
參數(shù)lpFileName是要加載的動態(tài)鏈接庫的文件名。如果加載成功的話,就返回其句柄。否則的,返回NULL。
與這個(gè)API相配對的是FreeLibrary,其原型如下:
這個(gè)就不用我多說了吧。
當(dāng)動態(tài)鏈接庫被LoadLibrary所加載時(shí),C運(yùn)行庫通過_DllMainCRTStartup來完成動態(tài)鏈接庫的初始化,如全局對象(變量)、靜態(tài)成員變量的生成以及賦初值。最重要的是它還會調(diào)用DllMain函數(shù)。每一個(gè)動態(tài)鏈接庫都必須有這個(gè)函數(shù),就像應(yīng)用程序必須有main或WinMain一樣。它的原型是:
你可以通過DllMain函數(shù)來完成你的動態(tài)鏈接庫中的環(huán)境初始化和析構(gòu)操作。啊,事情是這樣的:
DllMain被調(diào)用有四種情況,這四種情況可以從fdwReason參數(shù)來分別出來:
它們分別是
1. DLL_PROCESS_ATTACH,當(dāng)動態(tài)鏈接庫被加載到進(jìn)程時(shí),調(diào)用DllMain。
2. DLL_THREAD_ATTACH,當(dāng)進(jìn)程建立一個(gè)新線程時(shí),進(jìn)程會調(diào)用所以已加載了的動態(tài)鏈接庫的DllMain。
3. DLL_THREAD_DETACH,當(dāng)一個(gè)線程結(jié)束時(shí),進(jìn)程會調(diào)用所以已加載了的動態(tài)鏈接庫的DllMain。
4. DLL_THREAD_DETACH,當(dāng)動態(tài)鏈接庫被缷載或進(jìn)程結(jié)束時(shí),調(diào)用DllMain。
這樣,通過DllMain函數(shù)就可以反應(yīng)出一個(gè)動態(tài)鏈接庫的生命周期了。
當(dāng)加載成功后,我們會得到一個(gè)HMODULE句柄。這個(gè)句柄的使用與HINSTANCE應(yīng)用程序?qū)嵗木浔芟嗨?追查定義,HMODULE就是HINSTANCE)。我們可以使用下面一些API函數(shù)來使用HMODULE句柄:
LoadBitmap、LoadIcon、LoadString、…、GetProcAddress等等
其中,最重要的就是GetProcAddress。它是用來返回鏈接庫中的某個(gè)函數(shù)的函數(shù)指針,然后我們就可以通過這個(gè)函數(shù)指針來調(diào)用這個(gè)鏈接庫函數(shù)了。(如果你對函數(shù)指針不熟的話,最好再看一看C\C++語法書。我覺得函數(shù)指針的聲明方法很怪異)其原型如下:
啊,hModule我就不說了。lpProcName參數(shù)是一個(gè)字符串,這個(gè)字符串寫著我們要找的函數(shù)的函數(shù)名。如果找到了的話,就返回這個(gè)函數(shù)的指針,否則返回NULL。
舉個(gè)例子:
比如說有個(gè)鏈接庫函數(shù)是”int Plus(int nAugend, int nAddend)”,我要調(diào)用它。
如果我以及這個(gè)鏈接庫沒有問題的話,我想輸出結(jié)果應(yīng)該是2。
我仍然認(rèn)為函數(shù)指針的聲明很怪異,可讀性并不高,所以我一般會換一種寫法。
雖然會出一個(gè)警告,但我覺得這樣會舒服一些。
嗯,動態(tài)鏈接庫的情況就基本如此了。具體動態(tài)鏈接庫的編寫會和COM組件的編寫一塊在后續(xù)章節(jié)里談及。
?二、面向?qū)ο蟮慕M件模型-COM
Windows系統(tǒng)霸主地位詁計(jì)三四年內(nèi)是不會被動搖的。因此,有n多Windows開發(fā)平臺出現(xiàn)在我們面前。n多種開發(fā)語言是百花齊放啊。于是,我們像圣經(jīng)里說的那樣,操著不同的語言,彼此無法溝通。為改變這一現(xiàn)實(shí),可愛的比爾就站出來了,”偶要改變世界!”。微軟公司制定一個(gè)基于二進(jìn)制通用接口規(guī)范-Component Object Model(組件對象模型)。但是,一開始COM的解決目標(biāo)并非是為了通用接口,而是應(yīng)用于復(fù)合文檔(OLE)的實(shí)現(xiàn)。而今由于語言無關(guān)性、進(jìn)程透明性、可重用性、保密性(除非高手高手高高手,有誰能從匯編碼中看出實(shí)現(xiàn)技術(shù)來)、而且編寫并不困難,所以發(fā)展成為了一項(xiàng)應(yīng)用廣泛的技術(shù)。
1) 組件對象與接口
組件對象、接口是COM的根基。
下面,請?jiān)试S我用C++對象做一個(gè)類比。
組件對象與C++對象的意義是基本相同的。它是一個(gè)功能、屬性與邏輯的整體。它是一個(gè)實(shí)體對象,通過對它的接口操作,可以使用它所提供的功能。
接口相當(dāng)于C++對象中的public成員。它被暴露給外部使用者,使用者只被允許調(diào)用這些被暴露在外面的接口來使用對象的功能。與public成員有所不同的是,接口不是一個(gè)變量也不是一個(gè)函數(shù),而應(yīng)該是一組函數(shù)。在邏輯上,這個(gè)組函數(shù)應(yīng)該是功能相關(guān)的。一個(gè)組件對象可以擁有許多個(gè)接口。
我只知道C++的COM實(shí)現(xiàn)方法,至于Dephi我就一無所知。
C++實(shí)現(xiàn)方法是:由C++類對象來完成組件對象的實(shí)現(xiàn),由C++純虛類來代表接口。C++類對象通過多重繼承多個(gè)接口,來的擁有多個(gè)接口。
下面,我舉一個(gè)例子,來說明C++中的組件對象與接口的關(guān)系(下面的例子并不是一個(gè)COM實(shí)現(xiàn),只是用來表示組件對象與接口的關(guān)系)
我如果要做一個(gè)人的組件對象的話,我首先要定義一些接口來表示人的外部表現(xiàn)行為。
我將人的行為分成了生理學(xué)、心理學(xué)和動力學(xué)三類,讓它們分別表示人不同的行為。那么,這么三組相關(guān)函數(shù)就是三個(gè)接口。C++組件對象的實(shí)現(xiàn)就是從這些接口中多重派生,并實(shí)現(xiàn)它們。這樣,我們就得到一個(gè)組件對象(聲明啊,本示例只是一個(gè)表示概念,真正的COM組件對象還需要加一些東東)。
class human : public physiology,public psychics,public dynamics { public:void eat(Food in) {cout << "Good! Very delicious!"; }void drink(Liquid in) {cout << "No! I am not drunk!"; }Something toilet() {cout << "hum…….";return dejecta(); }Sound laugh() {return Sound("Ha…Ha…Ha"); }Sound cry() {return Sound("dad!Don’t beat my buns."); }Sound angry() {return Sound("where did you go last night? Darling."); }Speed run() {cout << "Run, Police come!";return 20km/h; }Speed walk() {cout << "out. yegg, I am no…not afraid o….of y…you.";return 1m/s; }Interval jump() {cout << "Yeah….";return 4m; } };
這樣,一個(gè)組件對象就定義完了。當(dāng)使用組件對象時(shí),系統(tǒng)所給予你的一個(gè)指針。它是一個(gè)組件對象實(shí)現(xiàn)了的虛類指針,我們可以使用它來調(diào)用組件對象對于這個(gè)純虛類所實(shí)現(xiàn)的功能(當(dāng)然,我們有選擇什么虛類指針的權(quán)利;只要組件對象支持就可以了)。
總之,一個(gè)組件對象外部特征是由不同的接口也就是這些虛類所組成,它們向使用者展現(xiàn)組件所提供的功能。
注:如果你的C++虛函數(shù)沒學(xué)得不太好的話,那么請找一本C++語法書再看一看. 或請參看VCKBASE第12期的《解析動態(tài)聯(lián)編》。
2) 標(biāo)識符(GUID)
上面,我說過COM組件是基于二進(jìn)制的。那么要我們使用簽名(比如說類名、接口名)來指定一個(gè)組件顯然是不理想的了(至少在識別方面會有些麻煩)。那么,既然是二進(jìn)制系統(tǒng)最方便當(dāng)然就是使用數(shù)字標(biāo)識了。于是,微軟定義了這么一個(gè)結(jié)構(gòu)標(biāo)準(zhǔn):
結(jié)構(gòu)用來儲存一些數(shù)字信息,來表識一個(gè)COM對象,接口以及其它COM元素。這個(gè)結(jié)構(gòu)體就叫做標(biāo)識符。
在C++中一個(gè)標(biāo)識符是這么表示的:
同樣的標(biāo)識符在其它非C環(huán)境中是這么表示的:
{54bf6567-1007-11d1-b0aa-444553540000} 這個(gè)標(biāo)識符代表著一個(gè)COM對象,這是因?yàn)橐粋€(gè)COM對象的標(biāo)識符名都以CLISID_為前綴。接口名則是以IID_為前綴。不要問我,標(biāo)識符定義與對象具體有什么關(guān)系式。我不知道。它們根本就沒有什么關(guān)系的。一個(gè)COM對象在編寫時(shí),我們會使用隨機(jī)的方法來確定它的標(biāo)識符(這個(gè)工作可以由VC來幫我們搞定)。一旦COM對象得到一個(gè)標(biāo)識符并發(fā)布出去的話,那么就不能更改了。另外,不要擔(dān)心GUID會有所沖突。如果你的高中數(shù)學(xué)已經(jīng)及格了的話,那么請算一算128位二進(jìn)制中,重復(fù)的概率會有多少。假如你真的發(fā)現(xiàn)了GUID有沖突的話(你要保證這不是人為),建議你趕去買彩票吧。你離500萬不遠(yuǎn)了。
3) IUnknown接口
COM模式所有接口必須遵守一定規(guī)范,這就是IUnknown接口的出處。每個(gè)一接口都必須從這個(gè)接口繼承。在C++中,微軟已經(jīng)為我們把IUnknown定義好了:
注:void *可以指向任何對象。我開始的時(shí)候?qū)oid*一點(diǎn)都不理解。這里使用的原因是傳出與傳入指針類型不確定。
QueryInterface函數(shù)功能是當(dāng)我們得到一個(gè)接口指針,并且我們想得到另一個(gè)接口指針的時(shí)候,提供幫助。我們將我們想要得到的接口的標(biāo)識符傳給iid,將把指針的做一個(gè)次&來傳給ppv。如果QueryInterface成功的話,會返回S_OK。我們指針中就會指向我們想要的接口。
AddRef,Release用于實(shí)現(xiàn)引用計(jì)數(shù)機(jī)制。
在二進(jìn)制系統(tǒng)中,組件對象不像C++環(huán)境中對象那樣具有明確的生存期。可能會出現(xiàn)這種情況,兩個(gè)(或者兩個(gè)以上)的地方(可能是不同的程序之間,也可能是不同的線程之間)同時(shí)使用著一個(gè)組件對象,如果其中一個(gè)地方delete掉了組件對象的話。其它地方不可能會知道,當(dāng)它們嘗試調(diào)用這個(gè)象的話,輕則導(dǎo)致重傷,重則導(dǎo)致死亡。這不是我們希望看到的。于是,COM模型設(shè)制一個(gè)引用計(jì)數(shù)機(jī)制。
當(dāng)一個(gè)地方開始使用對象的時(shí)候,它必須調(diào)用AddRef()一次。當(dāng)我們使用QueryInterface時(shí)候,QueryInterface必須為我們調(diào)用一次AddRef()。AddRef()會使組件對象的引用計(jì)數(shù)增1。當(dāng)這個(gè)地方不再使用對象時(shí),它必須調(diào)用Release()一次。Release()會使組件對象的引用計(jì)數(shù)減1。當(dāng)組件對象的引用計(jì)數(shù)變成0,就表明沒有人再去使用組件對象了。這時(shí),組件對象應(yīng)該結(jié)束自己的生命。這樣,就保證了組件對象生存期間其它程序的安全。
當(dāng)然,你可以使用自己的引用機(jī)制,只要你的行為上支持AddRef和Release。比如說,不設(shè)置對象的引用計(jì)數(shù),而是為每個(gè)接口設(shè)置一個(gè)引用計(jì)數(shù)。當(dāng)所有的接口引用計(jì)數(shù)都為0時(shí),delete對象。
好了,前面的示例中,我并沒有遵守IUknown規(guī)范,下面我要遵守它。我把上次同樣?xùn)|西用……省略掉了。
// {6AAF876E-FCED-4ee0-B5D3-63CD6E2242F5} static const GUID IID_IPhysiology = { 0x6aaf876e, 0xfced, 0x4ee0, { 0xb5, 0xd3, 0x63, 0xcd, 0x6e, 0x22, 0x42, 0xf5 } }; class IPhysiology:public IUnknown { public:…… };// {183FC7A1-4C27-4c38-B72D-D1326E2E8A7C} static const GUID IID_IPsychics = { 0x183fc7a1, 0x4c27, 0x4c38, { 0xb7, 0x2d, 0xd1, 0x32, 0x6e, 0x2e, 0x8a, 0x7c } }; class IPsychics:public IUnknown { public:…… };// {5F144D5C-A20C-42e7-8F91-4D5CAE430B29} static const GUID IID_IDynamics = { 0x5f144d5c, 0xa20c, 0x42e7, { 0x8f, 0x91, 0x4d, 0x5c, 0xae, 0x43, 0xb, 0x29 } }; class IDynamics:public IUnknown { public:…… };// {ABFA7022-7E2F-4d0e-8A4F-F58BBCEBB2DA} static const GUID CLISID_Human = { 0xabfa7022, 0x7e2f, 0x4d0e, { 0x8a, 0x4f, 0xf5, 0x8b, 0xbc, 0xeb, 0xb2, 0xda } }; class human : public IPhysiology,public IPsychics,public IDynamics { public: …… human() {m_ulRef = 0; }HRESULT QueryInterface(const IID& iid, void **ppv) {if (iid == IID_IUnknown || iid == IID_IPhysiology){*ppv = static_cast<IPhysiology*>(this);(IPhysiology*)(*this))->AddRef();}else if (iid == IID_IPsychics){*ppv = static_cast<IPsychics*>(this);(IPsychics*)(*this))->AddRef();}else if (iid == IID_IDynamics){*ppv = static_cast<IDynamics*>(this);(IDynamics*)(*this))->AddRef();}else{*ppv = NULL;return E_NOTINTERFACE;}return S_OK; }ULONG AddRef() {return ++m_ulRef; }ULONG Release() {m_ulRef--;if (m_ulRef <= 0){m_ulRef = 0;delete this;}return m_ulRef; }ULONG m_ulRef; };
這樣我們的組件對象就定義完全了。
下面給出我們這個(gè)組件對象的IDL描述和圖形描述
#include "olectl.h" import "oaidl.idl"; import "ocidl.idl";[object,uuid(6AAF876E-FCED-4ee0-B5D3-63CD6E2242F5),nonextensible,helpstring("IPhysiology 接口"),pointer_default(unique) ] interface IPhysiology : IUnknown { void eat(Food in);void drink(Liquid in);Somethings toilet(); };[object,uuid(5F144D5C-A20C-42e7-8F91-4D5CAE430B29),nonextensible,helpstring("IPsychics 接口"),pointer_default(unique) ] interface IPsychics : IUnknown { Sound laugh();Sound cry();Sound angry(); };[object,uuid(5F144D5C-A20C-42e7-8F91-4D5CAE430B29),nonextensible,helpstring("IDynamics 接口"),pointer_default(unique) ] interface IDynamics : IUnknown { Speed run() = 0;Speed walk() = 0;Interval jump() = 0; };[uuid(6CC7B329-B92F-4A8F-9CDD-1AB6D7E4CF4D),version(1.0),helpstring("OLEOBJECT 1.0 類型庫") ] library OLEOBJECTLib {importlib("stdole2.tlb");[uuid(62FD0E39-DA84-4B19-BAB0-960A27AC2B71),helpstring("OlePaint Class")]coclass OlePaint{[default] interface IPhysiology,interface IPsychics,interface IDynamics}; };
請伃細(xì),觀察上面的描述IDL代碼和圖形。并不是太難吧。
4) COM對象的接口原則
為了規(guī)范COM的接口機(jī)制,微軟向COM開發(fā)者發(fā)布了COM對象的接口原則。
(1)IUnknown接口的等價(jià)性
當(dāng)我們要等到兩個(gè)接口指針,我如何判斷它們從屬于一個(gè)對象呢。COM接口原則規(guī)定,同一個(gè)對象的Queryinterface的IID_IUnknown查詢出來的IUnknown指針值應(yīng)當(dāng)相等。也就是說,每個(gè)對象的IUnknown指是唯一的。我們可以通過判斷IUnknown指針是否相等來判斷它們是否指向同一個(gè)對象。
當(dāng)然,如果查詢的不是IUnknown接口,則無此限制。同一對象對非IUnknown接口的查詢值可以不同。
(2)接口自反性,對一個(gè)接口來說,查詢它本身應(yīng)該是允許的。
設(shè)pPsychics是已賦值IPsychics的接口。
那么pPsychics->QueryInterface(IID_IPsychics,(void **) &XXX);應(yīng)當(dāng)成功。
(3)接口對稱性,當(dāng)我們從一個(gè)接口查詢到另一個(gè)接口時(shí),那么我們再從結(jié)果接口還可以查詢到原來的接口。
例如:
如果pSrcPsychics->QueryInterface(IID_IDynamics,(void **) &pDynamics);成功的話。
那么pDynamics->QueryInterface(IID_IPsychics,(void **) &pTarget);也相當(dāng)成功。
(4)接口傳遞性。如果我們從第一個(gè)接口查詢到了第二個(gè)接口,又從第二個(gè)接口查詢到了第三接口。則我們應(yīng)該能夠從第三個(gè)接口查詢到第一個(gè)接口。其它依此類推。
(5)接口查詢時(shí)間無關(guān)性。當(dāng)我們在某時(shí)查詢到一個(gè)接口,那么在任意時(shí)刻也應(yīng)該查詢到這個(gè)接口。
嗯,COM的基本知識好像這么多了。好像片篇太長呵。那么COM實(shí)現(xiàn)方法留到下一篇吧。
(待續(xù)...)
作者信息:
釋雪?
MSN Messenger:Blue_Atlantis400@hotmail.com
QQ:63068279
總結(jié)
以上是生活随笔為你收集整理的接触VC之四:COM组件模型基础的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: X5开发中buttongrounp对应c
- 下一篇: 使用 rose 将 c++代码转换为 u