轉自 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"文件完成類似功能。
還有一些實現上的問題,就不多講了,千言萬語,都在代碼裏面:)。
本文完全基於下面兩篇英文文章,作了一個簡單的理解和翻譯,代碼也請到下面的鏈接地址裏下載。
參考文章