物件導向的思想難以適應這種分布式軟體模型,於是組件化程式設計思想得到了迅速的發展。
按照組件化的程式設計的思想,複雜的應用程式被設計成一些小的,功能單一的組件模組,這些組件模組可以運行在同一台機器上,也可以運行在不同的機器上。
為了實現這樣的應用軟體,組建程式和組建程式之間需要一些極為細緻的規範, 只有組件程式遵守了這些共同的規範,然間系統才能正常運行。
為此,OMG和Microsoft分別提出了CORBA(Common Object Request Breaker Architecture)和COM(Component Object model)標準,目前CORBA模型主要應用於UNIX作業系統平台上,而COM 則主要應用於Microsoft Windows作業系統平台上。
在COM標準中,一個組件程式也被稱為一個模組,它可以是一個動態串連庫(DLL), 被稱為進程內組件(in-of-process component)也可以是一個可執行程式(EXE),被稱為進程外組件(out-of-process component).
COM對象是建立在二進位可執行代碼級的基礎上,而C++等語言中的對象是建立在原始碼級基礎上的,因此COM對象是語言無關的。這一特性使用不同程式設計語言開發的組件對象進行互動成為可能。
在Microsoft Windows系統平台上,COM技術被應用於系統的各個層次,從底層的COM對象管理到上層的應用程式互動都用到了COM標準。
概述
COM既提出了組件之間進行互動的規範,也提供了實現互動的環境, 因為組件對象之間互動的規範不依賴於任何特定的語言,所以COM也可以是不同語言協作開發的一種標準。
OLE技術以COM規範為基礎,OLE充分發揮了COM標準的優勢,使Windows作業系統上的應用程式具有極強的可互動性。如果沒有OLE的支援,Windows作業系統則會遜色很多。
但是,COM規範並不局限於OLE技術,實際上,OLE技術只是COM的一個應用而已,這幾年,OLE技術在進行網路互連是顯示出了很大的局限性,而COM則表現出了極強的適應能力。
COM標準包括規範和實現兩大部分,規範部分定義了組件和組件之間通訊的機制,這些規範不依賴於任何特定的語言和作業系統,只要按照該規範,任何語言都可以使用; COM標準的實現部分是COM庫,COM庫為COM規範的具體實現提供了一些核心服務。
COM是面對對象的軟體模型,因而對象是他的基本要素之一。類似於C++中對象的概念,對象是某個類(class)的一個執行個體;而類則是一組相關的資料和功能組和在一起的一個定義。使用對象的應用(或另一個對象)成為客戶,有時也成為對象的使用者。
介面是一組邏輯上相關的函數集合,其函數也被稱為介面成員函數。對象通過介面成員函數為客戶提供各種形式的服務。
在COM模型中,對象本身對於客戶來說是不可見的,客戶請求服務時,只能通過介面進行。每一個介面都由一個128位的通用唯一識別碼 (GUID,Globally Unique Identifier)來標識。客戶通過GUID獲得介面的指標,在通過介面指標,客戶就可以調用其相應的成員函數。
一般來說,介面是不變的,只要客戶期望的介面在組建對象中還存在,它就可以繼續使用該介面所提供的服務。對象可以支援多個介面,因此對組件對象的升級可通過增加介面的辦法實現,這樣得到的新介面可以不影響老介面的使用。
客戶如何來標識COM對象呢?與介面類似,每個對象也用一個128位GUID來標識,稱為CLSID(class identifier,類別識別項或類ID),用CLSID標識對象可以保證(機率意義上)在全球範圍內的唯一性。
只要系統中含有這類COM對象的資訊,並包括COM對象所在的模組檔案(DLL或EXE檔案)以及COM對象在代碼中的進入點,客戶程式就可以由此CLSID來建立COM對象。
那麼客戶怎麼使用COM對象所提供的服務呢?客戶獲得的又是什麼呢?
實際上,客戶成功建立對象後,它得到的是一個指向對象某個介面的指標,因為COM對象至少實現一個介面,所以客戶就可以調用該介面提供的所有服務。
但是COM對象可以有自己的狀態,正是這種狀態才使客戶感覺到COM對象的存在。如果客戶同時擁有兩個相同CLSID的對象,則兩個對象可以有不同的狀態,客戶完全不必關心COM對象是怎麼實現的,以及兩個對象的狀態資料之間有什麼關係(數組或者鏈表)。當然,COM對象也可以是無狀態的,這種COM對象以提供功能服務為主,可以用來代替傳統的API函數介面,使得API更為有序,組織層次性更強。
COM本身除了規範之外,也有實現的部分,其中包括一些核心的系統級代碼,也正是這部分核心代碼,才使得對象和客戶之間可通過介面在二進位代碼級進行互動 。
在Microsoft Windows作業系統環境下,這些庫以.dll檔案的形勢存在,其中包括以下內容:
(1) 提供了少量的API函數實現客戶和服務端COM應用的建立過程。在用戶端,主要是一些建立函數;而在伺服器端,提供一些對象的訪問支援。
(2) COM通過註冊表尋找本機伺服器即EXE程式,以及程式名與CLSID的轉換等。
(3) 提供了一些標準的記憶體控制方法,使應用控制進程中記憶體的分配。
COM庫一般不在應用程式層實現,而在作業系統層次上實現,因此一個作業系統只有一個COM庫實現。而且COM庫的實現必須依賴於具體的系統平台,尤其是系統底層的一些標準。
COM庫可以保證所有的組件按照統一的方式進行互動操作,而且它使我們在編寫COM應用時,可以不用編寫為進行COM通訊而必需的大量基礎代碼,而是直接利用COM庫提供的API進行編程,從而大大加快了開發的速度。例如,現在COM庫的版本都支援遠程組件即分布式COM,我們不用編寫任何網路或者 RPC(remote procedure call)的代碼,就可以實現在網路上進行程式之間的通訊。
如果我們用物件導向語言來實現COM對象,則很自然可以用類類定義對象。在C語言中,對象的概念可能變成一個邏輯概念,如果兩個對象同時存在,則在介面實現中必須明確知道所進行的操作是針對哪個對象的,這個過程可由COM介面的定義保證。
COM規範使用GUID來標識COM對象的思想源於OSF(Open Software Foundation)採用的UUID(Universallz Unique Identifier), UUID被定義為DCE(Distributed Computing Environment)的一部分,主要用於表識RPC通訊的雙方。
除了封裝性和重用性,C++對象還有一個重要特性是多態性。正是C++對象的多態性,才體現了C++語言用類描述事物的高度抽象的特徵; COM對象也具有多態性,但這種多態性需要通過COM對象所具有的介面才能體現出來,就像C++對象的多態性需要通過其(virtual)函數才能體現一樣。
從API到COM介面
假如我們要實現一個文書處理應用系統,它需要一個查字典的功能,按照組件化程式設計的方法,自然應該把查字典的功能放到一個組件(.dll)程式中實現。如果以後字典程式的尋找演算法或者字典庫改變了,只要應用程式和組件之間的介面不變,則新的組件程式仍然可以被應用系統使用。這就是採用組件程式帶來的靈活性。
為了把應用系統和組件程式串連起來,又能使它們協同工作,最簡單的做法就是先定義一組查字典的函數,而且這組函數儘可能一般化,不要加入特定的與字典庫相關的知識。
函數
功能說明
Initialize
初始化
LoadLibrary
裝入字典庫
InsertWord
插入一個單詞
DeleteWord
刪除一個單詞
LookupWord
尋找單詞
RestoreLibrary
把記憶體中的字典庫存入指定的檔案中
FreeLibrary
釋放字典庫
平面型的API介面層可以很好地把兩個程式串連起來,但存在以下一些問題:
(1) 當API函數非常多時,使用會非常不方便,需要對函數進行組織。
(2) API函數需要標準化,按照統一的調用方式進行處理,以適應不同的語言編程實現。參數的行程順序,參數類型,寒暑返回處理都需要標準化。
COM定義了一套完整的介面規範,不僅可以彌補以上API作為組件借口的不足,還充分發揮了組件對象的優勢,並實現了組件對象的多態性。
介面定義和標識
從技術上講,介面是包含了一組函數的資料結構,通過這組資料結構,客戶代碼可以調用組件對象的功能。介面定義了一群組成員函數,這群組成員函數是組件對象暴露出來的所有資訊,客戶程式利用這些函數或的組件對象的服務。
客戶程式用一個指向介面資料機構的指標來調用介面成員函數。介面指標實際上又指向另一個指標,這第二個指標指向一組函數,稱為介面函數表 (虛函數表),介面函數表中每一項為4個位元組長的函數指標,每個函數指標與對象的具體實現串連起來。通過這種方式,客戶只要獲得了介面指標,就可以調用到對象的實際功能。
對於一個介面來說,他的虛函數表vtable是確定的,因此介面的成員函數個數是不變的,而且成員函數的先後順序也是不變的;對於每個成員函數來說,其參數和傳回值也是確定的。
在一個介面的定義中,所有這些資訊都必須在二進位一級確定,不管什麼語言,只要能支援這樣的記憶體結構描述,也就是能夠支援 “structure“或“record“類型,並且這種類型能夠包含雙重的指向函數指標表的成員,則它就可以支援介面的描述,從而可以用於編寫COM組件或者使用COM組件。
介面描述語言IDL
COM規範在採用OSF的DCE規範描述遠程調用介面IDL的基礎上,進行擴充形成了COM介面的描述語言。
COM規範使用的IDL介面描述語言不僅可用於定義COM介面,同時還定義了一些常用的資料類型,也可以描述自訂的資料結構,對於介面成員函數,我們可以指定每個參數的類型,輸入輸出特性,甚至支援可變長度的數組的描述。IDL支援指標類型,與C/C++很類似。
Microsoft Visual C++提供了MIDL工具,可以把IDL介面描述檔案編譯成C/C++相容的介面描述標頭檔(.h)。
IUnknown的定義(IDL):
interface IUnknown
{
HRESULT QueryInterface([in] REFIID iid, [out] void **ppv);
ULONG AddRef(void);
ULONG Release(void);
}
IUnknown的定義(C++):
class IUnknown
{
Public:
virtual HRESULT _stdcall QueryInterface([in] REFIID iid, [out] void **ppv)=0;
virtual ULONG _stdcall AddRef(void)=0;
virtual ULONG _stdcall Release(void)=0;
}
進程內組件
因為進程內組件和客戶程式運行在同一個進程地址空間中,所以一旦客戶程式與組件程式建立起通訊關係之後,客戶程式得到的介面指標指向組件程式中介面的vtable,這個vtable包含了所有成員函數地址,客戶代碼可以直接調用這些成員函數,所以其效率非常高。
因為DLL程式是在運行時刻被客戶裝入到記憶體中的,所以DLL模組本身也是獨立的,它並不依賴於客戶程式。
在C++語言中,為了使編製的DLL程式更為通用,一般指定DLL的引出函數使用_stdcall調用習慣,如果使用了_cdecl調用習慣,則有些程式設計語言環境就不能使用這些DLL程式。C++編譯器為DLL程式的每個引出函數產生了一個修飾名,這些修飾名對於不同的編譯器並不相容,因此,從通用性角度出發,我們在每個函數定義前加上extern ?C“ 說明符。在Visual C++ 開發環境中,下面的說明語句可以很好的說明一個引出函數:
extern ? C“ int _stdcall MyFunction(int n);
為了編製DLL程式,我們可以按照這樣的步驟:
(1) 建立一個DLL工程
(2) 對每個引出函數,使用extern ? C“說明符,以及_stdcall修飾符,如上面對MyFunction函數的說明。
(3) 按照傳統的編程方法,我們還應該編寫一個DEF檔案,用來描述DLL程式的模組資訊。在Win32平台上,我們可以不使用DEF檔案,而是直接在函數說明時使用_declspec(dllexport)說明符,例如:
extern ? C“_declspec(dllexport) int _stdcall MyFunction(int n);
按照這樣的方法建立起來的DLL模組可以被其他程式調用,因為C++連接器會把所有引出函數的資訊串連到最終的目標代碼中。
從客戶程式一方來看,有三個系統函數可用於操作DLL程式,LoadLibrary, GetProcAddress, 和FreeLibrary。
一般地,對於DLL程式的使用過程按照這樣的步驟進行:
首先,客戶程式使用LoadLibrary函數裝入DLL,該函數返回模組的執行個體控制代碼,供以後操作該模組使用。
然後,客戶程式可以調用GetProcAddress函數獲得DLL中引出的函數的地址,我們既可以按函數的序號(在DEF檔案中指定)也可以按函數的名字來擷取引出函數的地址,因為客戶程式和DLL程式在相同的記憶體位址空間中,所以客戶程式可以直接調用這些引出函數。
最後FreeLibrary,把DLL程式卸出記憶體,以便釋放資源。
說明:
(1) DLL程式不僅可以引出函數,也可以引出全域變數,因為客戶程式和DLL程式在同一個地址空間,所以,把DLL中的全域變數引出到客戶程式中是有意義的。引用的方法並不複雜,或者把變數名放到DEF檔案的EXPORTS部分,並加上DATA選項; 或者在變數說明前面加上_declspec(dllexport)說明符。
(2) DumpBin 通過/Exports選項可以列出DLL程式中的所有被引出的資訊。
(3) 客戶程式本身也可以是一個DLL程式,但它一定先被裝入到進程空間中,以便可以調用系統函數操作作為服務程式的DLL模組。
進程外組件
因為進程外組件程式和客戶程式位於不同的進程空間之中,他們使用不同的地址空間,所以組件和客戶之間的通訊必須跨越進程邊界,這就涉及到以下一些問題:
(1) 一個進程如何調用另外一個進程中的函數
(2) 參數如何從一個進程被傳遞到另外一個進程中
Windows平台上,在不同進程之間進行通訊的辦法很多,包括DDE, named pipe,或者共用記憶體等等,COM採用了LPC(Local Procedure Call)和RPC(Remote Procedure Call)
RegEdit可檢查CLSID子鍵下的COM對象(63頁)
Microsoft Visual C++提供OleView.exe,可列出當前機器上的所有類別資訊,以及每一種類別下的組件對象列表。
RegSvr32 D:/DicComp/DictComp.dll
RegSvr32 /u D:/DicComp/DictComp.dll
DLL組件必須有DllRegisterServer和DllUnregisterServer兩個用於註冊的入口函數,才能用RegSvr32註冊。
COM規定,支援自註冊的進程外組件必須支援兩個命令列參數/RegServer和/UnregServer,以便完成註冊或登出操作。
Class Factory
實際上,客戶程式並不直接調用組件程式的引出函數,它調用COM庫的函數進行組件對象的建立工作,COM庫的建立函數根據註冊表的資訊調用組件程式的入口函數來建立組件對象。組件程式需要提供一個標準的入口函數DLLGetObjectClass,用於提供本組程式的組件資訊。
Class Factory和DLLGetObjectClass函數
類廠是COM對象的生產基地,COM庫通過類廠建立COM對象; 對應每一個COM類,有一個類廠專門用於該COM類的對象創操作。類廠本身也是一個COM對象,它支援一個特殊的介面:IClassFactory,其定義如下:
Class IClassFactory : public IUnknown
{
virtual HRESULT _stdcall CreateInstance(IUnknown *pUnknownOuter, const IID& iid, void **ppv) = 0;
virtual HRESULT _stdcall LockServer(BOOL bLock) = 0;
};
介面IClassFactory有一個重要的成員函數CreateInstance,用於建立對應的COM對象。因為每個類廠之針對特定的COM類對象,所以CreateInstance成員函數知道該建立什麼樣的COM對象。在CreateInstance成員函數的參數中,第一個參數 pUnknownOuter用於對象被彙總的情形,沒有彙總設成NULL。IClassFactory的另一個成員函數LockServer用於控制組建的生存周期。
因為類廠本身也是個COM對象,它被用於其它COM對象的建立過程,那麼類廠對象又由誰來建立呢?答案是DLLGetClassObject引出函數。DLLGetClassObject函數並不是COM庫的函數,而是由組件程式實現的引出函數,我們先看一下DLLGetClassObject函數的原型:
HRESULT DLLGetClassObject(const CLSID& clsid,
Const IID& iid,
(void **) ppv
);
COM庫在接到對象建立的指令後,它要調進程內組件的DLLGetClassObject函數,由該函數創類廠對象,並返回類廠對象的介面指標, COM庫或者客戶一旦有了類廠的介面指標,它們就可以通過類廠介面IClassFactory的成員函數CreateInstance建立相應的COM對象。
COM庫與類廠的互動(67頁)
在COM庫中,有三個API函數可用於對象的建立,它們分別是CoGetClassObject, CoCreateInstance和CoCreateInstanceEx。通常情況下,客戶程式調用其中之一完成對象的建立,並返回對象的初始介面指標。COM庫與類廠也通過這三個函數進行互動。