摘要
本文的目的是為使用和實行Microsoft的組件對象模型(COM)提供迅捷的參考。讀者若想更好的理解什麼是COM,以及隱藏在它的設計及體系中的動機,應該閱讀組件對象模型的技術說明書(MSDN庫,技術說明書)。
規則1:必須實現Iunknown
如果一個對象沒有至少實現一個最小程度為IUnknown的接口,那它就不是Microsoft的組件對象模型(COM)。
接口設計規則
接口必須直接或間接地從IUnknown繼承。
接口必須有唯一的識別(IID)。
接口是不變的。一旦分配和公布了IID,接口定義的任何因素都不能被改變。
接口的成員函數應該有HRESULT類型的返回值,使遠端結構可報告遠程過程調用(RPC)錯誤的情況。
接口成員函數的字符串參數應該是Unicode。
實現 IUnknown
對象的同一性。這要求對任何特定IUnknown接口的給定對象實例的QueryInterface調用返回相同的物理指針變量。這導致了所謂的兩個接口的QueryInterface(IID_IUnknown, ...)和結果的比較,以確定它們是否為同一對象(COM對象同一性)。
靜態接口的設置。任何經由QueryInterface來訪問對象的接口的設置,必須是靜態而不是動態的。也就是說,假如一旦QueryInterface獲得了一個給定的IID,那麼它總是對相同的對象(除非有意想不到情況)調用,假如QueryInterface不能獲得一個給定的IID,那麼隨後對相同IID的對象調用必定會失敗。
對象完整性。對於可處理的接口設置,必須有反身性,對稱性和過渡性。即給定代碼如下:
IA * pA = (some function returning an IA*);
IB * pB = NULL;
HRESULT hr;
hr = pA->QueryInterface(IID_IB,&pB); // line 4
Symmetric: pA->QueryInterface(IID_IA, ...) must succeed (a>>a)
Reflexive: If, in line 4, pB was successfully obtained, then
pB->QueryInterface(IID_IA, ...)
must succeed (a>>b, then b>>a).
Transitive: If, in line 4, pB was successfully obtained, and we do
IC * pC = NULL;
hr = pB->QueryInterface(IID_IC, &pC); //Line 7
and pC is successfully obtained in line 7,then
pA->QueryInterface(IID_IC, ...)
must succeed (a>>b, and b>>c,then a>>c).
最小參考服務大小。我們需要實現AddRef來維護一個服務台,它足夠大以便支持給定對象的所有接口的2 31 –1有出色的整體指示服務。一個32-位的無符號整型數滿足要求。
Release並不意味著失敗。假如客戶想知道關於資源已被釋放等情況,就必須在調用Release之前使用一些對象接口中的較高的語義。
內存管理規則
接口指針的生命期管理總是通過建立在每個COM接口上的AddRef和Release方法來實現。(參見下面的“引用計數規則”)
下面的規則適用於接口成員函數的參數,包括不是“按值”傳遞的返回值。
對於參數來說,調用程序應分配和釋放內存。
出口參數必須由被調用程序分配,由調用程序用標准的COM內存分配程序來釋放。
出入參數首先由調用程序分配,必要時由被調用程序釋放及重分配。至於出口參數,調用程序有責任釋放最終返回變量。此時必須使用標准的COM內存分配程序。
假如函數返回調用失敗的代碼,則通常調用者沒辦法清除出口和入出口參數。這導致了一些附加規則:
錯誤返回時,出口參數必須可靠地被設置成可清除變量,它不能對調用程序有影響。
此外,所有的出口指針參數(包括調用分配,被調用委任結構)必須被明顯地設為NULL。最直接的方法是在函數說明項中設成NULL。
返回錯誤時,所有的入出口參數必須為被調用者所擱置(這樣保持為調用程序初始化的值;若調用程序沒有對它初始化,則它是個出口參數,不是入出口參數),或者被明顯地設為出口錯誤返回情況。
參考計數規則:
規則1:對於接口指針的每一個新的副本,AddRef必須被調用;Release在接口指針的每一個破壞時調用,除了子規則明顯允許了其他情況。
以下規則對應於規則1的非例外情況。
規則1a:函數的入口出口參數。調用程序必須AddRef實際參數,因為當出口變量存放在它之上時,將由被調程序釋放。
規則1b:獲取全局變量。從全局變量的已存在的指針副本得到的接口指針的局部副本,必須被獨立地引用計數。因為存在局部副本時,被調函數會破壞全局副本。
規則1c:新指針合成所需資源不多。函數使用內在知識合成接口指針,而不是從其他資源所得,此時必須對新指針做初始AddRef。這樣的重要例子有事例生成法則,Iunknown::QueryInterface的實現,等等。
規則1d:內部存儲指針副本的返回。指針返回之後,被調程序不知道它的生命期和指針的內部存儲副本如何聯系。所以,被調程序必須在返回前對指針副本調用AddRef。
規則2:對於接口指針的兩個或更多的副本,它們的生命期的起始和終了的關系代碼的特定知識,使AddRef/Release可以被省略。
從COM客戶的角度,引用計數是和接口對應的概念。客戶不應認為對象的所有接口有同一引用計數。
不應依賴於Addref & Release的返回值,而應用於調試目的。
指針穩定性;參見在"Reference-Counting Rules"下的OLE幫助文件中的子部分:"Stabilizing the this Pointer and Keeping it Valid"。
參見Douglas Hodges寫的優秀的技術文章"Managing Object Lifetimes in OLE",及Kraig Brockschmidt (MSDN Library, Books)寫的Inside OLE的第三章來獲取更多信息。
COM申請責任:
以客戶,服務器,對象執行者之一身份使用COM的每一進程,要對三件事負責:
確定COM庫是同COM函數CoBuildVersion一致的版本。
在使用其他函數之前通過調用CoInitialize初始化COM庫。
在不用CoUninitialize時取消COM庫的初始化。
進程內服務器能假定載入的進程已執行了這些步驟。
服務器規則
進程內服務器必須輸出DllGetClassObject and DllCanUnloadNow。
進程內服務器必須支持COM自注冊。
進程內和局部服務器應該在它們的文件版本信息中提供OLESelfReg字符串。
進程內服務器必須輸出DllRegisterServer and DllUnRegisterServer。
局部服務器應支持/RegServer and /UnRegServer命令行開關。
生成集合對象
生成可合計的對象是可選的,且操作簡單,有諸多益處。以下規則使用於創建可合計的對象(通常稱為內部對象)。
由QueryInterface, AddRef, 和 Release對IUnknown接口的內部對象執行單獨控制內部接口的引用計數,且不能授權給外部未知指針。這種IUnknown執行稱為隱式IUnknown。
內部對象執行接口的QueryInterface, AddRef, 和 Release成員的實行,除了IUnknown自己,都必須授權給外部未知指針。這些實施不能直接影響內部對象的參考計數。
隱式Iunknown只對內部對象實施QueryInterface操作。
集合對象在占用外部未知指針參考時,不能調用AddRef。
如果當對象創建時,需要除Iunknown外的任一接口,創建失敗同E_UBKNOWN一起。
以下的代碼段闡明了使用嵌套類來實現集合對象接口的范例:
// CSomeObject is an aggregatable object that implements
// IUnknown and ISomeInterface
class CSomeObject : public IUnknown
{
private:
DWORD m_cRef;// Object reference count
IUnknown* m_pUnkOuter; //Outer unknown, no AddRef
// Nested class to implement the ISomeInterface interface
class CImpSomeInterface: public ISomeInterface
{
friend class CSomeObject ;
private:
private:DWORD m_cRef; //Interface ref-count, for debugging
private:IUnknown*m_pUnkOuter; // Outerunknown, for delegation
private:public:
private:CImpSomeInterface() { m_cRef = 0; };
private:~ CImpSomeInterface(void) {};
private:// IUnknown members delegate to the outer unknown
private:// IUnknown members do not control lifetime of object
private:STDMETHODIMP QueryInterface(REFIID riid, void** ppv)
private:{ return m_pUnkOuter->QueryInterface(riid,ppv);};
private:STDMETHODIMP_(DWORD) AddRef(void)
private:{ return m_pUnkOuter->AddRef(); };
private:STDMETHODIMP_(DWORD) Release(void)
private:{ return m_pUnkOuter->Release();};
private:// ISomeInterface members
private:STDMETHODIMP SomeMethod(void)
private:{ return S_OK; };
private:} ;
private:CImpSomeInterface m_ImpSomeInterface ;
private:public:
private:CSomeObject(IUnknown * pUnkOuter)
{
m_cRef=0;
// No AddRef necessary if non-NULL as we're aggregated.
m_pUnkOuter=pUnkOuter;
m_ImpSomeInterface.m_pUnkOuter=pUnkOuter;
} ;
// Static member function for creating new instances (don't use
// new directly).Protects against outer objects asking for interfaces
// other than IUnknown
static HRESULT Create(IUnknown* pUnkOuter, REFIID riid, void **ppv)
{
CSomeObject* pObj;
if (pUnkOuter != NULL && riid != IID_IUnknown)
return CLASS_E_NOAGGREGATION;
pObj = new CSomeObject(pUnkOuter);
if (pObj == NULL)
return E_OUTOFMEMORY;
// Set up the right unknown for delegation (the non-aggregation
case)
if (pUnkOuter == NULL)
pObj->m_pUnkOuter = (IUnknown*)pObj ;
HRESULT hr;
if (FAILED(hr = pObj->QueryInterface(riid, (void**)ppv)))
delete pObj ;
return hr;
}
// Implicit IUnknown members, non-delegating
// Implicit QueryInterface only controls inner object
STDMETHODIMP QueryInterface(REFIID riid, void** ppv)
{
*ppv=NULL;
if (riid == IID_IUnknown)
*ppv=this;
if (riid == IID_ISomeInterface)
*ppv=&m_ImpSomeInterface;
if (NULL==*ppv)
return ResultFromScode(E_NOINTERFACE);
((IUnknown*)*ppv)->AddRef();
return NOERROR;
} ;
STDMETHODIMP_(DWORD)AddRef(void)
{ return++m_cRef; };
STDMETHODIMP_(DWORD)Release(void)
{
if (--m_cRef != 0)
return m_cRef;
delete this;
return 0;
};
};
集合對象
當在一個對象上產生另一個集合對象時,必須遵循以下規則:
當創建一個內部對象時,外部對象必須明確的向Iunknown請求。
外部對象必須保護重入時對破壞代碼的人工引用的Release實施。
如果外部對象查詢任一內部對象接口,它必須調用自己的未知Release。釋放此指針時,外部對象緊隨內部對象指針調用自己的外部未知AddRef。
// Obtaining inner object interface pointer
pUnkInner->QueryInterface(IID_IFoo,&pIFoo);
pUnkOuter->Release();
// Releasing inner object interface pointer
pUnkOuter->AddRef();
pIFoo->Release();
外部對象不能盲目地對內部對象的未被識別接口進行查詢,除非操作是為外部對象特定目的。
房間線程化模型
房間模型線程化的細節實際上非常簡單,但是必須小心地遵循以下規則:
每個對象存在於單線程中(在單獨的房間中)。
所有對一對象的調用必須基於自己的線程(在自己的房間中)。直接從別的線程中調用對象是禁止的。試圖用空線程方式使用對象的申請,將在操作系統的未來版本中遇上不能正確運行的問題。這條規則的含義就是在房間之間,必須安排對象的所有指針。
為了處理從不同進程或同一進程的不同房間中的調用,在對象中的每一房間/線程必須有一個消息隊列。這就意味著線程的工作函數必須有一個GetMessage/DispatchMessage循環。假如在線程之間有別的同步原語用來通信,那麼Microsoft Win32的sgWaitForMultipleObjects將被用來等待消息和線程同步事件。
基於DLL或進程內的對象必須在注冊表中標記為"房間識別",通過給注冊數據庫的InprocServer32關鍵字增添名為"ThreadingModel=Apartment"的變量實現。
房間識別對象應仔細填寫DLL表項。對房間識別對象調用CoCreateInstance的每一個房間將從它的線程調用DllGetClassObject。故DllGetClassObject應能多級類對象或單線程安全對象。從任一線程調用CoFreeUnusedLibraries,都通過主房間線程來調用DllCanUnloadNow.。