| Feed:大富翁筆記 Title: 《COM 原理與應用》學習筆記 | Author: fgs_fgs Comments |
| 《COM 原理與應用》學習筆記- 第一部分COM原理 http://savetime.delphibbs.com 開始時間:2004.1.30 最後修改:2004.2.1 本文排版格式為: 正文由窗口自動換行;所有代碼以80 字符為邊界;中英文字符以空格符分隔。 (本文內容基本上是從《COM 原理與應用》書中摘錄,版權由作者潘愛民所有,請勿在公共媒體使用) 目錄 ================================================== ============================= ⊙ 第一章概述 COM 是什麼 COM 對象與接口 COM 進程模型 COM 可重用性 ⊙ 第二章COM 對像模型 全局唯一標識符GUID COM 對象 COM 接口 接口描述語言IDL IUnknown 接口 COM 對象的接口原則 ⊙ 第三章COM 的實現 COM 組件註冊信息 註冊COM 組件 類廠和DllGetObjectClass 函數 CoGetClassObject 函數 CoCreateInstance / CoCreateInstanceEx 函數 COM 庫的初始化 COM 庫的內存管理 組件程序的裝載和卸載 COM 庫常用函數 HRESULT 類型 ⊙ 第四章COM 特性 可重用性:包容和聚合 進程透明性(待學) 安全性(待學) 多線程特性(待學) ⊙ 第五章用Visual C++ 開發COM 應用 Win32 SDK 提供的一些頭文件的說明 與COM 接口有關的一些宏 ================================================== ============================= 正文 ================================================== ============================= ⊙ 第一章概述 ================================================== ============================= COM 是什麼 -------------------------------------------------- ----------------------------- COM 是由Microsoft 提出的組件標準,它不僅定義了組件程序之間進行交互的標準,並且也提供了組件程序運行所需的環境。在COM 標準中,一個組件程序也被稱為一個模塊,它可以是一個動態鏈接庫,被稱為進程內組件(in-process component);也可以是一個可執行程序(即EXE 程序),被稱作進程外組件(out-of-process component)。一個組件程序可以包含一個或多個組件對象,因為COM 是以對象為基本單元的模型,所以在程序與程序之間進行通信時,通信的雙方應該是組件對象,也叫做COM 對象,而組件程序(或稱作COM 程序)是提供COM 對象的代碼載體。 COM 對像不同於一般面向對象語言(如C++ 語言)中的對象概念,COM 對像是建立在二進制可執行代碼級的基礎上,而C++ 等語言中的對像是建立在源代碼級基礎上的,因此COM 對像是語言無關的。這一特性使用不同編程語言開發的組件對象進行交互成為可能。 -------------------------------------------------- ----------------------------- COM 對象與接口 -------------------------------------------------- ----------------------------- 類似於C++ 中對象的概念,對像是某個類(class)的一個實例;而類則是一組相關的數據和功能組合在一起的一個定義。使用對象的應用(或另一個對象)稱為客戶,有時也稱為對象的用戶。 接口是一組邏輯上相關的函數集合,其函數也被稱為接口成員函數。按照習慣,接口名常是以“I”為前綴。對象通過接口成員函數為客戶提供各種形式的服務。 在COM 模型中,對象本身對於客戶來說是不可見的,客戶請求服務時,只能通過接口進行。每一個接口都由一個128 位的全局唯一標識符(GUID,Global Unique Identifier)來標識。客戶通過GUID 來獲得接口的指針,再通過接口指針,客戶就可以調用其相應的成員函數。 與接口類似,每個組件也用一個128 位GUID 來標識,稱為CLSID(class identifer,類標識符或類ID),用CLSID 標識對象可以保證(概率意義上)在全球範圍內的唯一性。實際上,客戶成功地創建對像後,它得到的是一個指向對象某個接口的指針,因為COM 對象至少實現一個接口(沒有接口的COM 對像是沒有意義的),所以客戶就可以調用該接口提供的所有服務。根據COM 規範,一個COM 對像如果實現了多個接口,則可以從某個接口得到該對象的任意其他接口。從這個過程我們也可以看出,客戶與COM 對像只通過接口打交道,對像對於客戶來說只是一組接口。 -------------------------------------------------- ----------------------------- COM 進程模型 -------------------------------------------------- ----------------------------- COM 所提供的服務組件對像在實現時有兩種進程模型:進程內對象和進程外對象。如果是進程內對象,則它在客戶進程空間中運行;如果是進程外對象,則它運行在同機器上的另一個進程空間或者在遠程機器的空間。 進程內服務程序: 服務程序被加載到客戶的進程空間,在Windows 環境下,通常服務程序的代碼以動態連接庫(DLL)的形式實現。 本地服務程序: 服務程序與客戶程序運行在同一台機器上,服務程序是一個獨立的應用程序,通常它是一個EXE 文件。 遠程服務程序: 服務程序運行在與客戶不同的機器上,它既可以是一個DLL 模塊,也可以是一個EXE 文件。如果遠程服務程序是以DLL 形式實現的話,則遠程機器會創建一個代理進程。 雖然COM 對像有不同的進程模型,但這種區別對於客戶程序來說是透明的,因此客戶程序在使用組件對象時可以不管這種區別的存在,只要遵照COM 規範即可。然而,在實現COM 對象時,還是應該慎重選擇進程模型。進程內模型的優點是效率高,但組件不穩定會引起客戶進程崩潰,因此組件可能會危及客戶;(savetime 注:這裡有點問題,如果組件不穩定,進程外模型也同樣會出問題,可能是因為進程內組件和客戶同處一個地址空間,出現衝突的可能性比較大?)進程外模型的優點是穩定性好,組件進程不會危及客戶程序,一個組件進程可以為多個客戶進程提供服務,但進程外組件開銷大,而且調用效率相對低一點。 -------------------------------------------------- ----------------------------- COM 可重用性 -------------------------------------------------- ----------------------------- 由於COM 標準是建立在二進制代碼級的,因此COM 對象的可重用性與一般的面向對象語言如C++ 中對象的重用過程不同。對於COM 對象的客戶程序來說,它只是通過接口使用對象提供的服務,它並不知道對象內部的實現過程,因此,組件對象的重用性可建立在組件對象的行為方式上,而不是具體實現上,這是建立重用的關鍵。 COM 用兩種機制實現對象的重用。我們假定有兩個COM 對象,對象1 希望能重用對象2 的功能,我們把對象1 稱為外部對象,對象2 稱為內部對象。 (1)包容方式。 對象1 包含了對象2,當對象1 需要用到對象2 的功能時,它可以簡單地把實現交給對象2 來完成,雖然對象1 和對象2 支持同樣的接口,但對象1 在實現接口時實際上調用了對象2 的實現。 (2)聚合方式。 對象1 只需簡單地把對象2 的接口遞交給客戶即可,對象1 並沒有實現對象2 的接口,但它把對象2 的接口也暴露給客戶程序,而客戶程序並不知道內部對象2 的存在。 ================================================== ============================= ⊙ 第二章COM 對像模型 ================================================== ============================= 全局唯一標識符GUID -------------------------------------------------- ----------------------------- COM 規範採用了128 位全局唯一標識符GUID 來標識對象和接口,這是一個隨機數,並不需要專門機構進行分配和管理。因為GUID 是個隨機數,所以並不絕對保證唯一性,但發生標識符相重的可能性非常小。從理論上講,如果一台機器每秒產生10000000 個GUID,則可以保證(概率意義上)的3240 年不重複)。 GUID 在C/C++ 中可以用這樣的結構來描述: typedef struct _GUID { DWORD Data1; WORD Data2; WORD Data3; BYTE Data4[8]; } GUID; 例:{64BF4372-1007-B0AA-444553540000} 可以如下定義一個GUID: extern "C" const GUID CLSID_MYSPELLCHECKER = { 0x54BF0093, 0x1048, 0x399D, { 0xB0, 0xA3, 0x45, 0x33, 0x43, 0x90, 0x47, 0x47} }; Visual C++ 提供了兩個程序生成GUID: UUIDGen.exe(命令行) 和GUIDGen.exe(對話框)。 COM 庫提供了以下API 函數可以產生GUID: HRESULT CoCreateGuid(GUID *pguid); 如果創建GUID 成功,則函數返回S_OK,並且pguid 將指向所得的GUID 值。 -------------------------------------------------- ----------------------------- COM 對象 -------------------------------------------------- ----------------------------- 在COM 規範中,並沒有對COM 對象進行嚴格的定義,但COM 提供的是面向對象的組件模型,COM 組件提供給客戶的是以對象形式封裝起來的實體。客戶程序與COM 程序進行交互的實體是COM 對象,它並不關心組件模型的名稱和位置(即位置透明性),但它必須知道自己在與哪個COM 對象進行交互。 -------------------------------------------------- ----------------------------- COM 接口 -------------------------------------------------- ----------------------------- 從技術上講,接口是包含了一組函數的數據結構,通過這組數據結構,客戶代碼可以調用組件對象的功能。接口定義了一組成員函數,這組成員函數是組件對象暴露出來的所有信息,客戶程序利用這些函數獲得組件對象的服務。 通常我們把接口函數表稱為虛函數表(vtable),指向vtable 的指針為pVtable。對於一個接口來說,它的虛函數表是確定的,因此接口的成員函數個數是不變的,而且成員函數的先後先後順序也是不變的;對於每個成員函數來說,其參數和返回值也是確定的。在一個接口的定義中,所有這些信息都必須在二進制一級確定,不管什麼語言,只要能支持這樣的內存結構描述,就可以使用接口。 接口指針----> pVtable ----> 指針函數1 -> |----------| m_Data1 指針函數2 -> | 對象實現| m_Data2 指針函數3 -> |----------| 每一個接口成員函數的第一個參數為指向對象實例的指針(=this),這是因為接口本身並不獨立使用,它必須存在於某個COM 對像上,因此該指針可以提供對象實例的屬性信息,在被調用時,接口可以知道是對哪個COM 對像在進行操作。 在接口成員函數中,字符串變量必須用Unicode 字符指針,COM 規範要求使用Unicode 字符,而且COM 庫中提供的COM API 函數也使用Unicode 字符。所以如果在組件程序內部使用到了ANSI 字符的話,則應該進行兩種字符表達的轉換。當然,在即建立組件程序又建立客戶程序的情況下,可以使用自己定義的參數類型,只要它們與COM 所能識別的參數類型兼容。 Visual C++ 提供兩種字符串的轉換: namespace _com_util { BSTR ConvertStringToBSTR(const char *pSrc) throw(_com_error); BSTR ConvertBSTRToString(BSTR pSrc) throw(_com_error); } BSTR 是雙字節寬度字符串,它是最常用的自動化數據類型。 -------------------------------------------------- ----------------------------- 接口描述語言IDL -------------------------------------------------- ----------------------------- COM 規範在採用OSF 的DCE 規範描述遠程調用接口IDL (interface description language,接口描述語言)的基礎上,進行擴展形成了COM 接口的描述語言。接口描述語言提供了一種不依賴於任何語言的接口的描述方法,因此,它可以成為組件程序和客戶程序之間的共同語言。 COM 規範使用的IDL 接口描述語言不僅可用於定義COM 接口,同時還定義了一些常用的數據類型,也可以描述自定義的數據結構,對於接口成員函數,我們可以定義每個參數的類型、輸入輸出特性,甚至支持可變長度的數組的描述。 IDL 支持指針類型,與C/C++ 很類似。例如: interface IDictionary { HRESULT Initialize() HRESULT LoadLibrary([in] string); HRESULT InsertWord([in] string, [in] string); HRESULT DeleteWord([in] string); HRESULT LookupWord([in] string, [out] string *); HRESULT RestoreLibrary([in] string); HRESULT FreeLibrary(); } Microsoft Visual C++ 提供了MIDL 工具,可以把IDL 接口描述文件編譯成C/C++ 兼容的接口描述頭文件(.h)。 -------------------------------------------------- ----------------------------- IUnknown 接口 -------------------------------------------------- ----------------------------- IUnknown 的IDL 定義: interface IUnknown { HRESULT QueryInterface([in] REFIID iid, [out] void **ppv); ULONG AddRef(void); ULONG Release(void); } IUnkown 的C++ 定義: class IUnknown { virutal HRESULT _stdcall QueryInterface(const IID& iid, void **ppv) = 0; virtual ULONG _stdcall AddRef() = 0; virutal ULONG _stdcall Release() = 0; } -------------------------------------------------- ----------------------------- COM 對象的接口原則 -------------------------------------------------- ----------------------------- COM 規範對QueryInterface 函數設置了以下規則: 1. 對於同一個對象的不同接口指針,查詢得到的IUnknown 接口必須完全相同。也就是說,每個對象的IUnknown 接口指針是唯一的。因此,對兩個接口指針,我們可以通過判斷其查詢到的IUnknown 接口是否相等來判斷它們是否指向同一個對象。 2. 接口自反性。對一個接口查詢其自身總應該成功,比如: pIDictionary->QueryInterface(IID_Dictionary, ...) 應該返回S_OK。 3. 接口對稱性。如果從一個接口指針查詢到另一個接口指針,則從第二個接口指針再回到第一個接口指針必定成功,比如: pIDictionary->QueryInterface(IID_SpellCheck, (void **)&pISpellCheck); 如果查找成功的話,則再從pISpellCheck 查回IID_Dictionary 接口肯定成功。 4. 接口傳遞性。如果從第一個接口指針查詢到第二個接口指針,從第二個接口指針可以查詢到第三個接口指針,則從第三個接口指針一定可以查詢到第一個接口指針。 5. 接口查詢時間無關性。如果在某一個時刻可以查詢到某一個接口指針,則以後任何時間再查詢同樣的接口指針,一定可以查詢成功。 總之,不管我們從哪個接口出發,我們總可以到達任何一個接口,而且我們也總可以回到最初的那個接口。 ================================================== ============================= ⊙ 第三章COM 的實現 ================================================== ============================= COM 組件註冊信息 -------------------------------------------------- ----------------------------- 當前機器上所有組件的信息HKEY_CLASS_ROOT/CLSID 進程內組件HKEY_CLASS_ROOT/CLSID/guid/InprocServer32 進程外組件HKEY_CLASS_ROOT/CLSID/guid/LocalServer32 組件所屬類別(CATID) HKEY_CLASS_ROOT/CLSID/guid/Implemented Categories COM 接口的配置信息HKEY_CLASS_ROOT/Interface 代理DLL/存根DLL HKEY_CLASS_ROOT/CLSID/guid/ProxyStubClsid HKEY_CLASS_ROOT/CLSID/guid/ProxyStubClsid32 類型庫的信息HKEY_CLASS_ROOT/TypeLib 字符串命名ProgID HKEY_CLASS_ROOT/ (例如"COMCTL.TreeCtrl") 組件GUID HKEY_CLASS_ROOT/COMTRL.TreeControl/CLSID 缺省版本號HKEY_CLASS_ROOT/COMTRL.TreeControl/CurVer (例如CurVer = "COMTRL.TreeCtrl.1", 那麼 HKEY_CLASS_ROOT/COMTRL.TreeControl.1 也存在) 當前機器所有組件類別HKEY_CLASS_ROOT/Component Categories COM 提供兩個API 函數CLSIDFromProgID 和ProgIDFromCLSID 轉換ProgID 和CLSID。 如果COM 組件支持同樣一組接口,則可以把它們分到同一類中,一個組件可以被分到多個類中。比如所有的自動化對像都支持IDispatch 接口,則可以把它們歸成一類“Automation Objects”。類別信息也用一個GUID 來描述,稱為CATID。組件類別最主要的用處在於客戶可以快速發現機器上的特定類型的組件對象,否則的話,就必須檢查所有的組件對象,並把組件對象裝入到內存中實例化,然後依次詢問是否實現了必要的接口,現在使用了組件類別,就可以節省查詢過程。 -------------------------------------------------- ----------------------------- 註冊COM 組件 -------------------------------------------------- ----------------------------- RegSrv32.exe 用於註冊一個進程內組件,它調用DLL 的DllRegisterServer 和DllUnregisterServer 函數完成組件程序的註冊和註銷操作。如果操作成功返回TRUE,否則返回FALSE。 對於進程外組件程序,情形稍有不同,因為它自身是個可執行程序,而且它也不能提供入口函數供其他程序使用。因此,COM 規範中規定,支持自註冊的進程外組件必須支持兩個命令行參數/RegServer 和/UnregServer,以便完成註冊和註銷操作。命令行參數大小寫無關,而且“/” 可以用“-” 替代。如果操作成功,程序返回0,否則,返回非0 表示失敗。 -------------------------------------------------- ----------------------------- 類廠和DllGetObjectClass 函數 -------------------------------------------------- ----------------------------- 類廠(class factory)是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; } CreateInstance 成員函數用於創建對應的COM 對象。第一個參數pUnknownOuter 用於對像類被聚合的情形,一般設置為NULL;第二個參數iid 是對象創建完成後客戶應該得到的初始接口IID;第三個參數ppv 存放返回的接口指針。 LockServer 成員函數用於控制組件的生存週期。 類廠對像是由DLL 引出函數DllGetClassObject 創建的: HRESULT DllGetClassObject(const CLSID& clsid, const IID& iid, (void **)ppv); DllGetClassObject 函數的第一個參數為待創建對象的CLSID。因為一個組件可能實現了多個COM 對像類,所以在DllGetClassObject 函數的參數中有必要指定CLSID,以便創建正確的class factory。另兩個參數iid 和ppv 分別指於指定接口IID 和存放類廠接口指針。 COM 庫在接到對象創建的指令後,它要調用進程內組件的DllGetClassObject 函數,由該函數創建類廠對象,並返回類廠對象的接口指針。 COM 庫或客戶一旦擁有類廠的接口指針,它們就可以通過IClassFactory 的成員函數CreateInstance 創建相應的COM 對象。 -------------------------------------------------- ----------------------------- CoGetClassObject 函數 -------------------------------------------------- ----------------------------- 在COM 庫中,有三個API 可用於對象的創建,它們分別是CoGetClassObject、CoCreateInstnace 和CoCreateInstanceEx。通常情況下,客戶程序調用其中之一完成對象的創建,並返回對象的初始接口指針。 COM 庫與類廠也通過這三個函數進行交互。 HRESULT CoGetClassObject(const CLSID& clsid, DWORD dwClsContext, COSERVERINFO *pServerInfo, const IID& iid, (void **)ppv); CoGetClassObject 函數先找到由clsid 指定的COM 類的類廠,然後連接到類廠對象,如果需要的話,CoGetClassObject 函數裝入組件代碼。如果是進程內組件對象,則CoGetClassObject 調用DLL 模塊的DllGetClassObject 引出函數,把參數clsid、iid 和ppv 傳給DllGetClassObject 函數,並返回類廠對象的接口指針。通常情況下iid 為IClassFactory 的標識符IID_IClassFactory。如果類廠對像還支持其它可用於創建操作的接口,也可以使用其它的接口標識符。例如,可請求IClassFactory2 接口,以便在創建時,驗證用戶的許可證情況。 IClassFactory2 接口是對IClassFactory 的擴展,它加強了組件創建的安全性。 參數dwClsContext 指定組件類別,可以指定為進程內組件、進程外組件或者進程內控制對象(類似於進程外組件的代理對象,主要用於OLE 技術)。參數iid 和ppv 分別對應於DllGetClassObject 的參數,用於指定接口IID 和存放類對象的接口指針。參數pServerInfo 用於創建遠程對象時指定服務器信息,在創建進程內組件對像或者本地進程外組件時,設置NULL。 如果CoGetClassObject 函數創建的類廠對象位於進程外組件,則情形要復雜得多。首先CoGetClassObject 函數啟動組件進程,然後一直等待,直到組件進程把它支持的COM 類對象的類廠註冊到COM 中。於是CoGetClassObject 函數把COM 中相應的類廠信息返回。因此,組件外進程被COM 庫啟動時(帶命令行參數“/Embedding”),它必須把所支持的COM 類的類廠對象通過CoRegisterClassObject 函數註冊到COM 中,以便COM 庫創建COM 對象使用。當進程退出時,必須調用CoRevokeClassObject 函數以便通知COM 它所註冊的類廠對像不再有效。組件程序調用CoRegisterClassObject 函數和CoRevokeClassObject 函數必須配對,以保證COM 信息的一致性。 -------------------------------------------------- ----------------------------- CoCreateInstance / CoCreateInstanceEx 函數 -------------------------------------------------- ----------------------------- HRESULT CoCreateInstance(const CLSID& clsid, IUnknown *pUnknownOuter, DWORD dwClsContext, const IID& iid, (void **)ppv); CoCreateInstance 是一個被包裝過的輔助函數,在它的內部實際上也調用了CoGetClassObject 函數。 CoCreateInstance 的參數clsid 和dwClsContext 的含義與CoGetClassObject 相應的參數一致,(CoCreateInstance 的iid 和ppv 參數與CoGetClassObject 不同,一個是表示對象的接口信息,一個是表示類廠的接口信息)。參數pUnknownOuter 與類廠接口的CreateInstance 中對應的參數一致,主要用於對像被聚合的情況。 CoCreateInstance 函數把通過類廠創建對象的過程封裝起來,客戶程序只要指定對像類的CLSID 和待輸出的接口指針及接口ID,客戶程序可以不與類廠打交道。 CoCreateInstance 可以用下面的代碼實現: (savetime 注:下面代碼中ppv 指針的應用,好像應該是void **) HRESULT CoCreateInstance(const CLSID& clsid, IUnknown *pUnknownOuter, DWORD dwClsContext, const IID& iid, void *ppv) { IClassFactory *pCF; HRESULT hr; hr = CoGetClassObject(clsid, dwClsContext, NULL, IID_IClassFactory, (void *) pCF); if (FAILED(hr)) return hr; hr = pCF->CreateInstance(pUnknownOuter, iid, (void *)ppv); pFC->Release(); return hr; } 從這段代碼我們可以看出,CoCreateInstance 函數首先利用CoGetClassObject 函數創建類廠對象,然後用得到的類廠對象的接口指針創建真正的COM 對象,最後把類廠對象釋放掉並返回,這樣就把類廠屏蔽起來。 但是,用CoCreateInstance 並不能創建遠程機器上的對象,因為在調用CoGetClassObject 時,把第三個用於指定服務器信息的參數設置為NULL。如果要創建遠程對象,可以使用CoCreateInstance 的擴展函數CoCreateInstanceEx: HRESULT CoCreateInstanceEx(const CLSID& clsid, IUnknown *pUnknownOuter, DWORD dwClsContext, COSERVERINFO *pServerInfo, DWORD dwCount, MULTI_QI *rgMultiQI); 前三個參數與CoCreateInstance 一樣,pServerInfo 與CoGetClassOjbect 的參數一樣,用於指定服務器信息,最後兩個參數dwCount 和rgMultiQI 指定了一個結構數組,可以用於保存多個對象接口指針,其目的在於一次獲得多個接口指針,以便減少客戶程序與組件程序之間的頻繁交互,這對於網絡環境下的遠程對像是很有意義的。 -------------------------------------------------- ----------------------------- COM 庫的初始化 -------------------------------------------------- ----------------------------- 調用COM 庫的函數之前,為了使函數有效,必須調用COM 庫的初始化函數: HRESULT CoInitialize(IMalloc *pMalloc); pMalloc 用於指定一個內存分配器,可由應用程序指定內存分配原則。一般情況下,我們直接把參數設為NULL,則COM 庫將使用缺省提供的內存分配器。 返回值:S_OK 表示初始化成功 S_FALSE 表示初始化成功,但這次調用不是本進程中首次調用初始化函數 S_UNEXPECTED 表示初始化過程中發生了錯誤,應用程序不能使用COM 庫 通常,一個進程對COM 庫只進行一次初始化,而且,在同一個模塊單元中對COM 庫進行多次初始化並沒有意義。唯一不需要初始化COM 庫的函數是獲取COM 庫版本的函數: DWORD CoBuildVersion(); 返回值:高16 位主版本號 低16 位次版本號 COM 程序在用完COM 庫服務之後,通常是在程序退出之前,一定要調用終止COM 庫服務函數,以便釋放COM 庫所維護的資源: void CoUninitialize(void); 注意:凡是調用CoInitialize 函數返回S_OK 的進程或程序模塊一定要有對應的CoUninitialize 函數調用,以保證COM 庫有效地利用資源。 (? 如果在一個模塊中調用CoInitialize 返回S_OK,那麼它調用CoUnitialize 函數後,其它也在使用COM 庫的模塊是否會出錯誤?還是COM 庫會自動檢查有哪些模塊在使用?) -------------------------------------------------- ----------------------------- COM 庫的內存管理 -------------------------------------------------- ----------------------------- 由於COM 組件程序和客戶程序是通過二進制級標準建立連接的,所以在COM 應用程序中凡是涉及客戶、COM 庫和組件三者之間內存交互(分配和釋放不在同一個模塊中)的操作必須使用一致的內存管理器。 COM 提供的內存管理標準,實際上是一個IMalloc 接口: // IID_IMalloc: {00000002-0000-0000-C000-000000000046} class IMalloc: public IUnknown { void * Alloc(ULONG cb) = 0; void * Realloc(void *pv, ULONG cb) = 0; void Free(void *pv) = 0; ULONG GetSize(void *pv) = 0; // 返回分配的內存大小 int DidAlloc(void *pv) = 0; // 確定內存指針是否由該內存管理器分配 void HeapMinimize() = 0; // 使堆內存盡可能減少,把沒用到的內存還給 // 操作系統,用於性能優化 } 獲得IMalloc 接口指針: HRESULT CoGetMalloc(DWORD dwMemContext, IMalloc **ppMalloc); CoGetMalloc 函數的第一個參數dwMemContext 用於指定內存管理器的類型。 COM 庫中包含兩種內存管理器,一種就是在初始化時指定的內存管理器或者其內部缺省的管理器,也稱為作業管理器(task allocator),這種管理器在本進程內有效,要獲取該管理器,在dwMemContext 參數中指定為MEMCTX_TASK;另一種是跨進程的共享分配器,由OLE 系統提供,要獲取這種管理器,dwMemContext 參數中指定為MEMCTX_SHARED,使用共享管理器的便利是,可以在一個進程內分配內存並傳給第二個進程,在第二個進程內使用此內存甚至釋放掉此內存。 只要函數的返回值為S_OK,則ppMalloc 就指向了COM 庫的內存管理器接口指針,可以使用它進行內存操作,使用完畢後,應該調用Release 成員函數釋放控制權。 COM 庫封裝了三個API 函數,可用於內存分配和釋放: void * CoTaskMemAlloc(ULONG cb); void CoTaskFree(void *pv); void CoTaskMemRealloc(void *pv, ULONG cb); 這三個函數分配對應於IMalloc 的三個成員函數:Alloc、Realloc 和Free。 例:COM 程序如何從CLSID 值找到相應的ProgID 值: WCHAR *pwProgID; char pszProgID[128]; hResult = ::ProgIDFromCLSID(CLSID_Dictionary, &pwProgID); if (hResult != S_OK) { ... } wcstombs(pszProgID, pwProgID, 128); CoTaskMemFree(pwProgID); // 注意:必須釋放內存 在調用COM 函數ProgIDFromCLSID 返回之後,因為COM 庫為輸出變量pwProgID 分配了內存空間,所以應用程序在用完pwProgID 變量之後,一定要調用CoTaskMemFree 函數釋放內存。該例子說明了在COM 庫中分配內存,而在調用程序中釋放內存的一種情況。 COM 庫中其他一些函數也有類似的特性,尤其是一些包含不定長度輸出參數的函數。 -------------------------------------------------- ----------------------------- 組件程序的裝載和卸載 -------------------------------------------------- ----------------------------- 進程內組件的裝載: 客戶程序調用COM 庫的CoCreateInstance 或CoGetClassObject 函數創建COM 對象,在CoGetClassObject 函數中,COM 庫根據系統註冊表中的信息,找到類標識符CLSID 對應的組件程序(DLL 文件)的全路徑,然後調用LoadLibrary(實際上是CoLoadLibrary)函數,並調用組件程序的DllGetClassObject 引出函數。 DllGetClassObject 函數創建相應的類廠對象,並返回類廠對象的IClassFactory 接口。至此CoGetClassObject 函數的任務完成,然後客戶程序或者CoCreateInstance 函數繼續調用類廠對象的CreateInstance 成員函數,由它負責COM 對象的創建工作。 CoCreateInstance |-CoGetClassObject |-Get CLSID -> DLLfile path |-CoLoadLibrary |-DLLfile.DllGetClassObject |-return IClassFactory |-IClassFactory.CreateInstnace 進程外組件的裝載: 在COM 庫的CoGetClassObject 函數中,當它發現組件程序是EXE 文件(由註冊表組件對象信息中的LocalServer 或LocalServer32 值指定)時,COM 庫創建一個進程啟動組件程序,並帶上“/Embedding”命令行參數,然後等待組件程序;而組件程序在啟動後,當它檢查到“/Embedding”命令行參數後,就會創建類廠對象,然後調用CoRegisterClassObject 函數把類廠對象註冊到COM 中。當COM 庫檢查到組件對象的類廠之後,CoGetClassObject 函數就把類廠對象返回。由於類廠與客戶程序運行在不同的進程中,所以客戶程序得到的是類廠的代理對象。一旦客戶程序或COM 庫得到了類廠對象,它就可以完成組件對象的創建工作。 進程內對象和進程外對象的不同創建過程僅僅影響了CoGetClassObject 函數的實現過程,對於客戶程序來說是完全透明的。 CoGetClassObject |-LocalServer/LocalServer32 |-Execute EXE /Embedding |-Create class factory |-CoRegisterClassObject ( class factory ) |-return class factory (proxy) 進程內組件的卸載: 只有當組件程序滿足了兩個條件時,它才能被卸載,這兩個條件是:組件中對像數為0,類廠的鎖計數為0。滿足這兩個條件時,DllCanUnloadNow 引出函數返回TRUE。 COM 提供了一個函數CoFreeUnusedLibraries,它會檢測當前進程中的所有組件程序,當發現某個組件程序的DllCanUnloadNow 函數返回TRUE 時,就調用FreeLibrary 函數(實際上是CoFreeLibrary 函數)把該組件從程序從內存中卸出。 該由誰來調用CoFreeUnusedLibraries 函數呢?因為在組件程序執行過程中,它不可能把自己從內存中卸出,所以這個任務應該由客戶來完成。客戶程序隨時都可以調用CoFreeUnusedLibraries 函數完成卸出工作,但通常的做法是,在程序的空閑處理過程中調用CoFreeUnusedLibraries 函數,這樣做既可以避免程序中處處考慮對CoFreeUnusedLibraries 函數的調用,又可以使不再使用的組件程序得到及時清除,提高資源的利用率,COM 規範也推薦這種做法。 進程外組件的卸載: 進程外組件的卸載比較簡單,因為組件程序運行在單獨的進程中,一旦其退出的條件滿足,它只要從進程的主控函數返回即可。在Windows 系統中,進程的主控函數為WinMain。 前面曾經說過,在組件程序啟動運行時,它調用CoRegisterClassObject 函數,把類廠對象註冊到COM 中,註冊之後,類廠對象的引用計數始終大於0,因此單憑類廠對象的引用計數無法控制進程的生存期,這也是引入類廠對象的加鎖和減鎖操作的原因。進程外組件的載條件與DllCanUnloadNow 中的判斷類似,也需要判斷COM 對像是否還存在、以及判斷是否鎖計數器為0,只有當條件滿足了,進程的主函數才可以退出。 從原則上講,進程外組件程序的卸載就是這麼簡單,但實際上情況可能複雜一些,因為有些組件程序在運行過程中可以創建自己的對象,或者包含用戶界面的程序在運行過程中,用戶手工關閉了進程,那麼進程對這些動作的處理要復雜一些。例如,組件程序在運行過程中,用戶又打開了一個文件並進行操作,那麼即使原先創建的對像被釋放了,而且鎖計數器也為0,進程也不能退出,它必須繼續為用戶服務,就像是用戶打開的進程一樣。對這種程序,可以增加一個“用戶控制”標記flag,如果flag 為FALSE,則可以按簡單的方法直接退出程序即可;如果flag 為TRUE,則表明用戶參與了控制,組件進程不能馬上退出,但應該調用CoRevokeClassObject 函數以便與CoRegisterClassObject 調用相響呼應,把進程留給用戶繼續進行。 如果組件程序在運行過程中,用戶要關閉進程,而此時並不滿足進程退出條件,那麼進程可以採取兩種辦法:第一種方法,把應用隱藏起來,並把flag 標記設置為FALSE,然後組件程序繼續運行直到卸載條件滿足為止;另一種辦法是,調用CoDisconnectObject 函數,強迫脫離對象與客戶之間的關係,並強行終止進程,這種方法比較粗暴,不提倡採用,但不得已時可以也使用,以保證系統完成一些高優先級的操作。 -------------------------------------------------- ----------------------------- COM 庫常用函數 -------------------------------------------------- ----------------------------- 初始化函數CoBuildVersion 獲得COM 庫的版本號 CoInitialize COM 庫初始化 CoUninitialize COM 庫功能服務終止 CoFreeUnusedLibraries 釋放進程中所有不再使用的組件程序 GUID 相關函數IsEqualGUID 判斷兩個GUID 是否相等 IsEqualIID 判斷兩個IID 是否相等 IsEqualCLSID 判斷兩個CLSID 是否相等(*為什麼要3個函數) CLSIDFromProgID 字符串組件標識轉換為CLSID 形式 StringFromCLSID CLSID 形式標識轉化為字符串形式 IIDFromString 字符串轉換為IID 形式 StringFromIID IID 形式轉換為字符串 StringFromGUID2 GUID 形式轉換為字符串(*為什麼有2) 對象創建函數CoGetClassObject 獲取類廠對象 CoCreateInstance 創建COM 對象 CoCreateInstanceEx 創建COM 對象,可指定多個接口或遠程對象 CoRegisterClassObject 登記一個對象,使其它應用程序可以連接到它 CoRevokeClassObject 取消對象的登記 CoDisconnectObject 斷開其它應用與對象的連接 內存管理函數CoTaskMemAlloc 內存分配函數 CoTaskMemRealloc 內存重新分配函數 CoTaskMemFree 內存釋放函數 CoGetMalloc 獲取COM 庫內存管理器接口 -------------------------------------------------- ----------------------------- HRESULT 類型 -------------------------------------------------- ----------------------------- 大多數COM 函數以及一些接口成員函數的返回值類型均為HRESULT 類型。 HRESULT 類型的返回值反映了函數中的一些情況,其類型定義規範如下: 31 30 29 28 16 15 0 |-----|--|------------------------|--------------- --------------------| 類別碼(30-31) 反映函數調用結果: 00 調用成功 01 包含一些信息 10 警告 11 錯誤 自定義標記(29) 反映結果是否為自定義標識,1 為是,0 則不是; 操作碼(16-28) 標識結果操作來源,在Windows 平台上,其定義如下: #define FACILITY_WINDOWS 8 #define FACILITY_STORAGE 3 #define FACILITY_RPC 1 #define FACILITY_SSPI 9 #define FACILITY_WIN32 7 #define FACILITY_CONTROL 10 #define FACILITY_NULL 0 #define FACILITY_INTERNET 12 #define FACILITY_ITF 4 #define FACILITY_DISPATCH 2 #define FACILITY_CERT 11 操作結果碼(0-15) 反映操作的狀態,WinError.h 定義了Win32 函數所有可能返回結果。 以下是一些經常用到的返回值和宏定義: S_OK 函數執行成功,其值為0 (注意,其值與TRUE 相反) S_FALSE 函數執行成功,其值為1 S_FAIL 函數執行失敗,失敗原因不確定 E_OUTOFMEMORY 函數執行失敗,失敗原因為內存分配不成功 E_NOTIMPL 函數執行失敗,成員函數沒有被實現 E_NOTINTERFACE 函數執行失敗,組件沒有實現指定的接口 不能簡單地把返回值與S_OK 和S_FALSE 比較,而要用SECCEEDED 和FAILED 宏進行判斷。 ================================================== ============================= ⊙ 第四章COM 特性 ================================================== ============================= 可重用性:包容和聚合 -------------------------------------------------- ----------------------------- 包容模型: 組件對像在接口的實現代碼中執行自身創建的另一個組件對象的接口函數(客戶/服務器模型)。這個對象同時實現了兩個(或更多)接口的代碼。 聚合模型: 組件對像在接口的查詢代碼中把接口傳遞給自已創建的另一個對象的接口查詢函數,而不實現該接口的代碼。另一個對象必須實現聚合模型(也就是說,它知道自己正在被另一個組件對象聚合),以便QueryInterface 函數能夠正常運作。 在組件對像被聚合的情況下,當客戶請求它所不支持的接口或者請求IUnknown 接口時,它必須把控制交給外部對象,由外部對象決定客戶程序的請求結果。 聚合模型體現了組件軟件真正意義上的重用。 聚合模型實現的關鍵在CoCreateInstance 函數和IClassFactory 接口: HRESULT CoCreateInstance(const CLSID& clsid, IUnknown *pUnknownOuter, DWORD dwClsContext, const IID& iid, (void **)ppv); // class IClassFactory : public IUnknown virtual HRESULT _stdcall CreateInstance(IUnknown *pUnknownOuter, const IID& iid, void **ppv) = 0; 其中pUnknownOuter 參數用於指定組件對像是否被聚合。如果pUnknownOuter 參數為NULL,說明組件對象正常使用,否則說明被聚合使用,pUnknownOuter 是外部組件對象的接口指針。 聚合模型下的被聚合對象的引用計數成員函數也要進行特別處理。在未被聚合的情況下,可以使用一般的引用計數方法。在被聚合時,由客戶調用AddRef/Release 函數時,必須轉向外部組件對象的AddRef/Release 方法。這時,外部組件對像要控制被聚合的對象必須採用其它的引用計數接口。 -------------------------------------------------- ----------------------------- 進程透明性(待學) 安全性(待學) 多線程特性(待學) -------------------------------------------------- ----------------------------- ================================================== ============================= ⊙ 第五章用Visual C++ 開發COM 應用 ================================================== ============================= Win32 SDK 提供的一些頭文件的說明 -------------------------------------------------- ----------------------------- Unknwn.h 標準接口IUnknown 和IClassFacatory 的IID 及接口成員函數的定義 Wtypes.h 包含COM 使用的數據結構的說明 Objidl.h 所有標準接口的定義,即可用於C 語言風格的定義,也可用於C++ 語言 Comdef.h 所有標準接口以及COM 和OLE 內部對象的CLSID ObjBase.h 所有的COM API 函數的說明 Ole2.h 所有經過封裝的OLE 輔助函數 -------------------------------------------------- ----------------------------- 與COM 接口有關的一些宏 -------------------------------------------------- ----------------------------- DECLARE_INTERFACE(iface) 聲明接口iface,它不從其他的接口派生 DECLARE_INTERFACE_(iface, baseiface) 聲明接口iface,它從接口baseiface 派生 STDMETHOD(method) 聲明接口成員函數method,函數返回類型為HRESULT STDMETHOD_(type, method) 聲明接口成員函數method,函數返回類型為type ================================================== ============================= ⊙ 結束 ================================================== ============================= |