COM(組件對象模型)簡單介紹

轉自 http://blog.csdn.net/winglet/article/details/2872342

 

什麼是COM?

簡單地說,COM提供了一種在不同的應用程序和語言之間共享二進制代碼的規範。COM定義了軟件組件互相通訊的方式。它是一種二進制和網絡標準,允許任意兩個組件互相通訊,而不管它們是在什麼計算機上運行(只要計算機是相連的),也不管計算機運行的是什麼操作系統(只要該操作系統支持COM),也不管該組件是用什麼語言編寫的。COM還提供了位置透明性:當使用COM組件時,該組件是進程內Dll、本地exe還是位於其他機器上的組件,都無所謂。

爲什麼需要COM?

雖然可能有很多歷史的原因,但是,我們僅從二進制代碼共享的角度來看看,爲什麼要引入COM。

Windows可以使用DLLs在二進制級別共享代碼,如kernel32.dll、user32.dll等。但是,這些dll都採用C接口,它們僅能被C語言或者能夠理解C調用規範的語言使用。這從語言層次上限定了二進制代碼的共享範圍。

MFC有一種被稱爲MFC擴展DLL的共享機制。但是你只能在MFC應用程序中使用它們。

COM利用定義一個二進制規範來解決這些問題。使用COM編寫的二進制模塊必須匹配一些特定的數據結構。COM規範同樣也定義COM對象在內存中應該如何被組織。這份二進制代碼同樣也不能依賴任何語言層面的特徵。

COM對象的結構使用與C++虛函數相同的結構。所以很多COM使用C++編寫,但是嚴格來說COM與編寫該COM的語言是不想關的,因爲最後的二進制代碼可以被任何語言使用。

COM並不是Win32相關的,理論上,COM可以被移植到任何操作系統上,但是現實中COM依然只是Windows世界的東西。

COM基本術語

1.interface:一個接口可以看做是一組稱爲方法的函數。接口名以"I"打頭,如"IShellLink",在C++中,接口即是一個只含有純虛函數的抽象基類。

2.coclass:component object class的簡稱。coclass用來實現接口。一個COM對象是coclass在內存中的一個實例(instance)。注意COM"class"跟C++"class"不是一個東西,雖然COM class 的實現通常都是C++ class。

3.COM Server:含有一個或多個coclass的二進制模塊(DLL或者EXE)。

4.Registration:向Windows註冊,登記COM Server的位置和入口。Unregistration是一個相反的過程:從Windows中移除掉這個登記。

5.GUID:globally unique identifier。一個128位的數。COM使用語言無關的GUID作爲標識。每一個interface和coclass都有一個GUID,因爲GUID是全世界範圍內唯一的。所以避免了名字衝突。有時候GUID也被稱爲UUID(universally unique identifier)。一個coclass的class ID稱爲CLSID;一個interface的interfaceID稱爲IID。

6.HRESULT:一個整數類型的值,COM使用它來返回成功或者錯誤碼。可以使用Windows錯誤碼查詢來獲取HRESULT所代表的字符含義。

基接口(base interface):IUnknown

每一個COM接口都要從IUnknown接口繼承。IUnknown這個名字的含義是:如果你擁有一個指向IUnknown接口的COM對象指針,那個你無從知道它真正指向的對象是什麼,因爲COM中的每一個對象都實現了這個接口。

這個接口有三個方法:AddRef、Release和QueryInterface。其中QueryInterface用於在獲取某個COM的一個接口指針後,查詢該COM的其他接口指針。

HRESULT IUnknown::QueryInterface (
    REFIID iid,
    void** ppv );

使用COM

1.創建一個COM對象:C++中使用new操作符或者直接在棧上創建;創建COM對象則要調用COM library中的一個API(CoCreateInstance)。

2.刪除COM對象:C++中,使用delete操作符或者在棧上的對象超出作用域以後會自動析構。但是在COM中,所有的對象擁有它們自己的引用計數(reference count)。調用者必須在使用完COM對象以後通知它(減少引用計數)。COM對象在引用計數到達0時,自動從內存中釋放自己。

創建COM對象的函數原型如下:

HRESULT CoCreateInstance (
    REFCLSID  rclsid,
    LPUNKNOWN pUnkOuter,
    DWORD     dwClsContext,
    REFIID    riid,
    LPVOID*   ppv );

參數說明:

rclsid
coclass的CLSID。例如,你可以傳遞CLSID_ShellLink來創建一個用於創建快捷方式的COM對象。
pUnkOuter
只在使用聚合COM對象的時候有用。如果不是用,則傳遞NULL。
dwClsContext
指示我們希望使用什麼類型的COM Server。這裏我們使用最簡單的一種Server,an in-process DLL,傳遞 CLSCTX_INPROC_SERVER。
riid
我們要使用的接口的IID。例如,傳遞IID_IShellLink來得到IShellLink interface的指針。
ppv
所請求的接口的指針地址。

當調用CoCreateInstance函數時,它查詢註冊表中的CLSID,讀取COM server的位置,將其加載到內存中並創建函數所請求的coclass的一個實例。需要注意的是,在使用函數CoCreateInstance之前,確保你的程序已經初始化了COM library,並且需要在程序退出時卸載它,分別調用CoInitialize和CoUninitialize函數。

-----------------COM object create sample-------------------------------------------------------

HRESULT hr;
IShellLink* pISL;
hr = CoCreateInstance(CLSID_ShellLink,
NULL,
CLSCTX_INPROC_SERVER,
IID_IShellLink,
(void*)&pISL);
if (SUCCEEDED(hr))
{
pISL->Release();//just release.
}
else
{
}

---------------------------------------------------------------------------------------------------------

COM中的字符串操作

只要注意到COM中的字符串都是Unicode的,就應該沒有問題的。習慣於使用Unicode並在與COM的交互中使用Unicode,就不會有任何問題。當使用ANSI時,需要自己處理Unicode與ANSI之間的轉換。除了常規的轉換方法外,ATL提供了幾個宏處理這種轉換:W2A()、OLE2A()、OLE2CA()等。

一個完整的使用COM的樣例代碼通常如下,代碼從參考文章中copy的:

------------------------------------------------------------------------------------------------------------------------

CString       sWallpaper = wszWallpaper;  // Convert the wallpaper path to ANSI
IShellLink*   pISL;
IPersistFile* pIPF;

    // 1. Initialize the COM library (make Windows load the DLLs). Normally you would
    // call this in your InitInstance() or other startup code.  In MFC apps, use
    // AfxOleInit() instead.
    CoInitialize ( NULL );

    2. Create a COM object, using the Shell Link coclass provided by the shell.
    // The 4th parameter tells COM what interface we want (IShellLink).
    hr = CoCreateInstance ( CLSID_ShellLink,
                            NULL,
                            CLSCTX_INPROC_SERVER,
                            IID_IShellLink,
                            (void**) &pISL );

    if ( SUCCEEDED(hr) )
        {
        // 3. Set the path of the shortcut's target (the wallpaper file).
        hr = pISL->SetPath ( sWallpaper );

        if ( SUCCEEDED(hr) )
            {
            // 4. Get a second interface (IPersistFile) from the COM object.
            hr = pISL->QueryInterface ( IID_IPersistFile, (void**) &pIPF );

            if ( SUCCEEDED(hr) )
                {
                // 5. Call the Save() method to save the shortcut to a file.  The
                // first parameter is a Unicode string.
                hr = pIPF->Save ( L"C://wallpaper.lnk", FALSE );

                // 6a. Release the IPersistFile interface.
                pIPF->Release();
                }
            }

        // 6b. Release the IShellLink interface.
        pISL->Release();
        }

    // Printing of error messages omitted here.

    // 7. Uninit the COM library.  In MFC apps, this is not necessary since MFC
    // does it for us.
    CoUninitialize();

------------------------------------------------------------------------------------------------------------------------

編寫COM

下面看看一個最簡單的COM server,進程內server(in-process server),需要如何編寫。

要想能用COM lib來使用該COM server,首先必須做兩件事情:

1.必須在註冊表HKEY_CLASSES_ROOT/CLSID下注冊自己;

2.必須導出一個稱爲DllGetClassObject的函數。

這是這個COM的最低配置。需要在該註冊表項下創建一個以該GUID爲名的鍵,該鍵下有包含一系列的鍵值,如COM的位置以及它的線程模型等。

一般情況下,COM還需要導出下面三個函數:

DllCanUnloadNow(): COM lib調用此函數以判斷該server是否可以被卸載; DllRegisterServer(): 由安裝工具調用,像RegSvr32一樣讓server能夠註冊自身。 DllUnregisterServer(): 由卸載工具調用,用於卸載COM。

前面說道每一個接口都必須實現IUnknown接口,因爲IUnknown接口包含了COM對象的兩個基本特徵:引用計數和接口查詢。在寫coclass時,也必須實現該接口。下面是一個簡單的coclass:

-----------------------------------------------------------------------------------------------------------------------------

class CUnknownImpl : public IUnknown
{
public:
    // Construction and destruction
    CUnknownImpl();
    virtual ~CUnknownImpl();

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

protected:
    UINT m_uRefCount;  // object's reference count
};
HRESULT CUnknownImpl::QueryInterface ( REFIID riid, void** ppv )
{
HRESULT hrRet = S_OK;

    // Standard QI() initialization - set *ppv to NULL.
    *ppv = NULL;

    // If the client is requesting an interface we support, set *ppv.
    if ( IsEqualIID ( riid, IID_IUnknown ))
        {
        *ppv = (IUnknown*) this;
        }
    else
        {
        // We don't support the interface the client is asking for.
        hrRet = E_NOINTERFACE;
        }

    // If we're returning an interface pointer, AddRef() it.
    if ( S_OK == hrRet )
        {
        ((IUnknown*) *ppv)->AddRef();
        }

    return hrRet;
}

-----------------------------------------------------------------------------------------------------------------------------

在每一次實現一個coclass時,同時也需要寫一個被稱爲class factory配對的類,該類用於創建這個coclass的實例。需要這個工廠類的原因是爲了語言無關:COM本身不創建COM對象,因爲那樣的話就不是語言和實現無關的了。

當COM lib調用DllGetClassObject時,它傳遞COM客戶端請求的CLSID,server創建代表這個CLSID的類工廠。工廠類(class factory)本身也是一個coclass,它實現了IClassFactory。當DllGetClassObject成功返回時,它返回給COM lib一個指向IClassFactory的指針,然後COM lib使用IClassFactory的方法來創建COM對象。

IClassFactory像下面這樣:

struct IClassFactory : public IUnknown
{
    HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid,
                            void** ppvObject );
    HRESULT LockServer( BOOL fLock );
};
在這之後我們需要用COM實現自己的功能,首先定義一個接口,接口裏包含了我們自己的功能定義(記住,它必須實現IUnknown接口),例如:
struct ISimpleMsgBox : public IUnknown
{
    // IUnknown methods
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface( REFIID riid, void** ppv );

    // ISimpleMsgBox methods
    HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );
};

struct __declspec(uuid("{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}"))
                  ISimpleMsgBox;
注意到最後一行,__declspec表示將一個GUID分配給ISimpleMsgBox,然後我們可以使用__uuidof操作符來獲取這個GUID,__declspec和__uuidof都是微軟的C++擴展
(Microsoft C++ extensions)。
然後,你需要一個類來實現這個接口:
class CSimpleMsgBoxImpl : public ISimpleMsgBox  
{
public:
	CSimpleMsgBoxImpl();
	virtual ~CSimpleMsgBoxImpl();

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

    // ISimpleMsgBox methods
    HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );

protected:
    ULONG m_uRefCount;
};

class  __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}")) 
                  CSimpleMsgBoxImpl;
其他細節:與COM有關的宏
STDMETHOD():包含了virtual關鍵字和HRESULT返回類型以及__stdcall調用約定。STDMETHOD_()與之相同,但是允許指定非HRESULT的返回值。
PURE:與C++中的"=0"效果相同,將一個函數聲明爲純虛函數。
STDMETHODIMP:對應於STDMETHOD()的實現,相應地,STDMETHODIMP_()與STDMETHOD_()對應。
STDAPI:包含了返回類型和調用約定,由於STDAPI的擴展方式,使用了此宏以後不能再使用__declspec(dllexport)。可以使用".DEF"文件完成類似功能。
還有一些實現上的問題,就不多講了,千言萬語,都在代碼裏面:)。
本文完全基於下面兩篇英文文章,作了一個簡單的理解和翻譯,代碼也請到下面的鏈接地址裏下載。

參考文章

http://www.codeproject.com/KB/COM/comintro.aspx#upd0722b

http://www.codeproject.com/KB/COM/comintro2.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章