本文為剛剛接觸COM的程序員提供編程指南,解釋COM服務器內幕以及如何用C++編寫自己的接口。
繼上一篇COM編程入門之後,本文將討論有關COM服務器的內容,解釋編寫自己的COM接口和COM服務器所需要的步驟和知識,以及詳細討論當COM庫對COM服務器進行調用時,COM服務器運行的內部機制。
如果你讀過上一篇文章。應該很熟悉COM客戶端是怎麼會事了。本文將討論COM的另一端——COM服務器。內容包括如何用C++編寫一個簡單的不涉及類庫的COM服務器。深入到創建COM服務器的內部過程,毫無遮掩地研究那些庫代碼是充分理解COM服務器內部機制的最好方法。
本文假設你精通C++並掌握了上一篇文章所討論的概念和術語。在這一部分將包括如下內容:
走馬觀花看COM服務器——描述COM服務器的基本要求。
服務器生命其管理——描述COM服務器如何控制加載時間。
實現接口,從IUnknown開始——展示如何用C++類編寫一個接口實現並描述IUnknown之方法的目的。
深入CoCreateInstance()——探究CoCreateInstance()的調用機理。
COM服務器的注冊——描述完成服務器注冊所需要的注冊表入口。
創建COM對象——類工廠——描述創建客戶端要使用的COM對象的過程。
一個定制接口的例子——例子代碼示范了上述概念。
一個使用服務器的客戶端——舉例說明一個簡單的客戶端應用程序,用它來測試COM服務器。
其它內容——有關源代碼和調試的注釋。
走馬觀花看COM服務器
本文我們將討論最簡單的一種COM服務器,進程內服務器(in-process)。“進程內”意思是服務器被加載到客戶端程序的進程空間。進程內服務器都是DLLs,並且與客戶端程序同在一台計算機上。
進程內服務器在被COM庫使用之前必須滿足兩個條件或標准:
1、 必須正確在注冊表的HKEY_CLASSES_ROOT\CLSID 鍵值下注冊。
2、 必須輸出DllGetClassObject()函數。
這是進程內服務器運行的最小需求。在注冊表的HKEY_CLASSES_ROOT\CLSID 鍵值下必須創建一個鍵值,用服務器的GUID作為鍵名字,這個鍵值必須包含兩個鍵值清單,一是服務器的位置,而是服務器的線程模型。 COM庫對DllGetClassObject()函數進行調用是在CoCreateInstance() API中完成的。
還有三個函數通常也要輸出:
o DllCanUnloadNow():由COM庫調用來檢查是否服務器被從內存中卸載。
o DllRegisterServer():由類似RegSvr32的安裝實用程序調用來注冊服務器。
o DllUnregisterServer():由卸載實用程序調用來刪除由DllRegisterServer()創建的注冊表入口。
另外,只輸出正確的函數是不夠的——還必須遵循COM規范,這樣COM庫和客戶端程序才能使用服務器。
服務器生命其管理
DLL服務器的一個與眾不同的方面是控制它們被加載的時間。“標准的”DLLs被動的並且是在應用程序使用它們時被隨機加載/或卸載。從技術上講,DLL服務器也是被動的,因為不管怎樣它們畢盡還是DLL,但COM庫提供了一種機制,它允許某個服務器命令COM卸載它。這是通過輸出函數DllCanUnloadNow()實現的。這個函數的原型如下:
HRESULT DllCanUnloadNow();
當客戶應用程序調用COM API CoFreeUnusedLibraries()時,通常出於其空閒處理期間,COM庫遍歷這個客戶端應用已加載所有的DLL服務器並通過調用它的DllCanUnloadNow()函數查詢每一個服務器。另一方面,如果某個服務器確定它不再需要駐留內存,它可以返回S_OK讓COM將它卸載。
服務器通過簡單的引用計數來確定它是否能被卸載。下面是DllCanUnloadNow()的實現:
extern UINT g_uDllRefCount; // 服務器的引用計數
HRESULT DllCanUnloadNow()
{
return (g_uDllRefCount > 0) ? S_FALSE : S_OK;
}
如何處理引用計數將在下一節涉及到具體代碼時討論。
實現接口,從IUnknown開始
有必要回想一下IUnknown派生的每一個接口。因為IUnknown包含了兩個COM對象的基本特性——引用計數和接口查詢。當你編寫組件對象類時(coclass),還要寫一個滿足自己需要的IUnknown實現。以實現IUnknown接口的組件對象類為例——下面這個例子可能是你編寫的最簡單的一個組件對象類。我們將在一個叫做CUnknownImpl的C++類中實現IUnknown。下面是這個類的聲明:
class CUnknownImpl : public IUnknown
{
public:
// 構造函數和析構器
CUnknownImpl();
virtual ~CUnknownImpl();
// IUnknown 方法
ULONG AddRef();
ULONG Release)();
HRESULT QueryInterface( REFIID riid, void** ppv );
protected:
UINT m_uRefCount; // 對象的引用計數
};
構造器和析構器
構造器和析構器管理服務器的引用計數:
CUnknownImpl::CUnknownImpl()
{
m_uRefCount = 0;
g_uDllRefCount++;
}
CUnknownImpl::~CUnknownImpl()
{
g_uDllRefCount--;
}
當創建新的COM對象時,構造器被調用,它增加服務器的引用計數以保持這個服務器駐留內存。同時它還將對象的引用計數初始化為零。當這個COM對象被摧毀時,它減少服務器的引用計數。
AddRef()和Release()
這兩個方法控制COM對象的生命期。AddRef()很簡單:
ULONG CUnknownImpl::AddRef()
{
return ++m_uRefCount;
}
AddRef()只增加對象的引用計數並返回更新的計數。
Release()更簡單:
ULONG CUnknownImpl::Release()
{
ULONG uRet = --m_uRefCount;
if ( 0 == m_uRefCount ) // 是否釋放了最後的引用?
delete this;
return uRet;
}
除了減少對象的引用計數外,如果沒有另外的明確引用,Release()將摧毀對象。Release()也返回更新的引用計數。注意Release()的實現假設COM對象在堆中創建。如果你在全局粘上創建某個對象,當對象試圖刪除自己時就會出問題。
現在應該明白了為什麼在客戶端應用程序中正確調用AddRef()和 Release()是如此重要!如果在這了做得不對,你使用的對象會被很快摧毀,這樣的話在整個服務器中內存會很快溢出導致應用程序下次存取服務器代碼時崩潰。
如果你編寫多線程應用,可能會想到使用++&替代InterlockedIncrement()和InterlockedDecrement()的線程安全問題。++&——用於單線程服務器很保險,因為即使客戶端應用是多線程的並從不同的線程中進行方法調用,COM庫都會按順序進行服務器的方法調用。也就是說,一旦一個方法調用開始,所有其它試圖調用方法的線程都將阻塞,直到第一個方法返回。COM庫本身確保服務器一次不會被一個以上的線程闖入。
QueryInterface()
QueryInterface()簡稱QI(),由客戶端程序調用這個函數從COM對象請求不同的接口。我們在例子代碼中因為只實現一個接口,QI()會很容易使用。QI()有兩個參數:一個是所請求的接口IID,一個是指針的緩沖大小,如果查詢成功,QI()將接口指針地址存儲在這個緩沖指針中。
HRESULT CUnknownImpl::QueryInterface ( REFIID riid, void** ppv )
{
HRESULT hrRet = S_OK;
// 標准QI()初始化 – 置 *ppv 為 NULL.
*ppv = NULL;
// 如果客戶端請求提供的接口,給 *ppv.賦值
if ( IsEqualIID ( riid, IID_IUnknown ))
{
*ppv = (IUnknown*) this;
}
else
{
// 不提供客戶端請求的接口
hrRet = E_NOINTERFACE;
}
// 如果返回一個接口指針。 調用AddRef()增加引用計數.
if ( S_OK == hrRet )
{
((IUnknown*) *ppv)->AddRef();
}
return hrRet;
}
在QI()中做了三件不同的事情:
1、初始化傳入的指針為NULL[*ppv = NULL;]。
2、檢查riid,確定組件對象類(coclass)實現了客戶端所請求接口.
[if ( IsEqualIID ( riid, IID_IUnknown ))]
3、如果確實實現勒索請求的接口,則增加COM對象的引用計數。
[((IUnknown*) *ppv)->AddRef();]
AddRef()調用很關鍵。
*ppv = (IUnknown*) this;
要創建新的COM對象引用,就必須調用這個函數通知COM對象這個新引用成立。在AddRef()調用中的強制轉換IUnknown*看起來好像多余,但是在QI()中初始化的*ppv有可能不是IUnknown*類型,所以最好是養成習慣對之進行強行轉換。。
上面我們已經討論了一些DLL服務器的內部細節,接下來讓我們回頭看一看當客戶端調用CoCreateInstance()時是如何處理服務器的。
深入CoCreateInstance()
在本文的第一部分中,我們見過CoCreateInstance()API,其作用是當客戶端請求對象時,用它來創建對象。從客戶端的立場看,它是一個黑盒子。只要用正確的參數調用它即可得到一個COM對象。它並沒有什麼魔法,只是在一個定義良好的過程中加載COM服務器,創建請求的COM對象並返回所要的指針。就這些。
下面讓我們來浏覽一下這個過程。這裡要涉及到幾個不太熟悉的術語,但不用著急,後面會對它們作詳細討論。
1、客戶端程序調用CoCreateInstance(),傳遞組件對象類的CLSID以及所要接口的IID。
2、COM庫在HKEY_CLASSES_ROOT\CLSID.鍵值下查找服務器的CLSID鍵值,這個鍵值包含服務器的注冊信息。
3、COM庫讀取服務器DLL的全路徑並將DLL加載到客戶端的進程空間。
4、COM庫調用在服務器中DllGetClassObject()函數為所請求的組件對象類請求類工廠。
5、服務器創建一個類工廠並將它從DllGetClassObject()返回。
6、COM庫在類工廠中調用CreateInstance()方法創建客戶端程序請求的COM對象。
7、CreateInstance()返回一個接口指針到客戶端程序。
COM服務器注冊
COM服務器必須在Windows注冊表中正確注冊以後才能正常工作。如果你看一下注冊表中的HKEY_CLASSES_ROOT\CLSID鍵,就會發現大把大把子鍵,它們就是在這個計算機上注冊的COM服務器。當某個COM服務器注冊後(通常是用DllRegisterServer()進行注冊),就會以標准的注冊表格式在CLSID鍵下創建一個鍵,它名字為服務器的GUID。下面是一個這樣的例子:
{067DF822-EAB6-11cf-B56E-00A0244D5087}
大括弧和連字符是必不可少的,字母大小寫均可。
這個鍵的默認值是人可值別的組件對象類名,使用VC所帶的OLE/COM對象浏覽器可以察看到它們。
在GUID鍵的子鍵中還可以存儲其它信息。需要創建什麼子鍵依賴於COM服務器的類型以及COM服務器的使用方法。對於本文例子中這個簡單的進程內服務器,我們值需要一個子鍵:InProcServer32。
InProcServer32鍵包含兩個串:這兩個串的缺省值是服務器DLL的全路徑和線程模型值(ThreadingModel)。線程模型超出了本文所涉及的范圍,我們先接受這個概念,這裡我們指的是單線程服務器,用的模式為Apartment(即單線程公寓)。
創建COM對象——類工廠
回首看一看客戶端的COM,它是如何以自己獨立於語言的方式創建和銷毀COM對象。客戶端調用CoCreateInstance()創建新的COM對象。現在我們來看看它在服務器端是如何工作的。
你每次實現組件對象類的時候,都要寫一個旁類負責創建第一個組件對象類的實例。這個旁類就叫這個組件對象類的類工廠(class factory),其唯一目的是創建COM對象。之所以要一個類工廠,是因為語言無關的緣故。COM本身並不創建對象,因為它不是獨立於語言的也不是獨立於實現的。
當某個客戶端想要創建一個COM對象時,COM庫就從COM服務器請求類工廠。然後類工廠創建COM對象並將它返回客戶端。它們的通訊機制由函數DllGetClassObject()來提供。
術語 “類工廠”和“類對象”實際上是一回事。沒有那個單詞能精確描述類工廠的作用和義,但正是這個工廠創建了COM對象,而不是COM類所為。將“類工廠”理解成“對象工廠”可能會更有助於理解(實際上MFC就是這樣理解的——它的類工廠實現就叫做COleObjectFactory)。但“類工廠”是正式術語,所以本文也這樣用。
當COM庫調用DllGetClassObject()時,它傳遞客戶端請求的CLSID。服務器負責為所請求的CLSID創建者各類工廠並將它返回。類工廠本身就是一個組件對象類,並且實現IClassFactory接口。如果DllGetClassObject()調用成功,它返回一個IClassFactory指針給COM庫,然後COM庫用IClassFactory接口方法創建客戶端所請求的COM對象實例。
一下是IClassFactory接口:
struct IClassFactory : public IUnknown
{
HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppvObject );
HRESULT LockServer( BOOL fLock );
};
其中,CreateInstance()是創建COM對象的方法。LockServer()在必要時讓COM庫增加或減少服務器的引用計數。
一個定制接口的例子
這個工程是一個能運行的DLL服務器例子,對象由類工廠創建,此DLL服務器在 CSimpleMsgBoxImpl組件對象類中實現了一個接口:ISimpleMsgBox。
接口定義
我們的新接口是ISimpleMsgBox。所有的接口多必須從IUnknown派生。這個接口只有一個方法:DoSimpleMsgBox()。注意它返回標准類型HRESULT。所有的方法都應該返回HRESULT類型,並且所有返回到調用者的其它數據都應該通過指針參數操作。
struct ISimpleMsgBox : public IUnknown
{
// IUnknown 方法
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface( REFIID riid, void** ppv );
// ISimpleMsgBox方法
HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );
};
struct __declspec(uuid("{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}")) ISimpleMsgBox;
有__declspec的一行將一個GUID賦值給ISimpleMsgBox,並且以後可以用__uuidof操作符來獲取GUID。這兩個東西都是微軟的C++的擴展。
DoSimpleMsgBox()的第二個參數是BSTR類型。意思是二進制串——即定長序列位的COM表示。BSTRs主要用於Visual Basic 和 Windows Scripting Host之類的腳本客戶端。
接下來這個接口由CSimpleMsgBoxImpl C++類來實現。其定義如下:
class CSimpleMsgBoxImpl : public ISimpleMsgBox
{
public:
CSimpleMsgBoxImpl();
virtual ~CSimpleMsgBoxImpl();
// IUnknown 方法
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface( REFIID riid, void** ppv );
// ISimpleMsgBox 方法
HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );
protected:
ULONG m_uRefCount;
};
class __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}")) CSimpleMsgBoxImpl;
當某一客戶端想要創建一個SimpleMsgBox COM對象時,它應該用下面這樣的代碼:
ISimpleMsgBox* pIMsgBox;
HRESULT hr;
// 組件對象類的CLSID
hr = CoCreateInstance ( __uuidof(CSimpleMsgBoxImpl), NULL, // 非聚合
CLSCTX_INPROC_SERVER, // 進程內服務器
__uuidof(ISimpleMsgBox), // 所請求接口的IID
(void**) &pIMsgBox ); // 返回的接口指針的地址
類工廠實現
我們的類工廠SimpleMsgBox是在一個叫做CSimpleMsgBoxClassFactory的C++類中實現的:
class CSimpleMsgBoxClassFactory : public IClassFactory
{
public:
CSimpleMsgBoxClassFactory();
virtual ~CSimpleMsgBoxClassFactory();
// IUnknown方法
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface( REFIID riid, void** ppv );
// IClassFactory方法
HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppv );
HRESULT LockServer( BOOL fLock );
protected:
ULONG m_uRefCount;
};
構造函數、析構函數和IUnknown方法都和前面例子中的一樣,不同的只有IClassFactory的方法,LockServer(),看起來相當更簡單:
HRESULT CSimpleMsgBoxClassFactory::LockServer ( BOOL fLock )
{
fLock ? g_uDllLockCount++ : g_uDllLockCount--;
return S_OK;
}
CreateInstance()是重點。我們說過這個方法負責創建新的CSimpleMsgBoxImpl對象。讓我們進一步探討一下它的原型和參數:
HRESULT CSimpleMsgBoxClassFactory::CreateInstance ( IUnknown* pUnkOuter,
REFIID riid,
void** ppv );
第一個參數pUnkOuter只用於聚合的新對象,指向“外部的”COM對象,也就是說,這個“外部”對象將包含此新對象。對象的聚合超出了本文的討論范圍,本文的例子對象也不支持聚合。
riid 和ppv 與在QueryInterface()中的用法一樣——它們是客戶端所請求的接口IID和存儲接口指針的指針緩沖。
下面是CreateInstance()的實現。它從參數的有效性檢查和參數的初始化開始。
HRESULT CSimpleMsgBoxClassFactory::CreateInstance ( IUnknown* pUnkOuter,
REFIID riid,
void** ppv )
{
// 因為不支持聚合,所以這個參數pUnkOuter必須為NULL.
if ( NULL != pUnkOuter )
return CLASS_E_NOAGGREGATION;
//檢查指針ppv是不是void*類型
if ( IsBadWritePtr ( ppv, sizeof(void*) ))
return E_POINTER;
*ppv = NULL;
檢查完參數的有效性後,就可以創建一個新的對象了。
CSimpleMsgBoxImpl* pMsgbox;
// 創建一個新的COM對象
pMsgbox = new CSimpleMsgBoxImpl;
if ( NULL == pMsgbox )
return E_OUTOFMEMORY;
最後,用QI()來查詢客戶端所請求的新對象的接口。如果QI()失敗,則這個對象不可用,必須刪除它。
HRESULT hrRet;
// 用QI查詢客戶端所請求的對象接口
hrRet = pMsgbox->QueryInterface ( riid, ppv );
// 如果QI失敗,則刪除這個COM對象,因為客戶端不能使用它(客戶端沒有
//這個對象的任何接口)
if ( FAILED(hrRet) )
delete pMsgbox;
return hrRet;
}
深入DllGetClassObject()
現在讓我們深入DllGetClassObject()內部。它的原型是:
HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv );
rclsid是客戶端所請求的組件對象類的CLSID。這個函數必須返回指定組件對象類的類工廠。
這裡的兩個參數: riid 和 ppv類似QI()的參數。不過在這個函數中,riid指的是COM庫所請求的類工廠接口的IID。通常就是IID_IClassFactory。
因為DllGetClassObject()也創建一個新的COM對象(類工廠),所以代碼與IClassFactory::CreateInstance()十分相似。開始也是進行一些有效性檢查以及初始化。
HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv )
{
// 檢查客戶端所要的CSimpleMsgBoxImpl類工廠
if ( !InlineIsEqualGUID ( rclsid, __uuidof(CSimpleMsgBoxImpl) ))
return CLASS_E_CLASSNOTAVAILABLE;
//檢查指針ppv是不是void*類型
if ( IsBadWritePtr ( ppv, sizeof(void*) ))
return E_POINTER;
*ppv = NULL;
第一個if語句檢查rclsid參數。我們的服務器只有一個組件對象類,所以rclsid必須是CSimpleMsgBoxImpl類的CLSID。__uuidof操作符獲取先前在__declspec(uuid())聲明中指定的CsimpleMsgBoxImpl類的GUID。
下一步是創建一個類工廠對象。
CSimpleMsgBoxClassFactory* pFactory;
// 構造一個新的類工廠對象
pFactory = new CSimpleMsgBoxClassFactory;
if ( NULL == pFactory )
return E_OUTOFMEMORY;
這裡的處理與CreateInstance()中所做的有所不同。在CreateInstance()中是調用了QI(),並且如果調用失敗,則刪除COM對象。
我們可以把自己假設成一個所創建的COM對象的客戶端,調用AddRef()進行一次引用計數(COUNT = 1)。然後調用QI()。如果QI()調用成功,它將再一次用AddRef()進行引用計數(COUNT = 2)。如果QI()調用失敗。引用計數將保持為原來的值(COUNT = 1)。
在QI()調用之後,類工廠對象就使用完了,因此要調用Release()來釋放它。如果QI()調用失敗,這個對象將自我刪除(因為引用計數將為零),所以最終結果是一樣的。
// 調用AddRef()增加一個類工廠引用計數,因為我們正在使用它
pFactory->AddRef();
HRESULT hrRet;
// 調用QI()查詢客戶端所要的類工廠接口
hrRet = pFactory->QueryInterface ( riid, ppv );
// 使用完類工廠後調用Release()釋放它
pFactory->Release();
return hrRet;
}
再談QueryInterface()
前面討論過QI()的實現,但還是有必要再看一看類工廠的QI(),因為它是一個很現實的例子,其中COM對象實現的不光是IUnknown。首先進行的是對ppv緩沖的有效性檢查以及初始化。
HRESULT CSimpleMsgBoxClassFactory::QueryInterface( REFIID riid, void** ppv )
{
HRESULT hrRet = S_OK;
//檢查指針ppv是不是void*類型
if ( IsBadWritePtr ( ppv, sizeof(void*) ))
return E_POINTER;
//標准的QI初始化,將賦值為NULL.
*ppv = NULL;
接下來檢查riid,看看它是不是類工廠實現的接口之一:IUnknown 或 IclassFactory。
// 如果客戶端請求一個有效接口,則扶植給 *ppv.
if ( InlineIsEqualGUID ( riid, IID_IUnknown ))
{
*ppv = (IUnknown*) this;
}
else if ( InlineIsEqualGUID ( riid, IID_IClassFactory ))
{
*ppv = (IClassFactory*) this;
}
else
{
hrRet = E_NOINTERFACE;
}
最後,如果riid是有效接口,則調用接口的AddRef(),然後返回。
//如果返回有效接口指針,則調用AddRef()
if ( S_OK == hrRet )
{
((IUnknown*) *ppv)->AddRef();
}
return hrRet;
}
ISimpleMsgBox實現
最後的也是必不可少的一關是ISimpleMsgBox實現,我們的代碼只實現ISimpleMsgBox的方法DoSimpleMsgBox()。首先用微軟的擴展類_bstr_t將bsMessageText轉換成TCHAR串。
HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent, BSTR bsMessageText )
{
_bstr_t bsMsg = bsMessageText;
LPCTSTR szMsg = (TCHAR*) bsMsg; // 如果需要的話,用_bstr_t將串轉換為ANSI
做完轉換的工作後,顯示信息框,然後返回。
MessageBox ( hwndParent, szMsg, _T("Simple Message Box"), MB_OK );
return S_OK;
}
使用服務器的客戶端
我們已經完成了一個超級棒的COM服務器,如何使用它呢? 我們的接口一個定制接口,也就是說它只能被C或C++客戶端使用。(如果在組件對象類中同時實現IDispatch接口,那我們幾乎就可以在任何客戶端環境中——Visual Basic,Windows Scripting Host,Web頁面,PerlScript等使用COM對象。有關這方面的內容我們留待另外的文章討論)。本文提供了一個使用ISimpleMsgBox的例子程序。這個程序基於用Win32應用程序向導建立的Hello World例子。文件菜單包含兩個測試服務器的命令:
如圖所示:
Test MsgBox COM Server菜單命令創建CSimpleMsgBoxImpl對象並調用DoSimpleMsgBox()。因為這
是個簡單的方法,要寫的代碼不長。
我們先用CoCreateInstance()創建一個COM對象。
void DoMsgBoxTest(HWND hMainWnd)
{
ISimpleMsgBox* pIMsgBox;
HRESULT hr;
hr = CoCreateInstance ( __uuidof(CSimpleMsgBoxImpl), // 組件對象類的CLSID
NULL, // 非聚合
CLSCTX_INPROC_SERVER, // 只使用進程內服務器
__uuidof(ISimpleMsgBox), // 所請求接口的IID
(void**) &pIMsgBox ); // 容納接口指針的緩沖
if ( FAILED(hr) )
return;
然後調用DoSimpleMsgBox()方法並釋放接口。
pIMsgBox->DoSimpleMsgBox ( hMainWnd, _bstr_t("Hello COM!") );
pIMsgBox->Release();
}
就這麼簡單。代碼中從頭到尾都有TRACE語句,這樣在調試器中運行測試程序就可以看到服務器的每一個方法
是如何被調用的。
另外一個菜單命令是調用CoFreeUnusedLibraries()函數,從中你能看到服務器DllCanUnloadNow()函數的運行。
其它細節-COM宏
COM代碼中有些宏隱藏了實現細節,並允許在C和C++客戶端使用相同的聲明。本文中沒有使用宏,但在例子代
碼中用到了這些宏,所以必須掌握它們的用法。下面是ISimpleMsgBox的聲明
struct ISimpleMsgBox : public IUnknown
{
// IUnknown 方法
STDMETHOD_(ULONG, AddRef)() PURE;
STDMETHOD_(ULONG, Release)() PURE;
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
// ISimpleMsgBox 方法
STDMETHOD(DoSimpleMsgBox)(HWND hwndParent, BSTR bsMessageText) PURE;
};
STDMETHOD()包含virtual關鍵字,返回類型和調用規范。STDMETHOD_()也一樣,除非你指定不
同的返回類型。PURE擴展了C++的“=0”,使此函數成為一個純虛擬函數。
STDMETHOD()和STDMETHOD_()有對應的宏用於方法實現——STDMETHODIMP和STDMETHODIMP_()。
例如DoSimpleMsgBox()的實現:
STDMETHODIMP CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent, BSTR bsMessageText )
{
...
}
最後,標准的輸出函數用STDAPI宏聲明,如:
STDAPI DllRegisterServer()
STDAPI包括返回類型和調用規范。要注意STDAPI不能和__declspec(dllexport)一起使用,
因為STDAPI的擴展。輸出必須使用.DEF文件。
服務器注冊以及反注冊
前面講過服務器實現了DllRegisterServer()和DllUnregisterServer()兩個函數。它們的工作是創建和
刪除關於COM服務器的注冊表入口。其代碼都是對注冊表的處理,所以在此不必贅言,只是列出DllRegisterServer()創建的注冊表入口:
鍵名
鍵值
HKEY_CLASSES_ROOT
CLSID
{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}
Default="SimpleMsgBox class"
InProcServer32
Default=[path to DLL]; ThreadingModel="Apartment"
關於例子代碼的注釋
本文的例子代碼在一個WORKSPACE(工作間)文件中(SimpleComSvr.dsw)同時包含了服務器的源代碼和測試服
務器所用的客戶端源代碼。在VC的IDE環境中可以同時加載它們進行處理。在工作間的同級層次有兩個工程都要
用到的頭文件,但每個工程都有自己的子目錄。
同級的公共頭文件是:
ISimpleMsgBox.h——定義ISimpleMsgBox的頭文件。
SimpleMsgBoxComDef.h——包含__declspec(uuid())的聲明。這些聲明都在單獨的文件中,因為客戶
端需要CSimpleMsgBoxImpl的GUID,不是它的定義。將GUID移到單獨的文件中,使客戶端在存取GUID時不依賴
CSimpleMsgBoxImpl的內部結構。它是接口,ISimpleMsgBox,對客戶端很重要。
正如前面所說的,必須用.DEF文件來從服務器輸出四個標准的輸出函數。下面是例子工程的.DEF文件:
EXPORTS
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
每一行都包含函數名和PRIVATE關鍵字。這個關鍵字的意思是:此函數是輸出函數,但不包含在輸入庫(import lib)中。也就是說客戶端不能直接從代碼中調用這個函數,即使是鏈接了輸入庫也不行。這個關鍵字時必須要用的,否則鏈接器會出錯。
在服務器中設置斷點鏈
如果你想在服務器代碼中設置斷點,有兩種方法:第一種是將服務器工程(MsgBoxSvr)設置為活動工程,然後開始調試。MSVC將問你調試會話要運行的可執行程序。輸入客戶端測試程序的全路徑,你必須事先建立好。第二種方法是將客戶端工程(TestClient)設置為活動工程,配置工程的從屬(dependencies)屬性,以便服務器工程從屬於客戶端工程。這樣如果你改變了服務器的代碼,那麼在編譯客戶端工程時會自動重新編譯服務器工程代碼。最後還要做的是當你開始調試客戶端時必須告訴MSVC加載服務器符號(symbols)。
下面是設置工程屬性的對話框:Project->Dependencies菜單
為了加載服務器符號,打開TestClient的工程設置(Project->Settings菜單),選擇Debug標簽,並在Category組合框中選擇Additional DLLs。在列表框中單擊New一個入口,然後輸入服務器DLL的全路徑名。如下圖所示:
這樣設置以後,根據實際源代碼的所在位置,DLL的路徑將會做自動調整。
(第二部分完)
本文配套源碼