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,并且必須與客戶端程序同處于一臺機(jī)器上胶果。

一個 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 提供了一種機(jī)制使得 server 能夠告知 COM 它可以被卸載。這種機(jī)制是通過 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)的機(jī)制崇众。客戶端調(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)建對象然后返回給 客戶端辅肾。這一機(jī)制通過 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++ 擴(kuò)展 中的操作符兽赁。

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(). 我們使用 微軟擴(kuò)展類 _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)系作者
  • 序言:七十年代末涂身,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子搓蚪,更是在濱河造成了極大的恐慌蛤售,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妒潭,死亡現(xiàn)場離奇詭異悴能,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)雳灾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門漠酿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人谎亩,你說我怎么就攤上這事炒嘲。” “怎么了匈庭?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵夫凸,是天一觀的道長。 經(jīng)常有香客問我阱持,道長夭拌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任紊选,我火速辦了婚禮啼止,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘兵罢。我一直安慰自己献烦,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布卖词。 她就那樣靜靜地躺著巩那,像睡著了一般吏夯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上即横,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天噪生,我揣著相機(jī)與錄音,去河邊找鬼东囚。 笑死跺嗽,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的页藻。 我是一名探鬼主播桨嫁,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼份帐!你這毒婦竟也來了璃吧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤废境,失蹤者是張志新(化名)和其女友劉穎畜挨,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體噩凹,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡巴元,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了驮宴。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片务冕。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖幻赚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情臊旭,我是刑警寧澤落恼,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站离熏,受9級特大地震影響佳谦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜滋戳,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一钻蔑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧奸鸯,春花似錦咪笑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春扬虚,著一層夾襖步出監(jiān)牢的瞬間努隙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工辜昵, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留荸镊,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓堪置,卻偏偏與公主長得像躬存,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子晋柱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

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

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