青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

C++ Programmer's Cookbook

{C++ 基礎} {C++ 高級} {C#界面,C++核心算法} {設計模式} {C#基礎}

COM編程入門(2)

COM編程入門

第二部分 深入COM服務器

文/趙湘寧

下在例子代碼:src    demo
     本文為剛剛接觸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 client screen shot - 12K]
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菜單
 [Project dependencies - 7K]
為了加載服務器符號,打開TestClient的工程設置(Project->Settings菜單),選擇Debug標簽,并在Category組合框中選擇Additional DLLs。在列表框中單擊New一個入口,然后輸入服務器DLL的全路徑名。如下圖所示:
 [Debug settings - 15K]
這樣設置以后,根據實際源代碼的所在位置,DLL的路徑將會做自動調整。
(第二部分完)

COM編程入門

第二部分 深入COM服務器

文/趙湘寧

下在例子代碼:src    demo
     本文為剛剛接觸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 client screen shot - 12K]
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菜單
 [Project dependencies - 7K]
為了加載服務器符號,打開TestClient的工程設置(Project->Settings菜單),選擇Debug標簽,并在Category組合框中選擇Additional DLLs。在列表框中單擊New一個入口,然后輸入服務器DLL的全路徑名。如下圖所示:
 [Debug settings - 15K]
這樣設置以后,根據實際源代碼的所在位置,DLL的路徑將會做自動調整。
(第二部分完)
v

posted on 2005-12-23 13:09 夢在天涯 閱讀(1887) 評論(0)  編輯 收藏 引用 所屬分類: COM/ATL

公告

EMail:itech001#126.com

導航

統計

  • 隨筆 - 461
  • 文章 - 4
  • 評論 - 746
  • 引用 - 0

常用鏈接

隨筆分類

隨筆檔案

收藏夾

Blogs

c#(csharp)

C++(cpp)

Enlish

Forums(bbs)

My self

Often go

Useful Webs

Xml/Uml/html

搜索

  •  

積分與排名

  • 積分 - 1814982
  • 排名 - 5

最新評論

閱讀排行榜

青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
      <noscript id="pjuwb"></noscript>
            <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
              <dd id="pjuwb"></dd>
              <abbr id="pjuwb"></abbr>
              亚洲一区国产一区| 久久久久久综合| 国产精品激情av在线播放| 欧美激情一区三区| 欧美日韩国产成人在线观看| 欧美精品久久久久久久久老牛影院| 美女91精品| 欧美视频第二页| 国产日韩精品在线| 亚洲成人资源网| 这里是久久伊人| 久久激情视频免费观看| 欧美成人在线免费观看| 亚洲美女淫视频| 性欧美激情精品| 欧美r片在线| 国产精品稀缺呦系列在线| 一区二区亚洲精品国产| 亚洲精品人人| 欧美在线一级视频| 亚洲国语精品自产拍在线观看| 亚洲精品国产精品国自产在线| 亚洲一区二区少妇| 久久综合给合久久狠狠色| 欧美精品一区二| 国内精品久久久久久久影视蜜臀| 亚洲精品一品区二品区三品区| 亚洲四色影视在线观看| 国产精品二区三区四区| 国产亚洲精品久久久久婷婷瑜伽| 91久久亚洲| 久久精品国产亚洲精品| 日韩一本二本av| 久久久久久香蕉网| 国产精品视频久久久| 99精品久久久| 欧美成人精品福利| 午夜影院日韩| 国产精品理论片| 日韩午夜电影在线观看| 美女尤物久久精品| 亚洲综合激情| 国产精品黄色| 一区二区成人精品| 亚洲国产精品久久人人爱蜜臀| 久久国产日韩| 国产一级揄自揄精品视频| 亚洲欧美春色| 宅男精品视频| 国产精品大全| 亚洲永久字幕| 在线亚洲一区二区| 欧美少妇一区二区| 亚洲一级片在线观看| 亚洲欧洲中文日韩久久av乱码| 老鸭窝亚洲一区二区三区| 黄色在线一区| 久久综合色影院| 久久蜜桃香蕉精品一区二区三区| 激情亚洲网站| 欧美大胆人体视频| 欧美二区在线看| 日韩视频一区二区| 亚洲美女av在线播放| 欧美日韩视频在线一区二区| 一区二区三区黄色| 亚洲夜间福利| 国产偷久久久精品专区| 久久亚洲影院| 欧美gay视频| 中文av字幕一区| 亚洲一区二区3| 韩日精品在线| 亚洲人成77777在线观看网| 欧美日韩一区二区在线观看| 亚洲欧美国产精品桃花| 欧美一区免费视频| 亚洲第一伊人| 99精品视频免费全部在线| 国产精品区免费视频| 久久久久久久999精品视频| 久久只有精品| 亚洲一区免费观看| 久久免费视频在线观看| 日韩视频在线一区二区三区| 中文av一区特黄| 狠狠色狠狠色综合人人| 亚洲国产精品一区在线观看不卡 | 亚洲国产婷婷| 欧美日韩视频在线一区二区| 欧美一区二区三区四区在线观看地址| 午夜精品久久久久影视| 在线观看欧美日本| 夜夜嗨av一区二区三区免费区| 国产精品欧美日韩一区二区| 欧美大秀在线观看| 国产精品日本精品| 欧美国产欧美综合 | 国产麻豆午夜三级精品| 欧美成人a∨高清免费观看| 欧美日韩一区二区三区在线 | 激情综合视频| 亚洲视频免费在线观看| 在线日韩中文字幕| 亚洲无限av看| 亚洲精品一区二区三区在线观看| 亚洲在线电影| 99riav久久精品riav| 欧美在线日韩| 亚洲欧美日韩综合aⅴ视频| 欧美.com| 久久在线免费| 国产午夜久久久久| 亚洲一区二区av电影| 一区二区三区回区在观看免费视频| 久久久久久91香蕉国产| 亚洲欧美中文另类| 欧美视频一区二区三区…| 亚洲高清资源综合久久精品| 国语自产精品视频在线看| 亚洲自啪免费| 午夜免费日韩视频| 欧美网站在线观看| 亚洲理论在线观看| 亚洲人成网站色ww在线| 久久久青草青青国产亚洲免观| 性18欧美另类| 国产精品国产精品| 亚洲色在线视频| 亚洲一区二区三区精品在线观看| 欧美高清在线视频观看不卡| 欧美激情国产日韩精品一区18| 极品av少妇一区二区| 久久精品中文字幕一区二区三区| 久久国产毛片| 国产一区二区三区在线免费观看 | 欧美在线free| 国产亚洲欧美日韩美女| 欧美一区二区三区在| 久久精品国产免费观看| 国产亚洲欧美色| 久久久精品视频成人| 欧美成人按摩| 亚洲狼人精品一区二区三区| 亚洲精品自在在线观看| 亚洲毛片在线看| 欧美女主播在线| 一本色道久久88亚洲综合88| 亚洲一区二区三区久久| 国产精品尤物福利片在线观看| 亚洲欧美日韩另类精品一区二区三区| 欧美一级专区| 在线视频观看日韩| 欧美国产日韩一区二区| 日韩视频一区二区| 欧美亚洲综合久久| 精品成人一区二区三区四区| 久久久久久网站| 亚洲激情影院| 小黄鸭视频精品导航| 永久久久久久| 欧美精品一区二区在线观看| 一区二区三区毛片| 久久狠狠一本精品综合网| 亚洲国产午夜| 国产精品日日摸夜夜摸av| 久久精品一区中文字幕| 亚洲精品综合久久中文字幕| 午夜久久黄色| 亚洲三级视频在线观看| 国产精品久久77777| 久久九九免费| 99在线精品观看| 免费观看久久久4p| 亚洲影视综合| 亚洲二区在线视频| 国产精品久久久久9999高清| 久久免费少妇高潮久久精品99| 日韩视频三区| 欧美va亚洲va国产综合| 欧美亚洲网站| 中文久久精品| 亚洲欧洲美洲综合色网| 国产伦精品一区二区三区免费| 欧美成人午夜激情视频| 香蕉精品999视频一区二区| 亚洲每日更新| 亚洲第一毛片| 老鸭窝毛片一区二区三区| 亚洲女优在线| 一区二区免费在线播放| 狠狠色狠狠色综合日日小说| 国产精品福利网站| 欧美精品久久久久久久| 巨乳诱惑日韩免费av| 欧美影院午夜播放| 亚洲综合国产激情另类一区| 一本久道综合久久精品| 91久久视频| 亚洲国产日韩欧美在线图片|