COM 介紹 Part2

Introduction to COM Part II - Behind the Scenes of a COM Server

本文章翻譯自如下鏈接:
http://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a

這篇文章的目的

我在 上一篇文章 中已經(jīng)說明過,寫這篇文章的目的是為了幫助那些剛剛?cè)腴T COM 編程的程序員更好地理解 COM 基本概念。上一篇文章介紹了 COM 的基本概念,這篇文章主要介紹 COM Server 相關(guān)的細(xì)節(jié),及如何編寫自己的 COM 接口和 COM Server, 并且介紹了 COM library 調(diào)用時 COM Server 中實際進(jìn)行的操作有哪些。

介紹

如果你已經(jīng)讀過我的 上一篇文章 , 你應(yīng)該已經(jīng)了解了如何從客戶端的角度來使用 COM Server。現(xiàn)在,該學(xué)習(xí) COM 的另外一端 COM Server 本身了。我將用純 C++ 代碼展示如何編寫一個 COM Server, 不會調(diào)用到任何其它類庫。通過這種方式,你能更好地理解 server 中所發(fā)生的事情。

這篇文章假定你熟悉 C++ 并且理解了上一篇文章說說的若干概念。本文包含如下幾個章節(jié):

  • ** 簡要介紹 COM Server ** 描述一個 COM Server 所需要具備的基本功能
  • ** Server 的生命周期管理 ** 描述 COM Server 如何控制它加載的時間
  • ** 從 IUnknown 開始,實現(xiàn)接口 ** 展示如何用 C++ class 來實現(xiàn)接口,并介紹 IUnknown 接口各函數(shù)的作用
  • ** 深入 CoCreateInstance() ** 簡要介紹當(dāng)你調(diào)用 CoCreateInstance() 時內(nèi)部所發(fā)生的操作
  • ** COM Server 的注冊 ** 介紹注冊 COM Server 時所需要設(shè)置的注冊表鍵值
  • ** 創(chuàng)建 COM 對象 - Class Factory ** 介紹為客戶端程序創(chuàng)建 COM 對象的流程
  • ** 一個簡單的自定義接口 ** 用一個簡單的例子說明之前的概念
  • ** 使用我們的 COM 對象 ** 寫一個簡單的客戶端程序來測試我們的 COM Server
  • ** 其它細(xì)節(jié) ** 在源碼和調(diào)試時需要注意的地方

簡要介紹 COM Server

在本文中,我們將看到一個最簡單的 COM Server,一個 in-process server(進(jìn)程內(nèi)服務(wù)), "In-process" 意味著服務(wù)將被加載到客戶端的進(jìn)程地址空間中。通常它們是 DLL,并且必須與客戶端程序同處于一臺機器上。

一個 in-proc server 能夠被 COM library 使用,必須滿足兩條準(zhǔn)則:

  1. 它必須被正確地注冊到 HKEY_CLASSES_ROOT\CLSID 鍵值中;
  2. 它必須導(dǎo)出一個函數(shù)叫做 DllGetClassObject();

對應(yīng)上述準(zhǔn)則,為了讓 in-proc server 正常工作,你至少要做以下工作:將 server 的 GUID 注冊到 HKEY_CLASSES_ROOT\CLSID 鍵值下,并且這個鍵值必須包含一堆值標(biāo)識 Server 的路徑及它的線程模型。 DllGetClassObject() 函數(shù)將在 CoCreateInstance() 中由 COM library 調(diào)用。

通常還有其它幾個接口需要被導(dǎo)出:

  • DllCanUnloadNow() : 由 COM library 調(diào)用從而判斷 server 能否從內(nèi)存中卸載;
  • DllRegisterServer() : 由安裝工具如 RegSvr32 調(diào)用從而讓 server 將自己注冊到注冊表中;
  • DllUnregisterServer() : 由下載工具調(diào)用從而讓 server 將自己從注冊表鍵值中移除;

當(dāng)然,僅僅導(dǎo)出正確的函數(shù)是不夠的,這些函數(shù)必須符合 COM 規(guī)范才能給 COM library 和 客戶端程序使用。

Server 的生命周期管理

對于 DLL Server,有一點容易忽視的是它其實能夠控制自己的加載時間。一般的 DLL 是“被動的”,它們只能通過程序的控制來加載或卸載。通常情況下, DLL Server 也是“被動的” ,畢竟它也是個 DLL。但 COM library 提供了一種機制使得 server 能夠告知 COM 它可以被卸載。這種機制是通過 DllCanUnloadNow() 接口來實現(xiàn)的。它的原型如下:

HRESULT DllCanUnloadNow();

客戶端程序可以在進(jìn)程空閑時調(diào)用 COM API CoFreeUnusedLibraries(),這個函數(shù)會調(diào)用所有 DLL Server 的這個函數(shù),判斷是否可以將這個 server 卸載。如果該函數(shù)返回 S_FALSE 表示不能被卸載,返回 S_OK 表示可以。

Server 本身判斷自己能否被卸載可以使用引用計數(shù)的方法,以下是一個簡單的實現(xiàn):

extern UINT g_uDllRefCount;

HRESULT DllCanUnloadNow()
{
    return (g_uDllRefCount >0) ? S_FALSE : S_OK;
}

下一章節(jié)會討論引用計數(shù)的細(xì)節(jié),并給出簡單的代碼示例。

從 IUnknown 開始,實現(xiàn)接口

再說一遍,所有的接口都繼承自 IUnknown. 這是因為 IUnknown 包含了 COM 對象的兩個基本特性——引用計數(shù)和接口查詢。當(dāng)你編寫一個 coclass 的時候,你同時也編寫了一個滿足你要求的 IUnknown 接口實現(xiàn)。接下來展示一段 coclass 代碼,它僅僅實現(xiàn)了 IUnknown 接口。

class CUnknownImpl : public IUnknown
{
public:
    // 構(gòu)造和析構(gòu)函數(shù)
    CUnknownImpl();
    virtual CUnknownImpl();

    // IUnknown 函數(shù)
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface(REFIID riid, void** ppv);

protected:
    // 對象的引用計數(shù)
    UINT m_uRefCount;
}

構(gòu)造和析構(gòu)函數(shù)

這里我們在構(gòu)造函數(shù)里管理 server 的引用計數(shù):

CUnknownImpl::CUnknownImpl()
{
    m_uRefCount = 0;
    g_uDllRefCount ++;
}

CUnknownImpl::~CUnknownImpl()
{
    g_uDllRefCount --;
}

當(dāng)新的 COM 對象被創(chuàng)建時,構(gòu)造函數(shù)將被調(diào)用,因此需要在這時 遞增 g_uDllRefCount,保證 Server 不被卸載。同時,在這時將 COM 對象的 m_uRefCount 設(shè)定為 0。 m_uRefCount 維護(hù)的是 COM 對象本身的引用計數(shù)。

而在 COM 對象被銷毀時,析構(gòu)函數(shù)將被調(diào)用,在這時遞減 g_uDllRefCount.

AddRef() 和 Release()

這兩個函數(shù)是用來控制 COM 對象的生命周期的:

ULONG CUnknownImpl::AddRef()
{
    return ++m_uRefCount;
}

AddRef() 簡單地遞增了引用計數(shù),并返回遞增后的引用計數(shù)。

ULONG CUnknownImpl::Release()
{
    ULONG uRet = --m_uRefCount;
    if ( 0 == m_uRefCount )
        delete this;
    
    return uRet;
}

Release() 中,除了遞減引用計數(shù)之外,當(dāng)它檢測到引用計數(shù)為 0 時,將銷毀 COM 對象。Release() 調(diào)用完之后會返回新的引用計數(shù)。注意,以上的實現(xiàn)假定 COM 對象是在堆中被創(chuàng)建的。如果你在棧上或者全局域上創(chuàng)建了 COM 對象,那么 delete this 操作將發(fā)生錯誤。

現(xiàn)在你應(yīng)該能理解為啥在 客戶端程序中正確調(diào)用 AddRef() 和 Release() 這么重要。如果你沒有正確調(diào)用它們,COM 對象可能會過早被銷毀,或者一直沒銷毀。如果銷毀過早的話,COM Server 可能會提前從內(nèi)存中卸載,導(dǎo)致下次調(diào)用接口時程序崩潰。

如果你要編寫的是多線程程序,你可能會擔(dān)心使用 ++,-- 而不是 InterlockedIncrement(), InterlockedDecrement() 而導(dǎo)致的線程安全問題。但是,如果你的 COM Server 是一個 single-thread server(單線程 Server), 那么 ++,-- 就不會有問題。即使客戶端程序是多線程的,并且在不同的線程中調(diào)用了 COM Server 的接口,COM library 也會以序列的方式調(diào)用 server 接口。也就是說,一個函數(shù)開始調(diào)用后,下一個視圖調(diào)用的函數(shù)會被暫時鎖住,直到第一個函數(shù)返回后才能執(zhí)行。COM library 保證了我們的 server 任何時候都不會有多于一個線程同時進(jìn)入訪問。

QueryInterface()

客戶端程序通過 QueryInterface() 從 COM 對象中請求不同的接口。因為我們的示例代碼只實現(xiàn)了一個接口,所以它很簡單。QueryInterface() 接受兩個參數(shù),接口的 IID、用于接收接口指針的緩沖區(qū)地址指針。

HRESULT CUnknownImpl::QueryInterface( REFIID riid, void** ppv)
{
    HRESULT hrRet = S_OK;
    *ppv = NULL;

    if ( IsEqualIID( riid, IID_IUnknown) )
    {
        *ppv = (IUnknown*) this;
    }
    else
    {
        hrRet = E_NOINTERFACE;
    }

    if ( S_OK == hrRet )
    {
        ((IUnknown*) *ppv)->AddRef();
    }

    return hrRet;
}

上面代碼做了三件事兒:

  1. 將傳入的指針初始化為 NULL; [*ppv = NULL;]
  2. 檢查 coclass 是否實現(xiàn)了 riid 所要求的接口;[if ( IsEqualIID( riid, IID_IUnknown) )]
  3. 如果返回了接口指針,增加 COM 對象的引用計數(shù);[((IUnknown*) *ppv)->AddRef();]

注意, AddRef() 很重要,這一行代碼:

*ppv = (IUnknown*) this;

創(chuàng)建了 COM 對象的一個新的引用,因此要調(diào)用 AddRef() 來告知 COM 對象這個新引用的存在。將 ppv 轉(zhuǎn)換為 IUnknown 然后再調(diào)用 AddRef() 看起來很奇怪。但在實際的代碼中, ppv 可能不只是一個 IUnknown,轉(zhuǎn)換后再去掉用 AddRef() 是一個好的習(xí)慣。

現(xiàn)在我們已經(jīng)了解了 DLL Server 的一些內(nèi)部細(xì)節(jié),接下來我們退回去看看 CoCreateInstance() 里是如何使用 server 的。

深入 CoCreateInstance()

在上一篇文章中,我們已經(jīng)介紹過 CoCreateInstance(),當(dāng)客戶端程序調(diào)用它時它將創(chuàng)建一個所需要的 COM 對象。從客戶端的角度來看, CoCreateInstance() 是一個黑盒,只要給他傳入正確的函數(shù),你就能得到一個 COM 對象。這里面可沒什么黑魔法,它有一套經(jīng)過完善定義的流程去處理 COM Server 的載入、COM 對象的創(chuàng)建、接口的返回。

以下是這個流程的概覽,有一些我們之前沒講過,接下來的章節(jié)里會詳細(xì)討論:

  1. 客戶端程序調(diào)用 CoCreateInstance(), 傳入 coclass 的 CLSID 和所需要的接口的 IID;
  2. COM library 在注冊表 HKEY_CLASSES_ROOT\CLSID 里查詢 server 的 CLSID。這個鍵值存儲了 server 的注冊表信息;
  3. COM library 讀取 server DLL 的全路徑并將 DLL 載入客戶端程序的進(jìn)程地址空間;
  4. COM library 調(diào)用 server 的 DllGetClassObject() 來獲取 coclass 的類工廠 class factory;
  5. server 創(chuàng)建一個類工廠,并通過 DllGetClassObject() 返回;
  6. COM library 調(diào)用類工廠的 CreateInstance() 函數(shù)來創(chuàng)建 COM 對象;
  7. CoCreateInstance() 返回 COM 對象的接口指針;

COM Server 的注冊

在一切開始之前,COM Server 必須被正確地注冊到 Windows 注冊表中。如果你去看一下 HKEY_CLASSES_ROOT\CLSID 鍵,你會發(fā)現(xiàn)它下面有一堆子鍵。HKCR\CLSID 存儲了當(dāng)前電腦中所有可用的 COM Server 信息。當(dāng)一個 COM Server 被注冊時(通常通過 DllRegisterServer()), 它會在 CLSID 鍵下面創(chuàng)建一個以 server 的 GUID 為名稱的子鍵。例如下面這種:

{067DF822-EAB6-11cf-B56E-00A0244D5087} = CLSID_UnknownImpl
    |
    |__> InprocServer32
            |
            |__> default = C:\UnknownImpl.dll
            |
            |__> ThreadingModel = Apartment

大括號和連字符都是必要的,但字母大小寫都可以;

這個 key 的默認(rèn) value 是一個可讀的 coclass 名稱,VC 的 OLE/COM Object Viewer 工具可以看到這個名稱;

詳細(xì)信息存儲在這個 key 的子鍵中,這個子鍵名稱如何依賴于你的 COM Server 是什么類型。對于我們這個簡單程序來說,設(shè)定為 InProcServer32 就好了。

InProcServer32 子鍵中又有兩個鍵值,分別是默認(rèn)值,它表示 server DLL 的全路徑;以及一個 ThreadingModel 值,它表示 server 所使用的線程模型。線程模型超出了本文的范圍,我們這兒直接把他設(shè)定為 Apartment 就好了。

創(chuàng)建 COM 對象 - Class Factory

從客戶端的角度去看 COM,我之前已經(jīng)說過 COM 對創(chuàng)建和銷毀對象有它一套自己的語言無關(guān)的機制。客戶端調(diào)用 CoCreateInstance() 來創(chuàng)建 COM 對象,現(xiàn)在我們從 Server 的角度來看看它是怎么工作的。

每當(dāng)你實現(xiàn)了一個 coclass, 同時你還需要寫一個 伙伴 coclass 來專門負(fù)責(zé)創(chuàng)建第一個 coclass 實例。這個伙伴 coclass 也被叫做 class factory. 它的核心功能就是創(chuàng)建 COM 對象。之所以要編寫 class factory 的原因就是為了之前說的“語言無關(guān)性”,COM 本身不能去創(chuàng)建對象,因為對象的創(chuàng)建是語言相關(guān)的。

當(dāng)客戶端試圖創(chuàng)建 COM 對象時, COM library 會先從 COM Server 里請求對應(yīng)的 class factory. 得到 class factory 之后,COM 利用它來創(chuàng)建對象然后返回給 客戶端。這一機制通過 DllGetClassObject() 來實現(xiàn)。

class factory 和 object factory 實際上表示的是同一個東西。object factory 更清楚地表述了 class factory 的功能,因為 factory 創(chuàng)建的是 COM object, 而不是 COM class. 用 object factory 來表述可能更清楚一些(實際上, MFC 就是這么干的,它的 class factory 的實現(xiàn)就叫做 COleObjectFactory)。然而,官方明確確實是叫 class factory, 所以本文也沿用這個名稱。

當(dāng) COM library 調(diào)用 DllGetClassObject() 的時候,它會傳入客戶端所請求的 CLSID。由 Server 負(fù)責(zé)創(chuàng)建 CLSID 對應(yīng)的 class factory 并返回。class factory 本身是一個實現(xiàn)了 IClassFactory 的 coclass. 如果 DllGetClassObject() 執(zhí)行成功,它將返回一個 IClassFactory 接口給 COM library, 它將使用這個接口來創(chuàng)建 COM 對象并返回給 客戶端程序。

IClassFactory 接口大概長這樣兒:

struct IClassFactory : public IUnknown
{
    HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppvObject);
    HRESUTL LockServer( BOOL bLock );
}

CreateInstance() 函數(shù)用于創(chuàng)建 COM 對象; LockServer() 函數(shù)可以讓 COM library 在必要的時候遞增或遞減 server 的引用計數(shù)。

一個簡單的自定義接口

下面介紹一個 DLL Server 的代碼例子,它定義了一個 ISimpleMsgBox 接口, 并用一個叫做 CSimpleMsgBoxImpl 的 coclass 實現(xiàn)了這個接口。

接口定義

我們的新接口叫做 ISimpleMsgBox,像其它接口一樣,它必須繼承自 IUnknown. 這個接口里只有一個函數(shù) DoSimpleMsgBox(), 注意它返回標(biāo)準(zhǔn)類型 HRESULT。你寫得所有函數(shù)都應(yīng)該以這個類型作為返回值,如果需要返回其它內(nèi)容,必須通過指針參數(shù)來返回。

struct ISimpleMsgBox : public IUnknown
{
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface( REFIID riid, void** ppv );

    HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );
};

struct __declspec(uuid("{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}"))
                ISimpleMsgBox;

上面代碼中的 __declspec 操作符將一個 GUID 復(fù)制給了 ISimpleMsgBox 符號,這個 GUID 隨后可以用 __uuidof 操作符來獲取到。__declspec__uuidof 這兩個操作符都是 微軟 C++ 擴展 中的操作符。

DoSimpleMsgBox() 的第二個參數(shù)是一個 BSTR 類型的值。BSTR 代表 "binary string", 這是 COM 里表示定長字節(jié)序列的一個類型。BSTR 主要用在腳本型客戶端像是 Visual Basic, Windows Scripting Host 里面。

定義完了接口,接下來用一個叫做 CSimpleMsgBoxImpl 的 C++ 類來實現(xiàn)這個接口:

class CSimpleMsgBoxImpl : ISimpleMsgBox
{
public:
    CSimpleMsgBoxImpl();
    virtual ~CSimpleMsgBoxImpl();

    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface( REFIID riid, void** ppv );

    HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );

protected:
    ULONG m_uRefCount;
}

class __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}"))
                CSimpleMsgBoxImpl;

客戶端可以用如下方式來創(chuàng)建一個 SimpleMsgBox COM 對象:

ISimpleMsgBox* pIMsgBox;
HRESULT hr;

hr = CoCreateInstance( __uuidof(CSimpleMsgBoxImpl),  // coclass 的 CLSID
                        NULL,
                        CLSCTX_INPROC_SERVER,
                        __uuidof(ISimpleMsgBox),    // interface 的 IID
                        (void**) &pIMsgBox );

The Class Factory

** 我們的 class factory 實現(xiàn) **

SimpleMsgBox 類的類工廠也用 C++ 類來實現(xiàn),叫做 CSimpleMsgBoxClassFactory :

class CSimpleMsgBoxClassFactory : public IClassFactory
{
public:
    CSimpleMsgBoxClassFactory();
    virtual ~CSimpleMsgBoxClassFactory();
    
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface(REFIID riid, void** ppv);

    HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppv);
    HRESULT LockServer( BOOL bLock );

protected:
    ULONG m_uRefCount;
}

構(gòu)造、析構(gòu)函數(shù)、 IUnknown 接口函數(shù)都像之前的例子那樣寫就可以了。跟之前不同的是我們要實現(xiàn) IClassFactory 接口中的函數(shù):

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;

    // 創(chuàng)建 COM 對象
    CSimpleMsgBoxImpl* pMsgBox;
    pMsgBox = new CSimpleMsgBoxImpl;

    if ( NULL == pMsgBox )
        return E_OUTOFMEMORY;
    
    HRESULT hrRet;
    // 查詢客戶端所請求的接口,如果失敗,說明對象不可用,要刪除掉
    hrRet = pMsgBox->QuetyInterface( riid, ppv );

    if ( FAILED(hrRet) )
    {
        delete pMsgBox;
    }
    
    return hrRet;
}

** DllGetClassObject() **

接下來看看 DllGetClassObject() 的內(nèi)部,它的函數(shù)原型是:

HRESULT DllGetClassObject( REFCLSID rclsid, REFIID riid, void** ppv);

rclsid 是客戶端請求的 coclass 的 CLSID。函數(shù)將返回該 CLSID 所指的 coclass 的 class factory。

riid 和 ppv 跟 QueryInterface() 里的參數(shù)功能類似。在這里, riid 是 COM library 所請求的 class factory 接口的 IID,通常寫為 IID_IClassFactory.

因為 DllGetClassObject() 創(chuàng)建了一個新的 COM 對象(class factory), 因此代碼和 CreateInstance() 類似:

HRESULT DllGetClassObject( REFCLSID rclsid, REFIID riid, void** ppv)
{
    // 比較傳入的 rclsid 是否是 CSimpleMsgBoxImpl 的 CLSID
    if ( !InlineIsEqualGUID( rclsid __uuidof(CSimpleMsgBoxImpl) ) )
        return CLASS_E_CLASSNOTAVAILABLE;

    // ppv 必須是指向 void* 的指針
    if ( IsBadWritePtr ( ppv, sizeof(void*) )
        return E_POINTER;
    
    *ppv = NULL;

    // 創(chuàng)建 class factory 對象
    CSimpleMsgBoxClassFactory* pFactory;
    pFactory = new CSimpleMsgBoxClassFactory;

    if ( NULL == pFactory )
        return E_OUTOFMEMORY;
    
    // 我們要使用 pFactory 的 QueryInterface 接口,所以要 AddRef
    pFactory->AddRef();

    HRESULT hrRet;
    hrRet = pFactory->QueryInterface( riid, ppv );
    
    // 接口用完了,所以要 Release;
    pFactory->Release();

    return hrRet;
}

上面的 AddRef() 和 Release() 看起來有點兒奇怪,在 CreateInstance() 的例子里并沒有這么調(diào)用。其實這是兩種不同的寫法,功能都是一樣的,也就是在 QueryInterface() 失敗的時候,刪除 pFactory 對象。第一次調(diào)用 AddRef() 引用計數(shù)為 1;QueryInterface() 成功后,引用計數(shù)為 2, 失敗則仍然是 1;之后再調(diào)用 Release(),那么若 QueryInterface() 成功,這時引用計數(shù)減為 1,失敗時則減為 0,pFactory 自動銷毀自己。COM library 使用完 pFactory 之后,會再次調(diào)用 Release() 這時就能刪除掉 pFactory 了。

** 再看 QueryInterface() **

之前我已經(jīng)展示過一個 QueryInterface() 的代碼例子,但 class factory 的 QueryInterface() 接口要復(fù)雜一些。因為它不僅僅要實現(xiàn) IUnknown 接口還得進(jìn)行一系列檢查:

HRESULT CSimpleMsgBoxClassFactory::QueryInterface( REFIID riid, void** ppv)
{
    HRESULT hrRet = S_OK;

    if ( IsBadWritePtr( ppv, sizeof(void*) ) 
        return E_POINTER;

    *ppv = NULL;

    if ( InlineIsEqualGUID( riid, IID_IUnknown ))
    {
        *ppv = (IUnknown*) this;
    }
    else if ( InlineIsEqualGUID ( riid, IID_IClassFactory) )
    {
        *ppv = (IClassFactory*) this
    }
    else
    {
        hrRet = E_NOINTERFACE;
    }

    if ( S_OK == hrRet )
    {
        ((IUnknown*) *ppv)->AddRef();
    }
    
    return hrRet;
}

** ISimpleMsgBox 實現(xiàn) **

最后,看看 ISimpleMsgBox 的實現(xiàn),它就只有一個函數(shù) DoSimpleMsgBox(). 我們使用 微軟擴展類 _bstr_t 來將 bsMessageText 轉(zhuǎn)換成 TCHAR 類型字符串,然后用 MessageBox 展示它:

HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText )
{
    _bstr_t bsMsg = bsMessageText;
    LPCSTR szMsg = (TCHAR*) bsMsg;

    MessageBox(hwndParent, szMsg, _T("Simple Message Box"), MB_OK);

    return S_OK;
}

使用我們的 COM 對象

我們的 COM Server 已經(jīng)搞好了,怎么用呢?現(xiàn)階段我們的 接口還是一個 custom interface, 這意味著它只能被 C\C++ 客戶端使用。(如果我們的 coclass 實現(xiàn)了 IDispatch 接口,我們就能在任何語言的客戶端上使用它,這個就不在本文里探討了)。

客戶端代碼很簡單,使用 CoCreateInstance() 創(chuàng)建 COM 對象,然后調(diào)用它的 DoSimpleMsgBox() 函數(shù)彈出一個 MessageBox:

void DoMsgBoxTest(HWND hMainWnd)
{
    ISimpleMsgBox* pIMsgBox;
    HRESULT hr;

    hr = CoCreateInstance(__uuidof(CSimpleMsgBoxImpl),
                        NULL,
                        CLSCTX_INPROC_SERVER,
                        __uuidof(ISimpleMsgBox),
                        (void**) &pIMsgBox);
    
    if ( FAILED(hr) )
        return;
    
    pIMsgBox->DoSimpleMsgBox(hMainWnd, _bstr_t("Hello COM!"));
    pIMsgBox->Release();
}

其它細(xì)節(jié)

COM 宏

COM 里有一些 C\C++ 宏隱藏了具體實現(xiàn),我在文章里沒有用這些宏,但例子代碼里用了,因此在這兒解釋一下這些宏怎么用,以下是 ISimpleMsgBox 的聲明:

struct ISimpleMsgBox : public IUnknown
{
    STDMETHOD_(ULONG AddRef)() PURE;
    STDMETHOD_(ULONG Release)() PURE;
    STDMETHOD(QueryIntreface)(REFIID riid, void** ppv) PURE;

    STDMETHOD(DoSimpleMsgBox)(HWND hwndParent, BSTR bsMessageText) PURE;
};

STDMETHOD() 包含了 virtual 關(guān)鍵字、HRESULT 返回值,_stdcall 調(diào)用約定;
STDMETHOD
() 跟上面一樣,只是你可以指定一個不是 HRESULT 的返回值類型。
PURE 也就是 C++ 里的 "=0", 表示這個函數(shù)是純虛函數(shù);

STDMETHOD() 和 STDMETHOD_() 都有對應(yīng)的宏,用在函數(shù)實現(xiàn)上,分別是 STDMETHODIMP 和 STDMETHODIMP_, 例如下面是 DoSimpleMsgBox() 函數(shù)的實現(xiàn):

STDMETHODIMP CSimpleMsgBox::DoSimpleMsgBox(HWND hwndParent, BSTR bsMessageText)
{
    // ...
}

最后,標(biāo)準(zhǔn)導(dǎo)出函數(shù)可以使用 STDAPI 宏,如下:

STDAPI DllRegisterServer()

STDAPI 宏 包含了返回類型和調(diào)用約定。另外如果你使用了 STDAPI ,你不能再用 _declspec(dllexport) 來描述。你必須用 .DEF 文件來導(dǎo)出。

server 的注冊和反注冊

server 要實現(xiàn) DllRegisterServer() 和 DllUnregisterServer() 函數(shù)。這倆函數(shù)用來注冊和反注冊 server。這種操作很枯燥,我這兒就不寫了。他創(chuàng)建的注冊表鍵值類似下面這樣:

{067DF822-EAB6-11cf-B56E-00A0244D5087}
    |
    |__> InprocServer32
            |
            |__> default = [path to dll]
            |
            |__> ThreadingModel = Apartment

在 server 中設(shè)置斷點

如果你想在 server 里設(shè)置斷點,有兩種方法:

  1. 將 server 所在工程設(shè)定為啟動工程,然后開始調(diào)試, VS 會讓你輸入一個可執(zhí)行程序的路徑來加載 dll。輸入后就可以進(jìn)行調(diào)試了。
  2. 將 客戶端程序所在工程設(shè)定為啟動工程,然后設(shè)置項目依賴項。這樣也可以調(diào)試 server。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,796評論 18 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,726評論 18 399
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,674評論 25 708
  • 生活中的習(xí)慣在物理上即為慣性,習(xí)慣有著根深蒂固的力量,這種力量就像地球引力將我們固定在地球上一樣,使我們難以超越,...
    白卉閱讀 322評論 0 4
  • 這段時間過得比較匆忙,也過得比較沒心沒肺,一個字:忙,但不充盈……內(nèi)心所需要的感覺并不是這種。 想安安靜...
    麥子火了閱讀 413評論 1 1