http://www.vckbase.com/index.php/wv/60php
問題: 服務器
我用MFC編寫COM程序有一段時間了,知道如何使用宏和嵌套類,以及如何在嵌套類中處理IUnknown接口,但對IUnknown的使用仍是不太老練。函數
假設CMyClass是一個COM服務器,從CCmdTarget派生。它實現了IMyInterface。CMyClass的定義以下:spa
class CMyClass: public CCmdTarget { BEGIN_INTERFACE_PART(...) STDMETHOD.... END_INTERFACE_PART DECLARE_INTERFACE_MAP } ;
由於CCmdTarget沒有QueryInterface方法,CMyClass也沒有這個方法。個人問題是:這個COM服務器的客戶端老是要調用QueryInterface,它不調用ExternalQueryInterface, 也不調用 InternalQueryInterface。所以若是QueryInterface不是一個CMyClass的有效方法。這種狀況該怎麼處理?指針
解答: code
關於QueryInterface的這個問題,除你以外的許多人都對它感到困惑。可是不用怕,讀完本文就能夠見分曉。致使困惑的一個潛在的問題是CCmdTarget自己不從CObject派生,而且二者都有不一樣於QueryInterface的虛函數——例如,CObject中的第一個虛函數是GetRuntimeClass,CCmdTarget中的第一個虛函數是OnCmdMsg。因此從CCmdTarget派生出來的類怎麼多是COM類呢?並且還必須實現頭三個虛函數是AddRef, Release, 和 QueryInterface的IUnknown接口:對象
實際上,解開這個謎團的方法很簡單,但要求深刻了解MFC類庫。爲此,咱們必須進入MFC考察一番,主要目的是看看在調用CoCreateInstance建立類實例的時候會發生一些什麼事情。blog
首先,CoCreateInstance所作的第一件事情(或多或少)是到註冊表中檢查哪一個DLL實現你的類。爲簡單起見,假設使用的是進程內服務器。CoCreateInstance加載你的DLL並調用特殊函數DllGetClassObject.。繼承
// DLL 函數建立COM對象類工廠 DllGetClassObject(REFCLSID rclsid, // 類 ID REFIID riid, // 接口 ID LPVOID* ppv) // 返回的接口指針
實際上,DllGetClassObject得不到你的類實例,它獲得的是類工廠(IClassFactory)實例,經過這個類工廠來建立你得類實例。很怪是否是?MFC提供DllGetClassObject的實現,任務是搜索DLL中的全部類工廠,看看哪一個類的ID寓所請求的類ID匹配。MFC是如何知道搜索哪一個類工廠呢?當你在編寫子記得類代碼時,用宏DECLARE_OLECREATE 和 IMPLEMENT_OLECREATE聲明並實現你本身這個類的類工廠。尤爲是IMPLEMENT_OLECREATE聲明一個COleObjectFactory的靜態實例,這個對象將本身(經過調用類構造函數 COleObjectFactory::COleObjectFactory)添加到與模塊或DLL關聯的某個類工廠清單中。結果,給定某個類的ID,DllGetClassObject就知道如何發現與那個類關聯的類工廠。接口
一旦CoCreateInstance有了某個類工廠,它便調用IClassFactory::CreateInstance。在此MFC的COleObjectFactory又提供缺省的實現。
//作了許多精簡後 STDMETHODIMP COleObjectFactory::XClassFactory::CreateInstance(...) { METHOD_PROLOGUE_EX(COleObjectFactory, ClassFactory) …… // 這裏省略了許多代碼 CCmdTarget* pTarget = pThis->OnCreateObject(); return pTarget->InternalQueryInterface(&riid, ppvObject); }
這裏省略了許多瑣碎代碼以便突出重點,這些代碼包括:建立一個實例和調用InternalQueryInterface。OnCreateObject是個COleObjectFactory的虛擬函數,它經過MFC的運行時類建立你的類實例:
//一樣也做了簡化 CCmdTarget* COleObjectFactory::OnCreateObject() { return (CCmdTarget*)m_pRuntimeClass->CreateObject(); }
一旦COleObjectFactory::XClassFactory::CreateInstance有了你的類實例,便調用InternalQueryInterface獲取類的IUnknown接口指針。這裏省略了許多細節,例如,在實際代碼中MFC要調用IClassFactory2::CreateInstanceLic並檢查全部出錯條件以及聚合。InternalQueryInterface 和 ExternalQueryInterface之間的差異是外部版本委派外部IUnknown(若是有的話),而內部版本則否則。但即便聚合,最終都要歸到InternalQueryInterface。
若是還不明白,沒關係。類工廠的CreateInstance函數建立了一個你的類實例,並調用CCmdTarget::InternalQueryInterface來獲取類的IUnknown指針。InternalQueryInterface在哪裏獲得你的類IUnknown接口指針呢?難以捉摸的QueryInterface又在哪裏?
經歷了一些步驟和函數調用以後,CCmdTarget::InternalQueryInterface最終要調用一個函數:
LPUNKNOWN CCmdTarget::GetInterface(const void* iid) { LPUNKNOWN ptr = NULL; if (iid == IID_IUnknown) { ptr = // 接口映射的第一個接口 } else { ptr = // 在接口映射中查找iid } return ptr; // (若是沒找到則爲NULL) }
換句話說,若是請求的接口是IUnknown,則GetInterface返回接口映射中的第一個接口指針,不然它查找與請求的接口ID匹配的那一個。這裏的訣竅在於:你的類所提供的每個接口都必須和基類接口同樣實現IUnknown,而且還要以相同的方式實現,至於InternalQueryInterface返回哪個接口指針並不重要。CCmdTarget類自身沒有QueryInterface函數,只有嵌套類有,這個嵌套類實現每一個接口,每一個接口又都實現IUnknown。
圖一是一個典型的COM類實現。.CPP文件說明了你必須爲你的類所提供的每個接口編寫一樣煩人IUnknown實現。每個IUnknown方法爲父類(從CcmdTarget派生)調用相應的ExternalXxx(或者InternalXxx——若是你不想要聚合)方法。這個實現對於你編寫的每個接口都同樣。這是沒有辦法的,由於全部的接口都經過相同的單對象在內存中被實例化。AddRef 和Release必須增長和減小相同的物理指針——與AddRef 或Release實際屬於哪一個接口(嵌套類)無關。此乃高招所在。
這就是爲何在IUnknown的狀況下只有InternalQueryInterface能返回你的接口映射中的第一個接 口指針。因爲僅僅實現IUnknown的類沒什麼用,因此在你的映射中至少還要實現一個接口。若是你不明白,下面是具體步驟的解釋:
一、客戶端調用CoCreateInstance建立你的類實例。
二、CoCreateInstance查找並加載DLL,調用DllGetClassObject
三、DllGetClassObject搜索DLLs的類工廠清單找出與類工廠匹配的類ID,而後返回這個類工廠的指針。
四、CoCreateInstance調用IClassFactory::CreateInstance建立你的應用實例。
五、COleObjectFactory::XClassFactory::CreateInstance 調用 CRuntimeClass::CreateInstance在內存中建立你的MFC類實例。而後CreateInstance調用InternalQueryInterface獲取你的類IUnknown接口指針。InternalQueryInterface依次調用CCmdTarget::GetInterface.。
六、若是所請求的接口是IUnknown之外的其它接口,則GetInterface在你的類接口映射中查找這個接口。不然,GetInterface返回你的類接口映射中的第一個接口指針。這個工做對於實現IUnknown的每個嵌套類都是同樣的。
七、全部的函數返回、返回、返回……..而調用者只管接收某個接口指針。 誰說COM難學?
在結束本文的討論前,我想指出兩個有用的技巧和建議。重載MFC缺省行爲的方式有不少。第一,你能夠在類工廠中,有時你可能想從COleObjectFactory類派生本身專用的類工廠。只要你願意,能夠這麼作,訣竅是將類工廠掛鉤在對象上。不要使用DECLARE_OLECREATE 和 IMPLEMENT_OLECREATE,由於這些宏已經將ColeObjectFactory寫死在裏面了。但你能夠拷貝這些宏、更名以及江類工廠的名字改成本身的名字。尤爲是你可能要重載COleObjectFactory::OnCreateObject方法。例如,若是你的COM對象是單實例的,就要重載OnCreateObject返回一個且是惟一的一個對象實例(不要使用靜態對象,要否則可能遇到引用計數問題,由於靜態實例引用計數是1而且最終得不到Release釋放。應該取而代之用new在堆中分配單實例)。
最後,一個很是有用的重載是GetInterfaceHook。記得GetInterface嗎?這個函數在你的接口映射中查找接口,或若是請求的是IUnknown,則就返回第一個接口。下面是一段參考代碼:
LPUNKNOWN CCmdTarget::GetInterface(const void* iid) { // 容許常規構子首先起來 LPUNKNOWN lpUnk; if ((lpUnk = GetInterfaceHook(iid)) != NULL) return lpUnk; …… // 如前所述 }
在作其它事情以前,GetInterface調用虛函數CCmdTarget::GetInterfaceHook,缺省CCmdTarget實現返回NULL,但若是你想以某種特別方式實現QueryInterface接口的話,只要重載GetInterfaceHook並返回別的東西就好了。一旦GetInterfaceHook的返回值爲非空接口指針,則它首先調用QueryInterface,MFC將用到它。在個人另外一篇文章中,曾討論過如何重載GetInterfaceHook來完成一個徹底不一樣的COM實現,其中用了ATL風格的多繼承代替了MFC的嵌套類。 誰說COM複雜難懂?