⊙ 第一章 概述
===================================================
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
===================================================
⊙ 結 束
===================================================