COM接口與COM組件
COM接口是COM規(guī)范中最重要的部分,COM規(guī)范的核心內(nèi)容就是對接口的定義,甚至可以說“在COM中接口就是一切”。組件與組件之間、組件與客戶之間都要通過接口進行交互。接口成員函數(shù)將負責(zé)為客戶或其他組件提供服務(wù)。與標識COM對象的CLSID類似,每一個COM接口也使用一個GUID來進行標識,該標識也被稱為IID(interface identifier,接口標識符)。
COM接口實際限定了組件與使用該組件的客戶程序或其他組件所能進行的交互方式,任何一個具備相同接口的組件都可對此組件進行相對于其他組件透明的替換。只要接口不發(fā)生變化,就可以在不影響整個由組件構(gòu)成的系統(tǒng)的情況下自由的更換組件。通常在程序設(shè)計階段需要將接口設(shè)計的盡可能完美,以減少在開發(fā)階段對COM接口的更改。盡管如此,在實際應(yīng)用中是很難做到這一點的,往往需要在現(xiàn)有接口基礎(chǔ)上對其做進一步的發(fā)展。與C++中對類的繼承有些類似,對COM接口的發(fā)展也可以通過接口繼承來實現(xiàn)。但是COM接口的繼承只能是單繼承而不允許從多個基接口進行派生,而且派生接口只是繼承了對基接口成員函數(shù)的說明而沒有繼承其實現(xiàn)。
COM接口是COM規(guī)范中最重要的部分,COM規(guī)范的核心內(nèi)容就是對接口的定義,甚至可以說“在COM中接口就是一切”。組件與組件之間、組件與客戶之間都要通過接口進行交互。接口成員函數(shù)將負責(zé)為客戶或其他組件提供服務(wù)。與標識COM對象的CLSID類似,每一個COM接口也使用一個GUID來進行標識,該標識也被稱為IID(interface identifier,接口標識符)。
COM接口實際限定了組件與使用該組件的客戶程序或其他組件所能進行的交互方式,任何一個具備相同接口的組件都可對此組件進行相對于其他組件透明的替換。只要接口不發(fā)生變化,就可以在不影響整個由組件構(gòu)成的系統(tǒng)的情況下自由的更換組件。通常在程序設(shè)計階段需要將接口設(shè)計的盡可能完美,以減少在開發(fā)階段對COM接口的更改。盡管如此,在實際應(yīng)用中是很難做到這一點的,往往需要在現(xiàn)有接口基礎(chǔ)上對其做進一步的發(fā)展。與C++中對類的繼承有些類似,對COM接口的發(fā)展也可以通過接口繼承來實現(xiàn)。但是COM接口的繼承只能是單繼承而不允許從多個基接口進行派生,而且派生接口只是繼承了對基接口成員函數(shù)的說明而沒有繼承其實現(xiàn)。
interface IX // IX接口 { virtual void __stdcall Func1() = 0; virtual void __stdcall Func2() = 0; }; interface IY // IY接口 { virtual void __stdcall Func3() = 0; virtual void __stdcall Func4() = 0; }; class CObjectA // 組件A { public: // 抽象基類IX的實現(xiàn) virtual void Func1() {cout<<"Func1"<<endl;}; virtual void Func2() {cout<<"Func2"<<endl;}; // 抽象基類IY的實現(xiàn) virtual void Func3() {cout<<"Func3"<<endl;}; virtual void Func4() {cout<<"Func4"<<endl;}; }; |
對于接口,通常是采用抽象基類來定義,并利用類的多重繼承來實現(xiàn)該組件。例如,在上面這段代碼中,IX和IY是用于實現(xiàn)接口的抽象基類。所謂的抽象基類是只包含一個或多個虛函數(shù)聲明而未包括虛函數(shù)的具體實現(xiàn)的類。抽象基類不能被實例化,而只能用作基類使用,并要求其派生類完成其所有虛函數(shù)的實現(xiàn)。在上面這段代碼中,CObjectA組件即繼承了IX和IY這兩個抽象基類,并實現(xiàn)了其所定義的虛函數(shù)。圖2為此組件具有的這兩個接口的模型展示:

圖2 接口模型
抽象基類本身由于沒有實體函數(shù)與變量,所以并不分配內(nèi)存。通常只是用來為派生類指定內(nèi)存結(jié)構(gòu)。只有在派生類實現(xiàn)此抽象基類時,指定的內(nèi)存才會被分配。圖3為此內(nèi)存結(jié)構(gòu)的示意:

圖3 抽象基類定義的內(nèi)存結(jié)構(gòu)示意
圖中vtable為虛擬函數(shù)表,能夠為實例數(shù)據(jù)的提供一個方便保存的位置,并能夠在同一類的多個實例間共享。在每個實例的內(nèi)存映射中均包含一個指向該類的vtable表的指針pVtable。pVtable指針存放于所有數(shù)據(jù)成員之前,由于每個虛函數(shù)在vtable表中有唯一的索引,編譯器只需根據(jù)索引從vtable表中找到函數(shù)地址即可。也就是說,客戶只要獲取得到了接口指針,就可以使用此COM對象的實際功能。
由抽象基類指定的內(nèi)存結(jié)構(gòu)是符合COM規(guī)范的,因此抽象基類IX可以認為是一個COM接口,但這還不是一個嚴格意義上的COM接口。對于一個真正意義上的COM接口,在設(shè)計時應(yīng)遵循以下幾個規(guī)則:
1) 接口必須直接或間接地從IUnknown繼承。
2) 接口必須具有唯一的標識(IID)。
3) 一旦分配和公布了IID,有關(guān)接口定義的任何因素都不能被改變。
4) 接口成員函數(shù)應(yīng)具有HRESULT類型的返回值。
5) 接口成員函數(shù)的字符串參數(shù)應(yīng)采用Unicode類型。
這幾條規(guī)則中,最基本的是第一條,如果一個對象沒有至少實現(xiàn)一個最小程度為IUnknown的接口,那么該對象也就不是一個嚴格的COM對象。IUnknown接口是COM的核心接口,從上述規(guī)則可以得知,任何一個COM接口都必須從IUnknown接口繼承??蛻粼诮M件之間的通信是通過接口來實現(xiàn)的。組件可以不提供其他接口,但是必須提供IUnknown接口以使客戶能夠?qū)M件其他接口進行查詢。
IUnknown接口提供有成員函數(shù)QueryInterface()、AddRef()和Release(),分別用于查詢組件中的其他接口和進行生存期控制。由于任何COM接口都是從IUnknown接口派生,因此在所有COM接口虛擬函數(shù)表中保存的前三個成員函數(shù)指針一定是指向QueryInterface()、AddRef()和Release()的指針。這樣,任何一個COM接口都可以被當作IUnknown接口來處理。在創(chuàng)建組件時,客戶可以通過CreateInstance()函數(shù)得到IUnknown接口指針。
COM規(guī)范允許使用多接口,QueryInterface()成員函數(shù)可以用來查詢組件是否支持某個特定的接口。如果支持,QueryInterface()將返回此接口的指針。其第一個參數(shù)為一個IID結(jié)構(gòu),指出了客戶所要查詢的接口,查詢到的接口指針將存放在ppv所指向的變量中。函數(shù)的成功執(zhí)行與否將返回S_OK或E_NOINTERFACE。但是,在使用時不能簡單的將QueryInterface()返回值與其進行比較,而應(yīng)使用SUCCEEDED或FAILED宏。例如:
由于QueryInterface()過于靈活,為避免由此引發(fā)的沖突在COM規(guī)范中定義了QueryInterface()所有實現(xiàn)都必須遵循的一些規(guī)則:
1) 過同一對象各個接口指針所查詢得到的IUnknown接口指針必須是指向同一個IUnknown接口的。即,IUnknow接口的唯一性。
2) 如果某接口曾經(jīng)被成功查詢過,那么此后任何時間對該接口的查詢也必定會成功。即,接口與查詢時間的無關(guān)性。
3) 對于已經(jīng)獲取到的接口仍可對其進行再次查詢,并且必定會成功。即,接口的自反性。
4) 客戶能夠從任何接口查詢到另外一個接口,而且能夠返回到起始接口。即,接口的對稱性。
5) 如果能夠從某接口獲取到某特定接口,那么從任意接口都可以得到此接口。即,接口的傳遞性。
IUnknown接口的另兩個成員函數(shù)AddRef()和Release()對對象的生存期進行了控制。每個COM對象都記錄有一個引用計數(shù),該引用計數(shù)表示了當前引用了此COM對象的有效指針的個數(shù)。AddRef()和Release()實現(xiàn)的即是這種引用計數(shù)的內(nèi)存管理技術(shù):引用計數(shù)初始為0,客戶每得到一個指向此對象的接口指針即通過AddRef()將引用計數(shù)加1;在每用完此接口指針后,調(diào)用Release()函數(shù)將引用計數(shù)減1。如果引用計數(shù)減到0,則從內(nèi)存卸載掉此COM對象。關(guān)于引用計數(shù)的使用,在COM規(guī)范中也設(shè)置了以下幾條簡單的規(guī)則:
1) 任何能夠返回接口指針的函數(shù)(如QreryInterface()、CreateInstance()等)在返回接口指針之前,必須用相應(yīng)的指針調(diào)用AddRef()函數(shù)。
2) 在使用完任何一個接口后,應(yīng)及時調(diào)用該接口的Release()函數(shù)。
3) 在進行接口指針賦值操作后,應(yīng)調(diào)用AddRef()函數(shù)。
COM組件的創(chuàng)建可以通過CoCreateInstance()函數(shù)來完成,函數(shù)原型為:
函數(shù)參數(shù)clsid是要創(chuàng)建組件的CLSID,pIUnknownOuter用于聚合組件,如果不使用可以設(shè)置為NULL。參數(shù)dwClsContext則限定了所創(chuàng)建組件的執(zhí)行上下文。最后兩個參數(shù)iid和ppv則分別為要使用接口的IID和返回得到的接口指針。在使用時只需將CLSID、IID等作為參數(shù)傳入即可創(chuàng)建相應(yīng)的組件并從輸出參數(shù)ppv得到所請求接口的指針。如果函數(shù)是直接創(chuàng)建組件的,那么在函數(shù)返回時組件將創(chuàng)建完畢,這樣客戶將無法對組件的創(chuàng)建過程進行任何干預(yù),靈活性太差。因此,CoCreateInstance()在函數(shù)內(nèi)部實現(xiàn)中通過調(diào)用CoGetClassObject()函數(shù)先創(chuàng)建一種專門用來創(chuàng)建組件的組件來解決此問題。這種用途的組件被稱為類廠(class factory)。
類廠所支持的用以創(chuàng)建組件的接口是IClassFactory,該接口從IUnknown派生,并具有兩個自己的接口成員函數(shù)CreateInstance()和LockServer()。這兩個成員函數(shù)分別用于創(chuàng)建COM組件對象和控制組件的生存期。下面先給出CreateInstance()的函數(shù)聲明:
可以看出,這個用于創(chuàng)建組件對象的CreateInstance()函數(shù)并未包含一個用來接受CLSID的參數(shù),顯然該函數(shù)將只能創(chuàng)建同某個CLSID相應(yīng)的組件。對于一個類廠,由于只能通過CreateInstance()函數(shù)去創(chuàng)建組件,因此只能創(chuàng)建與某個特定CLSID相應(yīng)的組件。
創(chuàng)建類廠的CoGetClassObject()函數(shù)將接收一個CLSID作為參數(shù)并返回指向類廠對象IClassFactory接口的指針??蛻魧⒖梢酝ㄟ^此指針來創(chuàng)建所需要的組件并返回某接口的指針。通過此指針,客戶將可以直接調(diào)用新創(chuàng)建的COM對象接口的成員函數(shù),從而獲得COM對象的所有服務(wù)。
在用CoGetClassObject()創(chuàng)建類廠對象時,如果COM對象是進程內(nèi)組件(組件與客戶處于同一進程地址空間,通常多以DLL形式存在),CoGetClassObject()將調(diào)用DLL模塊的DllGetClassObject()引出函數(shù)并把clsid、iid和ppv等參數(shù)傳遞進去以創(chuàng)建類廠,并返回類廠對象的接口指針。
如果COM對象是進程外組件(擁有獨立的進程地址空間,通常多以EXE形式存在),則CoGetClassObject()將要首先啟動組件進程,并一直等待到組件進程通過CoRegisterClassObject()函數(shù)將類廠注冊到COM后,才會返回COM中相應(yīng)的類廠信息。一旦組件進程退出,此注冊的類廠對象也就不再有效,需調(diào)用CoRevokeClassObject()函數(shù)予以通知。圖4展示了通過類廠創(chuàng)建組件的過程:

圖4 組件的創(chuàng)建過程
客戶程序?qū)OM組件的調(diào)用主要分對進程內(nèi)組件調(diào)用和進程外調(diào)用兩種情況。在具體過程上卻并沒有什么太大的區(qū)別。為了能夠使用COM庫提供的API函數(shù),首先要用CoInitialize()初始化COM庫。
雖然通過CLSID和ProgID都可以標識一個組件,但ProgID顯然要比CLSID更易于理解和使用,因此通常很少直接使用CLSID,而是通過使用CLSIDFromProgID(),根據(jù)ProgID得到組件的CLSID。進而以此返回的CLSID作為參數(shù)去調(diào)用CoGetClassObject()以創(chuàng)建類廠對象并返回類廠接口指針。通過該指針調(diào)用類廠對象的CreateInstance()接口成員函數(shù),執(zhí)行結(jié)果將創(chuàng)建與CLSID相應(yīng)的組件對象并返回IUnknown接口指針。通過此接口的QueryInterface()成員函數(shù)將能夠進一步獲過程將是隱含進行的,使用更為簡單。
取組件的其他接口指針,從而使用組件提供的各種服務(wù)。
最后,通過Release()函數(shù)釋放接口指針。如果使用的進程內(nèi)組件,在調(diào)用CoUninitialize()函數(shù)釋放COM庫資源之前,應(yīng)首先調(diào)用CoFreeUnusedLibraries()將其從內(nèi)存卸載。由于在CoCreateInstance()函數(shù)內(nèi)部實現(xiàn)了對CoGetClassObject()的調(diào)用并一直完成了類廠對象接口函數(shù)對組件的創(chuàng)建和類廠對象的釋放

圖2 接口模型
抽象基類本身由于沒有實體函數(shù)與變量,所以并不分配內(nèi)存。通常只是用來為派生類指定內(nèi)存結(jié)構(gòu)。只有在派生類實現(xiàn)此抽象基類時,指定的內(nèi)存才會被分配。圖3為此內(nèi)存結(jié)構(gòu)的示意:

圖3 抽象基類定義的內(nèi)存結(jié)構(gòu)示意
圖中vtable為虛擬函數(shù)表,能夠為實例數(shù)據(jù)的提供一個方便保存的位置,并能夠在同一類的多個實例間共享。在每個實例的內(nèi)存映射中均包含一個指向該類的vtable表的指針pVtable。pVtable指針存放于所有數(shù)據(jù)成員之前,由于每個虛函數(shù)在vtable表中有唯一的索引,編譯器只需根據(jù)索引從vtable表中找到函數(shù)地址即可。也就是說,客戶只要獲取得到了接口指針,就可以使用此COM對象的實際功能。
由抽象基類指定的內(nèi)存結(jié)構(gòu)是符合COM規(guī)范的,因此抽象基類IX可以認為是一個COM接口,但這還不是一個嚴格意義上的COM接口。對于一個真正意義上的COM接口,在設(shè)計時應(yīng)遵循以下幾個規(guī)則:
1) 接口必須直接或間接地從IUnknown繼承。
2) 接口必須具有唯一的標識(IID)。
3) 一旦分配和公布了IID,有關(guān)接口定義的任何因素都不能被改變。
4) 接口成員函數(shù)應(yīng)具有HRESULT類型的返回值。
5) 接口成員函數(shù)的字符串參數(shù)應(yīng)采用Unicode類型。
這幾條規(guī)則中,最基本的是第一條,如果一個對象沒有至少實現(xiàn)一個最小程度為IUnknown的接口,那么該對象也就不是一個嚴格的COM對象。IUnknown接口是COM的核心接口,從上述規(guī)則可以得知,任何一個COM接口都必須從IUnknown接口繼承??蛻粼诮M件之間的通信是通過接口來實現(xiàn)的。組件可以不提供其他接口,但是必須提供IUnknown接口以使客戶能夠?qū)M件其他接口進行查詢。
IUnknown接口提供有成員函數(shù)QueryInterface()、AddRef()和Release(),分別用于查詢組件中的其他接口和進行生存期控制。由于任何COM接口都是從IUnknown接口派生,因此在所有COM接口虛擬函數(shù)表中保存的前三個成員函數(shù)指針一定是指向QueryInterface()、AddRef()和Release()的指針。這樣,任何一個COM接口都可以被當作IUnknown接口來處理。在創(chuàng)建組件時,客戶可以通過CreateInstance()函數(shù)得到IUnknown接口指針。
COM規(guī)范允許使用多接口,QueryInterface()成員函數(shù)可以用來查詢組件是否支持某個特定的接口。如果支持,QueryInterface()將返回此接口的指針。其第一個參數(shù)為一個IID結(jié)構(gòu),指出了客戶所要查詢的接口,查詢到的接口指針將存放在ppv所指向的變量中。函數(shù)的成功執(zhí)行與否將返回S_OK或E_NOINTERFACE。但是,在使用時不能簡單的將QueryInterface()返回值與其進行比較,而應(yīng)使用SUCCEEDED或FAILED宏。例如:
IUnknow* pI = CreateInstance(); IX* pIX = NULL; HRESULT hResult = pI->QueryInterface(IID_IX, (void**)&pIX); if (SUCCEEDED(hResult)) pIX->Func1(); |
由于QueryInterface()過于靈活,為避免由此引發(fā)的沖突在COM規(guī)范中定義了QueryInterface()所有實現(xiàn)都必須遵循的一些規(guī)則:
1) 過同一對象各個接口指針所查詢得到的IUnknown接口指針必須是指向同一個IUnknown接口的。即,IUnknow接口的唯一性。
2) 如果某接口曾經(jīng)被成功查詢過,那么此后任何時間對該接口的查詢也必定會成功。即,接口與查詢時間的無關(guān)性。
3) 對于已經(jīng)獲取到的接口仍可對其進行再次查詢,并且必定會成功。即,接口的自反性。
4) 客戶能夠從任何接口查詢到另外一個接口,而且能夠返回到起始接口。即,接口的對稱性。
5) 如果能夠從某接口獲取到某特定接口,那么從任意接口都可以得到此接口。即,接口的傳遞性。
IUnknown接口的另兩個成員函數(shù)AddRef()和Release()對對象的生存期進行了控制。每個COM對象都記錄有一個引用計數(shù),該引用計數(shù)表示了當前引用了此COM對象的有效指針的個數(shù)。AddRef()和Release()實現(xiàn)的即是這種引用計數(shù)的內(nèi)存管理技術(shù):引用計數(shù)初始為0,客戶每得到一個指向此對象的接口指針即通過AddRef()將引用計數(shù)加1;在每用完此接口指針后,調(diào)用Release()函數(shù)將引用計數(shù)減1。如果引用計數(shù)減到0,則從內(nèi)存卸載掉此COM對象。關(guān)于引用計數(shù)的使用,在COM規(guī)范中也設(shè)置了以下幾條簡單的規(guī)則:
1) 任何能夠返回接口指針的函數(shù)(如QreryInterface()、CreateInstance()等)在返回接口指針之前,必須用相應(yīng)的指針調(diào)用AddRef()函數(shù)。
2) 在使用完任何一個接口后,應(yīng)及時調(diào)用該接口的Release()函數(shù)。
3) 在進行接口指針賦值操作后,應(yīng)調(diào)用AddRef()函數(shù)。
COM組件的創(chuàng)建可以通過CoCreateInstance()函數(shù)來完成,函數(shù)原型為:
HRESULT __stdcall CoCreateInstace( const CLSID& clsid, IUnknown* pIUnknownOuter, DWORD dwClsContext, const IID& iid, void** ppv ); |
函數(shù)參數(shù)clsid是要創(chuàng)建組件的CLSID,pIUnknownOuter用于聚合組件,如果不使用可以設(shè)置為NULL。參數(shù)dwClsContext則限定了所創(chuàng)建組件的執(zhí)行上下文。最后兩個參數(shù)iid和ppv則分別為要使用接口的IID和返回得到的接口指針。在使用時只需將CLSID、IID等作為參數(shù)傳入即可創(chuàng)建相應(yīng)的組件并從輸出參數(shù)ppv得到所請求接口的指針。如果函數(shù)是直接創(chuàng)建組件的,那么在函數(shù)返回時組件將創(chuàng)建完畢,這樣客戶將無法對組件的創(chuàng)建過程進行任何干預(yù),靈活性太差。因此,CoCreateInstance()在函數(shù)內(nèi)部實現(xiàn)中通過調(diào)用CoGetClassObject()函數(shù)先創(chuàng)建一種專門用來創(chuàng)建組件的組件來解決此問題。這種用途的組件被稱為類廠(class factory)。
類廠所支持的用以創(chuàng)建組件的接口是IClassFactory,該接口從IUnknown派生,并具有兩個自己的接口成員函數(shù)CreateInstance()和LockServer()。這兩個成員函數(shù)分別用于創(chuàng)建COM組件對象和控制組件的生存期。下面先給出CreateInstance()的函數(shù)聲明:
HRESULT __stdcall CreateInstance(IUnknown* pIUnknownOuter, const IID& iid, void** ppv); |
可以看出,這個用于創(chuàng)建組件對象的CreateInstance()函數(shù)并未包含一個用來接受CLSID的參數(shù),顯然該函數(shù)將只能創(chuàng)建同某個CLSID相應(yīng)的組件。對于一個類廠,由于只能通過CreateInstance()函數(shù)去創(chuàng)建組件,因此只能創(chuàng)建與某個特定CLSID相應(yīng)的組件。
創(chuàng)建類廠的CoGetClassObject()函數(shù)將接收一個CLSID作為參數(shù)并返回指向類廠對象IClassFactory接口的指針??蛻魧⒖梢酝ㄟ^此指針來創(chuàng)建所需要的組件并返回某接口的指針。通過此指針,客戶將可以直接調(diào)用新創(chuàng)建的COM對象接口的成員函數(shù),從而獲得COM對象的所有服務(wù)。
在用CoGetClassObject()創(chuàng)建類廠對象時,如果COM對象是進程內(nèi)組件(組件與客戶處于同一進程地址空間,通常多以DLL形式存在),CoGetClassObject()將調(diào)用DLL模塊的DllGetClassObject()引出函數(shù)并把clsid、iid和ppv等參數(shù)傳遞進去以創(chuàng)建類廠,并返回類廠對象的接口指針。
如果COM對象是進程外組件(擁有獨立的進程地址空間,通常多以EXE形式存在),則CoGetClassObject()將要首先啟動組件進程,并一直等待到組件進程通過CoRegisterClassObject()函數(shù)將類廠注冊到COM后,才會返回COM中相應(yīng)的類廠信息。一旦組件進程退出,此注冊的類廠對象也就不再有效,需調(diào)用CoRevokeClassObject()函數(shù)予以通知。圖4展示了通過類廠創(chuàng)建組件的過程:

圖4 組件的創(chuàng)建過程
客戶程序?qū)OM組件的調(diào)用主要分對進程內(nèi)組件調(diào)用和進程外調(diào)用兩種情況。在具體過程上卻并沒有什么太大的區(qū)別。為了能夠使用COM庫提供的API函數(shù),首先要用CoInitialize()初始化COM庫。
雖然通過CLSID和ProgID都可以標識一個組件,但ProgID顯然要比CLSID更易于理解和使用,因此通常很少直接使用CLSID,而是通過使用CLSIDFromProgID(),根據(jù)ProgID得到組件的CLSID。進而以此返回的CLSID作為參數(shù)去調(diào)用CoGetClassObject()以創(chuàng)建類廠對象并返回類廠接口指針。通過該指針調(diào)用類廠對象的CreateInstance()接口成員函數(shù),執(zhí)行結(jié)果將創(chuàng)建與CLSID相應(yīng)的組件對象并返回IUnknown接口指針。通過此接口的QueryInterface()成員函數(shù)將能夠進一步獲過程將是隱含進行的,使用更為簡單。
取組件的其他接口指針,從而使用組件提供的各種服務(wù)。
最后,通過Release()函數(shù)釋放接口指針。如果使用的進程內(nèi)組件,在調(diào)用CoUninitialize()函數(shù)釋放COM庫資源之前,應(yīng)首先調(diào)用CoFreeUnusedLibraries()將其從內(nèi)存卸載。由于在CoCreateInstance()函數(shù)內(nèi)部實現(xiàn)了對CoGetClassObject()的調(diào)用并一直完成了類廠對象接口函數(shù)對組件的創(chuàng)建和類廠對象的釋放