COM/DCOM綜述
COM/DCOM綜述
1. 分佈式組件對象模型
多少年來軟件的開發過程並沒有很大的改變,軟件開發過程中需要面對的主要問題如:開發週期長,難於確保程序的正確性,難於維護等還沒有得到非常好的解決,儘管出現瞭如面向對象,框架設計等等的概念和工具。組件對象模型是傳統面向對象模型的擴充,傳統面向對象模型的重點是源程序,以及系統分析和設計過程。組件的概念則強調大的軟件系統如何由不同開發商的小型可執行組件構成。以下首先從面向對象模型開始對各種概念作一番梳理。
· 面向對象編程
面向對象是一個被廣大編程人員和工業界認同已久的概念。面向對象程序設計語言讓開發者按照現實世界裏人們思考問題的模式來編寫程序,它讓開發者更好地用代碼直接表達現實中存在的對象,這樣開發代碼簡單並且易於維護。面嚮對象語言有以下三個最重要的概念:
封裝(Encapsulation)- 強調隱藏對象的實現細節,對象的使用者僅僅通過定義好的接口使用對象。
繼承(Inheritance)- 新的對象可以利用舊的對象的功能。
多態(Polymorphism)- 根據所使用的對象展現多種不同行爲的能力。
而從程序的整體來看,面向對象編程提供給用戶的最重要的概念則是代碼的共享與重用,它對於提高編寫程序的效率極爲重要。但是代碼的共享與重用一旦應用於實踐中仍然存在種種問題,如版本的升級、接口的變化、在不同程序設計語言之間共享代碼等等。對於這些困難原有的面向對象程序設計方法並沒有相應的答案,這就是組件對象模型提出的背景。
· 組件對象模型
將工程分解爲邏輯組件是面向組件分析和設計的基礎,這也是組件軟件的基礎。組件軟件系統由可重用的二進制形式的軟件組件模塊組成,只需要相當小的改動就可以將這些來自不同開發商的組件模塊組合在一起。特別重要的是這樣的組合並不需要源代碼,也不需要重新編譯,組件之間通過基於二進制的規範進行通訊,這被稱爲二進制重用。組件模塊是獨立於編程語言的,使用組件的客戶程序和組件之間除了通過標準的方法進行通訊以外,彼此不做任何限定。
組件可以劃分爲不同的類型,包括可視化組件如按鈕或者列表框;功能組件如打印或者拼寫檢查。例如一個基於組件的架構可以提供將多個開發商的拼寫檢查組件插入到另一個開發商的字處理應用程序中的機制,這樣用戶可以根據自己的喜好方便地選擇和替換字處理軟件。
組件結構中最重要的概念是接口。接口是集合在同一個名稱(通常是一個系統唯一的ID值)下的相關方法的的集合。組件之間的通訊是基於接口的,接口是組件和其客戶之間嚴格類型化的契約。實現相同接口的兩個對象就被認爲是多態的,這裏的多態不包含諸如基類指針指向派生類對象的意義,這裏是指同一個接口可以由多個對象以不同方法實現。
2. COM/DCOM的基本概念
· 概述:
以下將通過程序實例解釋COM/DCOM的基本概念。基於微軟的一貫作風,雖然COM/DCOM自稱爲是一個可跨平臺支持異構的模型(也確實從根本上說是可以跨平臺的),但它也是和Microsoft Windows系統中的其它概念緊密結合在一起的,而且除了Microsoft Windows系統以外很少有什麼系統支持COM/DCOM,所以在以下概念的介紹中將基於Microsoft Windows系統。COM/DCOM模型主要包括三方面的內容:(A)程序編寫的模式。(B)程序交互時遵循的二進制規範。(C)程序運行的輔助環境。首先通過圖1描述COM/DCOM基本機制。
由圖可見COM/DCOM是基於客戶機和服務器模型的,客戶程序和組件程序是相對的,進行功能請求調用的是客戶程序而響應該請求的是組件程序。組件程序也可以作爲客戶程序去調用其它的組件程序,正是這種角色的轉換和相互調用關係使組件程序最終構成一個軟件系統。根據COM/DCOM中客戶程序和組件程序的交互關係可以將組件分爲進程內組件和進程外組件兩種。所謂進程內組件是指客戶程序和組件程序在同一個進程地址空間內;進程外組件指客戶程序和組件程序分別處在不同的進程空間地址中。進程內組件是通過將組件作爲動態連接庫(DLL)來實現的,客戶程序將組件程序加載到自己的進程地址空間後再調用組件程序的函數。對於這兩種不同的組件,客戶程序和組件程序交互的內在方式是完全不同的。但是對於功能相同的進程內和進程外組件,從程序編寫的角度看,客戶程序是以同樣的方法來使用組件程序的,客戶程序不需要做任何的修改。因此以下先通過進程內組件的實現來理解COM/DCOM的編程模式。
· 進程內組件:
例子程序:
以下是一個用C++語言編寫的COM程序實例的主要內容:
頭文件:component.h
interface DECLSPEC_UUID("10000001-0000-0000-0000-000000000001")
ISum : public IUnknown
{public:
virtual HRESULT STDMETHODCALLTYPE Sum( int x, int y, int __RPC_FAR *retval) = 0;
};
客戶程序:
#include "component.h"
const CLSID CLSID_InsideCOM = {0x10000002,0x0000,0x0000,
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01}};
void main()
{
IUnknown* pUnknown;
ISum* pSum;
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
hr = CoCreateInstance(CLSID_InsideCOM, NULL,
CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pUnknown);
hr = pUnknown->QueryInterface(IID_ISum, (void**)&pSum);
if(FAILED(hr))
cout << "IID_ISum not supported. " << endl;
pUnknown->Release();
int sum;
hr = pSum->Sum(2, 3, &sum);
if(SUCCEEDED(hr))cout << "Client: Calling Sum(2, 3) = " << sum << endl;
pSum->Release();
CoUninitialize();
}
組件程序:
#include "component.h"
const CLSID CLSID_InsideCOM = {0x10000002,0x0000,0x0000,
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01}};
class CInsideCOM : public ISum{
public:
// IUnknown
ULONG __stdcall AddRef();
ULONG __stdcall Release();
HRESULT __stdcall QueryInterface(REFIID riid, void** ppv);
// ISum
HRESULT __stdcall Sum(int x, int y, int* retval);
CInsideCOM() : m_cRef(1) {}
private:
ULONG m_cRef;
};
ULONG CInsideCOM::AddRef()
{ return ++m_cRef; }
ULONG CInsideCOM::Release()
{ if(--m_cRef != 0) return m_cRef;
delete this;
return 0;
}
HRESULT CInsideCOM::QueryInterface(REFIID riid, void** ppv)
{
if(riid == IID_IUnknown)
{ *ppv = (IUnknown*)this; }
else if(riid == IID_ISum){
*ppv = (ISum*)this; }
else {
*ppv = NULL;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
HRESULT CInsideCOM::Sum(int x, int y, int* retval)
{ *retval = x + y;
return S_OK;
}
class CFactory : public IClassFactory
{
public:
// IUnknown
ULONG __stdcall AddRef();
ULONG __stdcall Release();
HRESULT __stdcall QueryInterface(REFIID riid, void** ppv);
// IClassFactory
HRESULT __stdcall CreateInstance(IUnknown *pUnknownOuter, REFIID riid, void** ppv);
CFactory() : m_cRef(1) { }
private:
ULONG m_cRef;
};
ULONG CFactory::AddRef()
{ return ++m_cRef; }
ULONG CFactory::Release()
{
if(--m_cRef != 0) return m_cRef;
delete this;
return 0;
}
HRESULT CFactory::QueryInterface(REFIID riid, void** ppv)
{
if(riid == IID_IUnknown)
{ *ppv = (IUnknown*)this; }
else if(riid == IID_IClassFactory)
{
*ppv = (IClassFactory*)this;
}
else{
*ppv = NULL;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
HRESULT CFactory::CreateInstance(IUnknown *pUnknownOuter, REFIID riid, void** ppv){
CInsideCOM *pInsideCOM = new CInsideCOM;
HRESULT hr = pInsideCOM->QueryInterface(riid, ppv);
return hr;
}
HRESULT __stdcall DllGetClassObject(REFCLSID clsid, REFIID riid, void** ppv){
if(clsid != CLSID_InsideCOM)
return CLASS_E_CLASSNOTAVAILABLE;
CFactory* pFactory = new CFactory;
if(pFactory == NULL)
return E_OUTOFMEMORY;
HRESULT hr = pFactory->QueryInterface(riid, ppv);
return hr;
}Top
由於COM/DCOM系統組件之間通訊是和位置無關的,也即一個使用組件功能的客戶程序在編寫時不需要考慮組件的位置,組件的定位和通訊由系統完成。因此不妨將客戶程序和組件程序分別加以分析。
客戶端程序:
(1) 調用CoInitializeEx初始化。
因爲程序的很多輔助功能是由庫函數和操作系統中的各種服務自動完成的,如組件的定位和加載,並且這些工作很複雜,所以程序需要首先作一些初始化。
(2)調用CoCreateInstance創建對象。
第1個參數CLSID_InsideCOM是一個128位的標識-類標識符(CLSID),在程序中定義爲 {0x10000002,0x0000, 0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01}},今後這樣128位的標識在表示時將省略0x並用"-"代替 ",";第4個參數IID_IUnknown也是一個128位的標識-接口標識符(IID);第5個參數(void**)&pUnknown是一個指針,在返回時它指向一個接口實例的指針。
CoCreateInstance()是一個庫函數,從語義上說它創建對應類標識(CLSID)的一個COM/DCOM對象實例,並獲得該對象的一個接口實例指針。對於進程內組件一個COM/DCOM對象是在一個DLL中實現的,在Windows系統註冊表中維護着CLSID和DLL文件路徑的對應關係,CoCreateInstance首先查找註冊表,然後加載對應的DLL程序,調用該DLL的DllGetClaseObject引出函數(任何作爲組件的DLL都必須提供該函數)以及其他的一些操作創建一個對象實例。
一個COM/DCOM對象對於客戶程序唯一可見的是它所包含的一組接口,每一個接口都由128位的IID標識,在整個COM/DCOM系統中都是唯一的(包括分佈在不同機器上的COM/DCOM系統)。任何類型的COM/DCOM對象都必須支持IID_IUknown(標識爲00000000-0000-0000-C000-000000000046)接口。不同語言編寫的客戶程序中基本都有某種機制來標識接口指針,COM/DCOM用對象的IID_IUnknown接口指針的值來區分對象而沒有獨立的對象引用,任何對象在生命期內返回給客戶程序的IID_IUnknown接口指針值必須是相同的。注意COM/DCOM對其他的接口指針值沒有如上的要求。
COM/DCOM規定IID_IUnknown接口由以下三個函數組成:
QueryInterface(const IID iid, void **ppv);
AddRef( );
Release( );
而且其他任何接口也必須包含這三個函數。其中AddRef和Release是用來控制對象生存週期的。 QueryInter- face則達到通過接口標識查詢對象實現的接口,COM/DCOM規定通過對象的任何接口的QueryInterface函數可以獲得同一個對象的其餘接口指針。
(3)pUnknown->QueryInterface(IID_ISum, (void**)&pSum);獲得接口標識符爲IID_Isum的另一個接口實例指針pSum。
(4)hr = pSum->Sum(2, 3, &sum);通過接口實例指針pSum,調用接口的成員函數Sum。從編程模式的角度來看客戶程序向組件程序發送功能請求在源程序中最終體現爲調用接口的一個成員函數,並且實際上不論對於進程內組件還是對於進程外組件都是同樣的。對於進程內組件這一調用就是通過調用同一個進程中的函數實現的,但是必須強調即使是進程內的調用遵循的仍然是二進制的規範,也即客戶程序中的pSum指向的內存格式必須滿足COM/DCOM的規範,至於這一規範是怎樣的將在後文中講述。對於一個確定的接口(確定值的IID),它的進程內組件不論用什麼程序設計語言實現,生成的目標DLL返回給客戶程序的接口指針所指向的內存格式都是一樣的。
(5)CoUninitialize();調用清理函數。
通過以上對客戶程序的分析,可見客戶程序的編程模式爲(a)創建對應CLSID的對象實例;(b)獲得對象的初始接口指針;(c)通過接口指針的QueryInterfase函數查詢其它接口指針;(d)通過接口指針調用接口的函數。(e)通過接口的AddRef()和Release()控制對象的生命期。客戶程序和進程內組件程序遵循的二進制規範則體現在(a)128位的類表識CLSID和接口表識IID;(b)組件程序必須是一個合法的DLL,並且引出若干標準的函數如DellGetClassObject。(c)組件程序返回給客戶程序的接口指針所指向的內存必須滿足COM/DCOM規範。
組件程序:
現在分析一下組件程序的編寫,瞭解對象是如何實現的。首先察看DllGetClassObject(const CLSID clsid, const IID id, (void **)ppv)函數,當客戶程序加載該DLL後將首先調用該引出函數。該函數是一個進程內組件提供其服務的最基本的入口,也是進程內組件所遵循的二進制規範的一部分。DllGetClassObject的功能是根據CLSID判斷本組件是否支持該類型的對象,一個組件可以支持多種類型的對象。DllGetClassObject根據CLSID生成對應的類廠對象,並根據輸入參數const IID id將類廠對象的對應接口指針通過ppv返回給客戶程序。這裏的引入了類廠這個在客戶程序中未提及的新概念。根據COM/DCOM規範,組件程序必須爲自己支持的每個CLSID提供類廠對象,由類廠對象負責創建對應類型的COM/DCOM對象實例。類廠對象提供通常稱爲IID_IClassFactory的接口(其值爲000001-0000-0000-C000-000000000046),客戶程序通過調用該接口的CreateInstance(Iunknown *pUnknown-Outer, const IID iid, void **ppv)函數真正創建對象實例並獲得對象的第一個接口指針。由此可見客戶程序調用CoCreateInstance庫函數實際完成了兩個步驟的工作,它首先請求組件創建類廠對象,然後又通過類廠對象創建對應CLSID的對象實例。
上面組件程序的實例中,在DllGetClassObject函數中通過new Cfactory創建了類廠對象,在CFactory:: CreateInstance 中通過new CInsideCOM創建類對象。COM/DCOM對象是以C++對象的形式實現的,接口指針是以C++中的對象指針的形式返回的。對於進程內組件,組件和客戶程序在編寫時是分別進行的,而在運行時處於同一個地址空間內又以指針的方式進行交互,那麼交互的二進制兼容自然是基於內存格式的。
在瞭解進程內組件的編寫後,讀者最大的疑惑必然是如何確保客戶程序和組件程序在彼此獨立的編寫的過程中(甚至使用不同的語言)如何確保二進制兼容的。如前所述對於進程內組件二進制兼容包含三個方面的內容,128位標識符的識別以及確保組件DLL程序的合法性是很容易做到的,而如何確保在基於內存的交互時接口指針所指向的內存格式符合規範則顯得有些複雜。下一節將介紹IDL語言,它是解決以上問題的重要手段。
3. IDL語言
在上面的例子程序中,不論是客戶程序還是組件程序都沒有使用任何的輔助手段就達到了COM/DCOM所要求的二進制的規範。不難想象:符合一定結構的一般C++程序經過編譯後生成的二進制代碼是滿足COM/DCOM二進制規範的。同樣不難想象:爲了達到符合COM/DCOM的二進制規範,一種簡單的方法就是對於生成目標代碼的源程序的格式作一定的限制,對於例子中的C++程序顯然只要對實現對象的C++類定義作限制就可以了。同時考慮到COM/DCOM是和編程語言無關的,使用C++的頭文件顯然是行不通的,因此必須使用一種獨立的語言來描述接口,微軟選用的語言就是IDL。
IDL語言是開放軟件基金會(OSF)爲分佈式計算環境RPC軟件包開發的,IDL幫助RPC程序員保證工程的客戶機和服務器都遵守同一接口。爲了將IDL語言應用於COM/DCOM系統中,微軟對IDL語言的語法進行了擴充。IDL本身不是一種編程語言,它是用來定義接口的一種工具,至於對IDL語言的解釋由使用它的系統決定。COM/DCOM對IDL語言的解釋和COM/DCOM的二進制規範密切相關,而這樣的解釋和其它利用IDL的系統毫無關係。
COM/DCOM通常並不直接將IDL語言定義的接口翻譯成二進制代碼。C++語言的用戶使用微軟提供的MIDL.EXE程序可將IDL語言翻譯成對應的C++頭文件,上面例子程序中的頭文件就是由以下的IDL文件生成的,接口ISum 繼承了接口IUnknown。接口定義文件精確地描述了接口所包含的函數,函數的參數及參數的類型。
import "unknwn.idl";
[ object, uuid(10000001-0000-0000-0000-000000000001) ]
interface ISum : IUnknown{
HRESULT Sum([in] int x, [in] int y, [out, retval] int* retval);
};
其中unknwn.idl是系統預定義的,其內容如下:
[local,
object,
uuid(00000000-0000-0000-C000-000000000046),
]
interface IUnknown{
HRESULT QueryInterface([in] REFIID riid, [out, iid_is(riid)] void **ppvObject);
ULONG AddRef();
ULONG Release();
}
由IDL生成的C++頭文件在客戶程序和組件程序中分別通過#include被包含。使用由MIDL.EXE翻譯而成的C++頭文件一方面確保客戶程序和組件程序中接口指針所指的內存結構一致,解決了同一編程語言實現的組件間的互操作性;另一方面也確保了接口指針所指的內存結構符合COM/DCOM的二進制規範,解決不同編程語言實現的組件間的互操作性。不過由於微軟的MIDL.EXE沒有通過IDL文件直接生成其它語言(如VB, JAVA)相應頭文件的功能,這些語言的用戶需要其它的工具才能利用IDL,這裏不作敘述。
4.組件對象的繼承
COM/DCOM模型作爲組件對象模型具有對象模型的基本特性,其中對象的封裝性,多態性前已做過論述。但對象模型的另一個重要特性---繼承性---還沒有涉及。COM/DCOM通過包容和聚合提供類似的特性。
包容和聚合有一個共同的特點就是對象包容和聚合後必須使客戶相信那是一個對象。如前所述,客戶程序區別對象的唯一標誌是對象的IID_IUnknown接口指針的值,而且通過同一對象的接口的QueryInterface函數必須能夠查詢到本對象的其它接口。圖2表示包容和聚合的實現,對象B實現接口IID_IB和IID_ISum,其中IID_Isum的功能是通過創建另一個CLSID_InsideCOM類型的對象完成的。
如圖所示當採用包容模式時,對象B簡單地創建一個CLSID_InsideCom對象,客戶程序所有對對象B接口IID_ISum的調用都可以利用CLSID_InsideCOM對象的IID_ISum接口完成。當採用聚合模式時,對象B仍然創建一個CLSID_InsideCom對象,但對象B自己並不實現Top
1. 分佈式組件對象模型
多少年來軟件的開發過程並沒有很大的改變,軟件開發過程中需要面對的主要問題如:開發週期長,難於確保程序的正確性,難於維護等還沒有得到非常好的解決,儘管出現瞭如面向對象,框架設計等等的概念和工具。組件對象模型是傳統面向對象模型的擴充,傳統面向對象模型的重點是源程序,以及系統分析和設計過程。組件的概念則強調大的軟件系統如何由不同開發商的小型可執行組件構成。以下首先從面向對象模型開始對各種概念作一番梳理。
· 面向對象編程
面向對象是一個被廣大編程人員和工業界認同已久的概念。面向對象程序設計語言讓開發者按照現實世界裏人們思考問題的模式來編寫程序,它讓開發者更好地用代碼直接表達現實中存在的對象,這樣開發代碼簡單並且易於維護。面嚮對象語言有以下三個最重要的概念:
封裝(Encapsulation)- 強調隱藏對象的實現細節,對象的使用者僅僅通過定義好的接口使用對象。
繼承(Inheritance)- 新的對象可以利用舊的對象的功能。
多態(Polymorphism)- 根據所使用的對象展現多種不同行爲的能力。
而從程序的整體來看,面向對象編程提供給用戶的最重要的概念則是代碼的共享與重用,它對於提高編寫程序的效率極爲重要。但是代碼的共享與重用一旦應用於實踐中仍然存在種種問題,如版本的升級、接口的變化、在不同程序設計語言之間共享代碼等等。對於這些困難原有的面向對象程序設計方法並沒有相應的答案,這就是組件對象模型提出的背景。
· 組件對象模型
將工程分解爲邏輯組件是面向組件分析和設計的基礎,這也是組件軟件的基礎。組件軟件系統由可重用的二進制形式的軟件組件模塊組成,只需要相當小的改動就可以將這些來自不同開發商的組件模塊組合在一起。特別重要的是這樣的組合並不需要源代碼,也不需要重新編譯,組件之間通過基於二進制的規範進行通訊,這被稱爲二進制重用。組件模塊是獨立於編程語言的,使用組件的客戶程序和組件之間除了通過標準的方法進行通訊以外,彼此不做任何限定。
組件可以劃分爲不同的類型,包括可視化組件如按鈕或者列表框;功能組件如打印或者拼寫檢查。例如一個基於組件的架構可以提供將多個開發商的拼寫檢查組件插入到另一個開發商的字處理應用程序中的機制,這樣用戶可以根據自己的喜好方便地選擇和替換字處理軟件。
組件結構中最重要的概念是接口。接口是集合在同一個名稱(通常是一個系統唯一的ID值)下的相關方法的的集合。組件之間的通訊是基於接口的,接口是組件和其客戶之間嚴格類型化的契約。實現相同接口的兩個對象就被認爲是多態的,這裏的多態不包含諸如基類指針指向派生類對象的意義,這裏是指同一個接口可以由多個對象以不同方法實現。
2. COM/DCOM的基本概念
· 概述:
以下將通過程序實例解釋COM/DCOM的基本概念。基於微軟的一貫作風,雖然COM/DCOM自稱爲是一個可跨平臺支持異構的模型(也確實從根本上說是可以跨平臺的),但它也是和Microsoft Windows系統中的其它概念緊密結合在一起的,而且除了Microsoft Windows系統以外很少有什麼系統支持COM/DCOM,所以在以下概念的介紹中將基於Microsoft Windows系統。COM/DCOM模型主要包括三方面的內容:(A)程序編寫的模式。(B)程序交互時遵循的二進制規範。(C)程序運行的輔助環境。首先通過圖1描述COM/DCOM基本機制。
由圖可見COM/DCOM是基於客戶機和服務器模型的,客戶程序和組件程序是相對的,進行功能請求調用的是客戶程序而響應該請求的是組件程序。組件程序也可以作爲客戶程序去調用其它的組件程序,正是這種角色的轉換和相互調用關係使組件程序最終構成一個軟件系統。根據COM/DCOM中客戶程序和組件程序的交互關係可以將組件分爲進程內組件和進程外組件兩種。所謂進程內組件是指客戶程序和組件程序在同一個進程地址空間內;進程外組件指客戶程序和組件程序分別處在不同的進程空間地址中。進程內組件是通過將組件作爲動態連接庫(DLL)來實現的,客戶程序將組件程序加載到自己的進程地址空間後再調用組件程序的函數。對於這兩種不同的組件,客戶程序和組件程序交互的內在方式是完全不同的。但是對於功能相同的進程內和進程外組件,從程序編寫的角度看,客戶程序是以同樣的方法來使用組件程序的,客戶程序不需要做任何的修改。因此以下先通過進程內組件的實現來理解COM/DCOM的編程模式。
· 進程內組件:
例子程序:
以下是一個用C++語言編寫的COM程序實例的主要內容:
頭文件:component.h
interface DECLSPEC_UUID("10000001-0000-0000-0000-000000000001")
ISum : public IUnknown
{public:
virtual HRESULT STDMETHODCALLTYPE Sum( int x, int y, int __RPC_FAR *retval) = 0;
};
客戶程序:
#include "component.h"
const CLSID CLSID_InsideCOM = {0x10000002,0x0000,0x0000,
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01}};
void main()
{
IUnknown* pUnknown;
ISum* pSum;
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
hr = CoCreateInstance(CLSID_InsideCOM, NULL,
CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pUnknown);
hr = pUnknown->QueryInterface(IID_ISum, (void**)&pSum);
if(FAILED(hr))
cout << "IID_ISum not supported. " << endl;
pUnknown->Release();
int sum;
hr = pSum->Sum(2, 3, &sum);
if(SUCCEEDED(hr))cout << "Client: Calling Sum(2, 3) = " << sum << endl;
pSum->Release();
CoUninitialize();
}
組件程序:
#include "component.h"
const CLSID CLSID_InsideCOM = {0x10000002,0x0000,0x0000,
{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01}};
class CInsideCOM : public ISum{
public:
// IUnknown
ULONG __stdcall AddRef();
ULONG __stdcall Release();
HRESULT __stdcall QueryInterface(REFIID riid, void** ppv);
// ISum
HRESULT __stdcall Sum(int x, int y, int* retval);
CInsideCOM() : m_cRef(1) {}
private:
ULONG m_cRef;
};
ULONG CInsideCOM::AddRef()
{ return ++m_cRef; }
ULONG CInsideCOM::Release()
{ if(--m_cRef != 0) return m_cRef;
delete this;
return 0;
}
HRESULT CInsideCOM::QueryInterface(REFIID riid, void** ppv)
{
if(riid == IID_IUnknown)
{ *ppv = (IUnknown*)this; }
else if(riid == IID_ISum){
*ppv = (ISum*)this; }
else {
*ppv = NULL;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
HRESULT CInsideCOM::Sum(int x, int y, int* retval)
{ *retval = x + y;
return S_OK;
}
class CFactory : public IClassFactory
{
public:
// IUnknown
ULONG __stdcall AddRef();
ULONG __stdcall Release();
HRESULT __stdcall QueryInterface(REFIID riid, void** ppv);
// IClassFactory
HRESULT __stdcall CreateInstance(IUnknown *pUnknownOuter, REFIID riid, void** ppv);
CFactory() : m_cRef(1) { }
private:
ULONG m_cRef;
};
ULONG CFactory::AddRef()
{ return ++m_cRef; }
ULONG CFactory::Release()
{
if(--m_cRef != 0) return m_cRef;
delete this;
return 0;
}
HRESULT CFactory::QueryInterface(REFIID riid, void** ppv)
{
if(riid == IID_IUnknown)
{ *ppv = (IUnknown*)this; }
else if(riid == IID_IClassFactory)
{
*ppv = (IClassFactory*)this;
}
else{
*ppv = NULL;
return E_NOINTERFACE;
}
AddRef();
return S_OK;
}
HRESULT CFactory::CreateInstance(IUnknown *pUnknownOuter, REFIID riid, void** ppv){
CInsideCOM *pInsideCOM = new CInsideCOM;
HRESULT hr = pInsideCOM->QueryInterface(riid, ppv);
return hr;
}
HRESULT __stdcall DllGetClassObject(REFCLSID clsid, REFIID riid, void** ppv){
if(clsid != CLSID_InsideCOM)
return CLASS_E_CLASSNOTAVAILABLE;
CFactory* pFactory = new CFactory;
if(pFactory == NULL)
return E_OUTOFMEMORY;
HRESULT hr = pFactory->QueryInterface(riid, ppv);
return hr;
}Top
由於COM/DCOM系統組件之間通訊是和位置無關的,也即一個使用組件功能的客戶程序在編寫時不需要考慮組件的位置,組件的定位和通訊由系統完成。因此不妨將客戶程序和組件程序分別加以分析。
客戶端程序:
(1) 調用CoInitializeEx初始化。
因爲程序的很多輔助功能是由庫函數和操作系統中的各種服務自動完成的,如組件的定位和加載,並且這些工作很複雜,所以程序需要首先作一些初始化。
(2)調用CoCreateInstance創建對象。
第1個參數CLSID_InsideCOM是一個128位的標識-類標識符(CLSID),在程序中定義爲 {0x10000002,0x0000, 0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01}},今後這樣128位的標識在表示時將省略0x並用"-"代替 ",";第4個參數IID_IUnknown也是一個128位的標識-接口標識符(IID);第5個參數(void**)&pUnknown是一個指針,在返回時它指向一個接口實例的指針。
CoCreateInstance()是一個庫函數,從語義上說它創建對應類標識(CLSID)的一個COM/DCOM對象實例,並獲得該對象的一個接口實例指針。對於進程內組件一個COM/DCOM對象是在一個DLL中實現的,在Windows系統註冊表中維護着CLSID和DLL文件路徑的對應關係,CoCreateInstance首先查找註冊表,然後加載對應的DLL程序,調用該DLL的DllGetClaseObject引出函數(任何作爲組件的DLL都必須提供該函數)以及其他的一些操作創建一個對象實例。
一個COM/DCOM對象對於客戶程序唯一可見的是它所包含的一組接口,每一個接口都由128位的IID標識,在整個COM/DCOM系統中都是唯一的(包括分佈在不同機器上的COM/DCOM系統)。任何類型的COM/DCOM對象都必須支持IID_IUknown(標識爲00000000-0000-0000-C000-000000000046)接口。不同語言編寫的客戶程序中基本都有某種機制來標識接口指針,COM/DCOM用對象的IID_IUnknown接口指針的值來區分對象而沒有獨立的對象引用,任何對象在生命期內返回給客戶程序的IID_IUnknown接口指針值必須是相同的。注意COM/DCOM對其他的接口指針值沒有如上的要求。
COM/DCOM規定IID_IUnknown接口由以下三個函數組成:
QueryInterface(const IID iid, void **ppv);
AddRef( );
Release( );
而且其他任何接口也必須包含這三個函數。其中AddRef和Release是用來控制對象生存週期的。 QueryInter- face則達到通過接口標識查詢對象實現的接口,COM/DCOM規定通過對象的任何接口的QueryInterface函數可以獲得同一個對象的其餘接口指針。
(3)pUnknown->QueryInterface(IID_ISum, (void**)&pSum);獲得接口標識符爲IID_Isum的另一個接口實例指針pSum。
(4)hr = pSum->Sum(2, 3, &sum);通過接口實例指針pSum,調用接口的成員函數Sum。從編程模式的角度來看客戶程序向組件程序發送功能請求在源程序中最終體現爲調用接口的一個成員函數,並且實際上不論對於進程內組件還是對於進程外組件都是同樣的。對於進程內組件這一調用就是通過調用同一個進程中的函數實現的,但是必須強調即使是進程內的調用遵循的仍然是二進制的規範,也即客戶程序中的pSum指向的內存格式必須滿足COM/DCOM的規範,至於這一規範是怎樣的將在後文中講述。對於一個確定的接口(確定值的IID),它的進程內組件不論用什麼程序設計語言實現,生成的目標DLL返回給客戶程序的接口指針所指向的內存格式都是一樣的。
(5)CoUninitialize();調用清理函數。
通過以上對客戶程序的分析,可見客戶程序的編程模式爲(a)創建對應CLSID的對象實例;(b)獲得對象的初始接口指針;(c)通過接口指針的QueryInterfase函數查詢其它接口指針;(d)通過接口指針調用接口的函數。(e)通過接口的AddRef()和Release()控制對象的生命期。客戶程序和進程內組件程序遵循的二進制規範則體現在(a)128位的類表識CLSID和接口表識IID;(b)組件程序必須是一個合法的DLL,並且引出若干標準的函數如DellGetClassObject。(c)組件程序返回給客戶程序的接口指針所指向的內存必須滿足COM/DCOM規範。
組件程序:
現在分析一下組件程序的編寫,瞭解對象是如何實現的。首先察看DllGetClassObject(const CLSID clsid, const IID id, (void **)ppv)函數,當客戶程序加載該DLL後將首先調用該引出函數。該函數是一個進程內組件提供其服務的最基本的入口,也是進程內組件所遵循的二進制規範的一部分。DllGetClassObject的功能是根據CLSID判斷本組件是否支持該類型的對象,一個組件可以支持多種類型的對象。DllGetClassObject根據CLSID生成對應的類廠對象,並根據輸入參數const IID id將類廠對象的對應接口指針通過ppv返回給客戶程序。這裏的引入了類廠這個在客戶程序中未提及的新概念。根據COM/DCOM規範,組件程序必須爲自己支持的每個CLSID提供類廠對象,由類廠對象負責創建對應類型的COM/DCOM對象實例。類廠對象提供通常稱爲IID_IClassFactory的接口(其值爲000001-0000-0000-C000-000000000046),客戶程序通過調用該接口的CreateInstance(Iunknown *pUnknown-Outer, const IID iid, void **ppv)函數真正創建對象實例並獲得對象的第一個接口指針。由此可見客戶程序調用CoCreateInstance庫函數實際完成了兩個步驟的工作,它首先請求組件創建類廠對象,然後又通過類廠對象創建對應CLSID的對象實例。
上面組件程序的實例中,在DllGetClassObject函數中通過new Cfactory創建了類廠對象,在CFactory:: CreateInstance 中通過new CInsideCOM創建類對象。COM/DCOM對象是以C++對象的形式實現的,接口指針是以C++中的對象指針的形式返回的。對於進程內組件,組件和客戶程序在編寫時是分別進行的,而在運行時處於同一個地址空間內又以指針的方式進行交互,那麼交互的二進制兼容自然是基於內存格式的。
在瞭解進程內組件的編寫後,讀者最大的疑惑必然是如何確保客戶程序和組件程序在彼此獨立的編寫的過程中(甚至使用不同的語言)如何確保二進制兼容的。如前所述對於進程內組件二進制兼容包含三個方面的內容,128位標識符的識別以及確保組件DLL程序的合法性是很容易做到的,而如何確保在基於內存的交互時接口指針所指向的內存格式符合規範則顯得有些複雜。下一節將介紹IDL語言,它是解決以上問題的重要手段。
3. IDL語言
在上面的例子程序中,不論是客戶程序還是組件程序都沒有使用任何的輔助手段就達到了COM/DCOM所要求的二進制的規範。不難想象:符合一定結構的一般C++程序經過編譯後生成的二進制代碼是滿足COM/DCOM二進制規範的。同樣不難想象:爲了達到符合COM/DCOM的二進制規範,一種簡單的方法就是對於生成目標代碼的源程序的格式作一定的限制,對於例子中的C++程序顯然只要對實現對象的C++類定義作限制就可以了。同時考慮到COM/DCOM是和編程語言無關的,使用C++的頭文件顯然是行不通的,因此必須使用一種獨立的語言來描述接口,微軟選用的語言就是IDL。
IDL語言是開放軟件基金會(OSF)爲分佈式計算環境RPC軟件包開發的,IDL幫助RPC程序員保證工程的客戶機和服務器都遵守同一接口。爲了將IDL語言應用於COM/DCOM系統中,微軟對IDL語言的語法進行了擴充。IDL本身不是一種編程語言,它是用來定義接口的一種工具,至於對IDL語言的解釋由使用它的系統決定。COM/DCOM對IDL語言的解釋和COM/DCOM的二進制規範密切相關,而這樣的解釋和其它利用IDL的系統毫無關係。
COM/DCOM通常並不直接將IDL語言定義的接口翻譯成二進制代碼。C++語言的用戶使用微軟提供的MIDL.EXE程序可將IDL語言翻譯成對應的C++頭文件,上面例子程序中的頭文件就是由以下的IDL文件生成的,接口ISum 繼承了接口IUnknown。接口定義文件精確地描述了接口所包含的函數,函數的參數及參數的類型。
import "unknwn.idl";
[ object, uuid(10000001-0000-0000-0000-000000000001) ]
interface ISum : IUnknown{
HRESULT Sum([in] int x, [in] int y, [out, retval] int* retval);
};
其中unknwn.idl是系統預定義的,其內容如下:
[local,
object,
uuid(00000000-0000-0000-C000-000000000046),
]
interface IUnknown{
HRESULT QueryInterface([in] REFIID riid, [out, iid_is(riid)] void **ppvObject);
ULONG AddRef();
ULONG Release();
}
由IDL生成的C++頭文件在客戶程序和組件程序中分別通過#include被包含。使用由MIDL.EXE翻譯而成的C++頭文件一方面確保客戶程序和組件程序中接口指針所指的內存結構一致,解決了同一編程語言實現的組件間的互操作性;另一方面也確保了接口指針所指的內存結構符合COM/DCOM的二進制規範,解決不同編程語言實現的組件間的互操作性。不過由於微軟的MIDL.EXE沒有通過IDL文件直接生成其它語言(如VB, JAVA)相應頭文件的功能,這些語言的用戶需要其它的工具才能利用IDL,這裏不作敘述。
4.組件對象的繼承
COM/DCOM模型作爲組件對象模型具有對象模型的基本特性,其中對象的封裝性,多態性前已做過論述。但對象模型的另一個重要特性---繼承性---還沒有涉及。COM/DCOM通過包容和聚合提供類似的特性。
包容和聚合有一個共同的特點就是對象包容和聚合後必須使客戶相信那是一個對象。如前所述,客戶程序區別對象的唯一標誌是對象的IID_IUnknown接口指針的值,而且通過同一對象的接口的QueryInterface函數必須能夠查詢到本對象的其它接口。圖2表示包容和聚合的實現,對象B實現接口IID_IB和IID_ISum,其中IID_Isum的功能是通過創建另一個CLSID_InsideCOM類型的對象完成的。
如圖所示當採用包容模式時,對象B簡單地創建一個CLSID_InsideCom對象,客戶程序所有對對象B接口IID_ISum的調用都可以利用CLSID_InsideCOM對象的IID_ISum接口完成。當採用聚合模式時,對象B仍然創建一個CLSID_InsideCom對象,但對象B自己並不實現Top
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.