原文:http://blog.csdn.net/byxdaz/article/details/6595210
一、Com概念
所謂COM(Componet Object Model,組件對象模型),是一種說明如何建立可動態互變組件的規范,此規范提供了為保證能夠互操作,客戶和組件應遵循的一些二進制和網絡標準。通過這種標準將可以在任意兩個組件之間進行通信而不用考慮其所處的操作環境是否相同、使用的開發語言是否一致以及是否運行于同一臺計算機。
在COM規范下將能夠以高度靈活的編程手段來開發、維護應用程序。可以將一個單獨的復雜程序劃分為多個獨立的模塊進行開發,這里的每一個獨立模塊都是一個自給自足的組件,可以采取不同的開發語言去設計每一個組件。在運行時將這些組件通過接口組裝起來以形成所需要的應用程序。構成應用程序的每一個組件都可以在不影響其他組件的前提下被升級。這里所說的組件是特指在二進制級別上進行集成和重用而能夠被獨立生產獲得和配置的軟件單元。COM規范所描述的即是如何編寫組件,遵循COM標準的任何一個組件都是可以被用來組合成應用程序的。至于對組件采取的是何種編程語言則是無關緊要的,可以自由選取。作為一個真正意義上的組件,應具備如下特征:
1) 實現了對開發語言的封裝。
2) 以二進制形式發布。
3) 能夠在不妨礙已有用戶的情況下被升級。
4) 在網絡上的位置必須能夠被透明的重新分配。
這些特征使COM組件具有很好的可重用性,這種可重用性與DLL一樣都是建立在二進制基礎上的代碼重用。但是COM在多個方面的表現均要比DLL的重用方式好的多。例如,在DLL中存在的函數重名問題、各編譯器對C++函數名稱修飾的不兼容問題、路徑問題以及與可執行程序的依賴性問題等在COM中通過使用虛函數表、查找注冊表等手段均被很好的解決。其實COM組件在發布形式上本身就包擴DLL,只不過通過制訂復雜的COM規范,使COM本身的機制改變了重用的方法,能夠以一種新的方法來利用DLL并克服DLL本身所固有的一些缺陷,從而實現了更高層次的重用。
(1)函數重名問題
DLL里是一個一個的函數,我們通過函數名來調用函數,那如果兩個DLL里有重名的函數怎么辦?
(2)各編譯器對C++函數的名稱修飾不兼容問題
對于C++函數,編譯器要根據函數的參數信息為它生成修飾名,DLL庫里存的就是這個修飾名,但是不同的編譯器產生修飾的方法不一樣,所以你在VC里編寫的DLL在BC里就可以用不了。不過也可以用extern"C";來強調使用標準的C函數特性,關閉修飾功能,但這樣也喪失了C++的重載多態性功能。
(3)路徑問題
放在自己的目錄下面,別人的程序就找不到,放在系統目錄下,就可能有重名的問題。而真正的組件應該可以放在任何地方甚至可以不在本機,用戶根本不需考慮這個問題。
(4)DLL與EXE的依賴問題
我們一般都是用隱式連接的方式,就是編程的時侯指明用什么DLL,這種方式很簡單,它在編譯時就把EXE與DLL綁在一起了。如果DLL發行了一個新版本,我們很有必要重新鏈接一次,因為DLL里面函數的地址可能已經發生了改變。DLL的缺點就是COM的優點。
二、COM相關的結構、接口
1)、CLSID
CLSID其實就是一個號碼,或者說是一個16字節的數。觀察注冊表,在HKEY_CLASSES_ROOT\CLSID\{......}主鍵下,LocalServer32(DLL組件使用InprocServer32) 中保存著程序路徑名稱。CLSID 的結構定義如下:
typedef struct _GUID {
?????? DWORD Data1;? // 隨機數
?????? WORD Data2;?? // 和時間相關
?????? WORD Data3;?? // 和時間相關
?????? BYTE Data4[8];????? // 和網卡MAC相關
} GUID;
?
typedef GUID CLSID;? // 組件ID
typedef GUID IID;??? // 接口ID
#define REFCLSID const CLSID &
?
// 常見的聲明和賦值方法
CLSID CLSID_Excel ={0x00024500,0x0000,0x0000,{0xC0,0x00,0x00,0x00,0x00,0x00,0x00,0x46}};
struct __declspec(uuid("00024500-0000-0000-C000-000000000046"))CLSID_Excel;
class DECLSPEC_UUID("00024500-0000-0000-C000-000000000046")CLSID_Excel;
// 注冊表中的表示方法
{00024500-0000-0000-C000-000000000046}
2)、ProgID
每一個COM組件都需要指定一個CLSID,并且不能重名。它之所以使用16個字節,就是要從概率上保證重復是“不可能”的。但是,微軟為了使用方便與記憶,也支持另一個字符串名稱方式,叫 ProgID。由于 CLSID 和 ProgID 其實是一個概念的兩個不同的表示形式,所以我們在程序中可以隨便使用任何一種。下面介紹一下 CLSID 和 ProgID 之間的轉換方法和相關的函數:
? ? ? ? ? ? ? ? ? ?
? 函數 ? | ? 功能說明 ? |
? CLSIDFromProgID()、CLSIDFromProgIDEx() ? | ? 由 ProgID 得到 CLSID。沒什么好說的,你自己都可以寫,查注冊表貝 ? |
? ProgIDFromCLSID() ? | ? 由 CLSID 得到 ProgID,調用者使用完成后要釋放 ProgID 的內存(注5) ? |
? CoCreateGuid() ? | ? 隨機生成一個? GUID ? |
? IsEqualGUID()、IsEqualCLSID()、IsEqualIID() ? | ? 比較2個ID是否相等 ? |
? StringFromCLSID()、StringFromGUID2()、StringFromIID() ? | ? 由 CLSID,IID 得到注冊表中CLSID樣式的字符串,注意釋放內存 ? |
3)、接口
3.1、函數是通過 VTAB 虛函數表提供其地址, 從另一個角度來看,不管用什么語言開發,編譯器產生的代碼都能生成這個表。這樣就實現了組件的“二進制特性”輕松實現了組件的跨語言要求。
3.2、假設有一個指針型變量保存著 VTAB 的首地址,則這個變量就叫“接口指針, 變量命名的時候,習慣上加上"I"開頭。另外為了區分不同的接口,每個接口 也都要有一個名字,該名字就和 CLSID 一樣,使用 GUID 方式,叫 IID。
3.3、接口一經發表,就不能再修改了。不然就會出現向前兼容的問題。這個性質叫“接口不變性”。
3.4、組件中必須有3個函數,QueryInterface、AddRef、Release,它們3個函數也組成一個接口,叫"IUnknown"。
3.5、任何接口,其實都包含了 IUnknown 接口。隨著你接觸到更多的接口就會了更體會解到接口的另一個性質“繼承性”。
3.6、在任何接口上,調用表中的第一個函數,其實就是調用 QueryInterface()函數,就得到你想要的另外一個接口指針。這個性質叫“接口的傳遞性”
3.7、C/C++語言中需要事先對函數聲明,那么就 會要求組件也必須提供C語言的頭文件。不行!為了能使COM具有跨語言的能力,決定不再為任何語言提供對應的函數接口聲明,而是獨立地提供一個叫類型庫(TLB)的聲明。每個語言的IDE環境自己去根據TLB生成自己語言需要的包裝。這個性質叫“接口聲明的獨立性”。
4)COM組件數據類型
HRESULT 函數返回值
????? HRESULT Add( long n1, long n2, long *pSum)
????? {
????????? *pSum = n1 + n2;
????????? return S_OK;
????? }
如果函數正常執行,則返回 S_OK,同時真正的函數運行結果則通過參數指針返回。如果遇到了異常情況,則COM系統經過判斷,會返回相應的錯誤值。常見的返回值有:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? HRESULT ? | ? 值 ? | ? 含義 ? |
? S_OK ? | ? 0x00000000 ? | ? 成功 ? |
? S_FALSE ? | ? 0x00000001 ? | ? 函數成功執行完成,但返回時出現錯誤 ? |
? E_INVALIDARG ? | ? 0x80070057 ? | ? 參數有錯誤 ? |
? E_OUTOFMEMORY ? | ? 0x8007000E ? | ? 內存申請錯誤 ? |
? E_UNEXPECTED ? | ? 0x8000FFFF ? | ? 未知的異常 ? |
? E_NOTIMPL ? | ? 0x80004001 ? | ? 未實現功能 ? |
? E_FAIL ? | ? 0x80004005 ? | ? 沒有詳細說明的錯誤。一般需要取得? Rich Error 錯誤信息(注1) ? |
? E_POINTER ? | ? 0x80004003 ? | ? 無效的指針 ? |
? E_HANDLE ? | ? 0x80070006 ? | ? 無效的句柄 ? |
? E_ABORT ? | ? 0x80004004 ? | ? 終止操作 ? |
? E_ACCESSDENIED ? | ? 0x80070005 ? | ? 訪問被拒絕 ? |
? E_NOINTERFACE ? | ? 0x80004002 ? | ? 不支持接口 ? |
?????????????????????????????
?(圖:HRESULT 的結構)
HRESULT 其實是一個雙字節的值,其最高位(bit)如果是0表示成功,1表示錯誤。具體參見 MSDN 之"Structureof COM Error Codes"說明。我們在程序中如果需要判斷返回值,則可以使用比較運算符號;switch開關語句;也可以使用VC提供的宏:
????? HRESULT hr = 調用組件函數;
????? if( SUCCEEDED( hr ) ){...} // 如果成功
????? ......
????? if( FAILED( hr ) ){...} // 如果失敗
?
BSTR
COM 中除了使用一些簡單標準的數據類型外(注2),字符串類型需要特別重點地說明一下。還記得原則嗎?COM 組件是運行在分布式環境中的。通俗地說,你不能直接把一個內存指針直接作為參數傳遞給COM函數。你想想,系統需要把這塊內存的內容傳遞到“地球另一 邊”的計算機上,因此,我至少需要知道你這塊內存的尺寸吧?不然讓我如何傳遞呀?傳遞多少字節呀?!而字符串又是非常常用的一種類型,因此 COM 設計者引入了 BASIC 中字符串類型的表示方式---BSTR。BSTR 其實是一個指針類型,它的內存結構是:(輸入程序片段 BSTR p = ::SysAllocString(L"Hello,你好");斷點執行,然后觀察p的內存)
(圖:BSTR 內存結構)、
BSTR 是一個指向 UNICODE 字符串的指針,且 BSTR 向前的4個字節中,使用DWORD保存著這個字符串的字節長度( 沒有含字符串的結束符)。
有關 BSTR 的處理函數:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? API 函數 ? | ? 說明 ? |
? SysAllocString() ? | ? 申請一個 BSTR 指針,并初始化為一個字符串 ? |
? SysFreeString() ? | ? 釋放 BSTR 內存 ? |
? SysAllocStringLen() ? | ? 申請一個指定字符長度的? BSTR 指針,并初始化為一個字符串 ? |
? SysAllocStringByteLen() ? | ? 申請一個指定字節長度的? BSTR 指針,并初始化為一個字符串 ? |
? SysReAllocStringLen() ? | ? 重新申請 BSTR 指針 ? |
? CString 函數 ? | ? 說明 ? |
? AllocSysString() ? | ? 從 CString 得到 BSTR ? |
? SetSysString() ? | ? 重新申請 BSTR 指針,并復制到 CString 中 ? |
? CComBSTR 函數 ?ATL 的 BSTR 包裝類。在 atlbase.h 中定義 ? | |
? Append()、AppendBSTR()、AppendBytes()、ArrayToBSTR()、BSTRToArray()、AssignBSTR()、Attach()、Detach()、Copy()、CopyTo()、Empty()、Length()、ByteLength()、ReadFromStream()、WriteToStream()、LoadString()、ToLower()、ToUpper() 運算符重載:!,!=,==,<,>,&,+=,+,=,BSTR ? | ? ???? 太多了,但從函數名稱不能看出其基本功能。詳細資料,查看MSDN 吧。另外,左側函數,有很多是 ATL 7.0 提供的,VC6.0 下所帶的 ATL 3.0 不支持。 |
?
各種字符串類型之間的轉換
函數 WideCharToMultiByte(),轉換 UNICODE 到MBCS。使用范例:
????? LPCOLESTR lpw = L"Hello,你好";
????? size_t wLen = wcslen( lpw ) + 1;? // 寬字符字符長度,+1表示包含字符串結束符
?????
????? int aLen=WideCharToMultiByte(? // 第一次調用,計算所需 MBCS 字符串字節長度
????????????? CP_ACP,
????????????? 0,
????????????? lpw,? // 寬字符串指針
????????????? wLen, // 字符長度
????????????? NULL,
????????????? 0,? // 參數0表示計算轉換后的字符空間
????????????? NULL,
????????????? NULL);
??????
????? LPSTR lpa = new char [aLen];
??????
????? WideCharToMultiByte(
????????????? CP_ACP,
????????????? 0,
????????????? lpw,
????????????? wLen,
????????????? lpa,? // 轉換后的字符串指針
????????????? aLen, // 給出空間大小
????????????? NULL,
????????????? NULL);
?
????? // 此時,lpa 中保存著轉換后的 MBCS 字符串
????? ... ... ... ...
????? delete [] lpa;
??? 函數 MultiByteToWideChar(),轉換MBCS 到 UNICODE。使用范例:
????? LPCSTR lpa = "Hello,你好";
????? size_t aLen = strlen( lpa ) + 1;
?????
????? int wLen = MultiByteToWideChar(
????????????? CP_ACP,
????????????? 0,
????????????? lpa,
????????????? aLen,
????????????? NULL,
????????????? 0);
?????
????? LPOLESTR lpw = new WCHAR [wLen];
????? MultiByteToWideChar(
????????????? CP_ACP,
????????????? 0,
????????????? lpa,
????????????? aLen,
????????????? lpw,
????????????? wLen);
????? ... ... ... ...
????? delete [] lpw;
?使用 ATL 提供的轉換宏。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? A2BSTR ? | ? OLE2A ? | ? T2A ? | ? W2A ? |
? A2COLE ? | ? OLE2BSTR ? | ? T2BSTR ? | ? W2BSTR ? |
? A2CT ? | ? OLE2CA ? | ? T2CA ? | ? W2CA ? |
? A2CW ? | ? OLE2CT ? | ? T2COLE ? | ? W2COLE ? |
? A2OLE ? | ? OLE2CW ? | ? T2CW ? | ? W2CT ? |
? A2T ? | ? OLE2T ? | ? T2OLE ? | ? W2OLE ? |
? A2W ? | ? OLE2W ? | ? T2W ? | ? W2T ? |
上表中的宏函數,其實非常容易記憶:
? ? ? ? ? ? ? ? ? ?
? 2 ? | ? 好搞笑的縮寫,to 的發音和 2 一樣,所以借用來表示“轉換為、轉換到”的含義。 ? |
? A ? | ? ANSI 字符串,也就是 MBCS。 ? |
? W、OLE ? | ? 寬字符串,也就是 UNICODE。 ? |
? T ? | ? 中間類型T。如果定義了 _UNICODE,則T表示W;如果定義了 _MBCS,則T表示A ? |
? C ? | ? const 的縮寫 ? |
使用范例:
#include<atlconv.h>
?void fun()
?{
???? USES_CONVERSION;? // 只需要調用一次,就可以在函數中進行多次轉換
???? LPCTSTR lp = OLE2CT( L"Hello,你好"));
???? ... ... ... ...
??? // 不用顯式釋放 lp 的內存,因為
??? // 由于 ATL 轉換宏使用棧作為臨時空間,函數結束后會自動釋放棧空間。
}
?
VARIANT
C++、BASIC、Java、Pascal、Script......計算機語言多種多樣,而它們各自又都有自己的數據類型,COM 產生目的,其中之一就是要跨語言(注3)。而 VARIANT 數據類型就具有跨語言的特性,同時它可以表示(存儲)任意類型的數據。從C語言的角度來講,VARIANT 其實是一個結構,結構中用一個域(vt)表示------該變量到底表示的是什么類型數據,同時真正的數據則存貯在 union 空間中。結構的定義太長了(雖然長,但其實很簡單)大家去看 MSDN 的描述吧,這里給出如何使用的簡單示例:
學生:我想用 VARIANT 表示一個4字節長的整數,如何做?
老師:VARIANT v;v.vt=VT_I4; v.lVal=100;
學生:我想用 VARIANT 表示布爾值“真”,如何做?
老師:VARIANT v;v.vt=VT_BOOL; v.boolVal=VARIANT_TRUE;
?
在我們寫程序的時候到比較簡單,請大家遵守幾個原則:
1、啟動組件得到一個接口指針(Interface)后,不要調用AddRef()。因為系統知道你得到了一個指針,所以它已經幫你調用了AddRef()函數;
2、通過QueryInterface()得到另一個接口指針后,不要調用AddRef()。因為......和上面的道理一樣;
3、當你把接口指針賦值給(保存到)另一個變量中的時候,請調用AddRef();
4、當不需要再使用接口指針的時候,務必執行Release()釋放;
5、當使用智能指針的時候,可以省略指針的維護工作;
?
內存分配和釋放
??? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??? ? ? ? ? ? ? ? ? ? ?
? C語言 ? | ? C++語言 ? | ? Windows 平臺 ? | ? COM ? | ? IMalloc 接口 ? | ? BSTR ? | |
? 申請 ? | ? malloc() ? | ? new ? | ? GlobalAlloc() ? | ? CoTaskMemAlloc() ? | ? Alloc() ? | ? SysAllocString() ? |
? 重新申請 ? | ? realloc() ? | ? GlobalReAlloc() ? | ? CoTaskRealloc() ? | ? Realloc() ? | ? SysReAllocString() ? | |
? 釋放 ? | ? free() ? | ? delete ? | ? GlobalFree() ? | ? CoTaskMemFree() ? | ? Free() ? | ? SysFreeString() ? |
以上這些函數必須要按類型配合使用(比如:new 申請的內存,則必須用delete 釋放)。
?
三、VC進行COM編程
VC進行COM編程,必須要掌握哪些COM理論知識。要學COM的基本原理,我推薦的書是《COM技術內幕》。但僅看這樣的書是遠遠不夠的,我們最終的目的是要學會怎么用COM去編程序,而不是拼命的研究COM本身的機制。所以我個人覺得對COM的基本原理不需要花大量的時間去追根問底,沒有必要,是吃力不討好的事。其實我們只需要掌握幾個關鍵概念就夠了。這里用VC編程所必需掌握的幾個關鍵概念。
1) COM組件實際上是一個C++類,而接口都是純虛類。組件從接口派生而來我們可以簡單的用純粹的C++的語法形式來描述COM是個什么東西:
class IObject
{
public:
virtual Function1(...) = 0;
virtual Function2(...) = 0;
....
};
class MyObject : public IObject
{
public:
virtual Function1(...){...}
virtual Function2(...){...}
....
};?
看清楚了嗎?IObject就是我們常說的接口,MyObject就是所謂的COM組件。切記接口都純虛類,它所包含的函數都是純虛函數,而且它沒有成員變量。而COM組件就是從這些純虛類繼承下來的派生類,它實現了這些虛函數,僅此而已。從上面也可以看出,COM組件是以C++為基礎的,特別重要的是虛函數和多態性的概念,COM中所有函數都是虛函數,都必須通過虛函數表VTable來調用,這一點是無比重要的,必需時刻牢記在心。
2) COM組件有三個最基本的接口類,分別是IUnknown、IClassFactory、IDispatchCOM規范規定任何組件、任何接口都必須從IUnknown繼承,IUnknown包含三個函數,分別是QueryInterface、AddRef、Release。這三個函數是無比重要的,而且它們的排列順序也是不可改變的。QueryInterface用于查詢組件實現的其它接口,說白了也就是看看這個組件的父類中還有哪些接口類,AddRef用于增加引用計數,Release用于減少引用計數。引用計數也是COM中的一個非常重要的概念。大體上簡單的說來可以這么理解,COM 組件是個DLL,當客戶程序要用它時就要把它裝到內存里。
另一方面,一個組件也不是只給你一個人用的,可能會有很多個程序同時都要用到它。但實際上DLL只裝載了一次,即內存中只有一個COM組件,那COM組件由誰來釋放?由客戶程序嗎?不可能,因為如果你釋放了組件,那別人怎么用,所以只能由COM組件自己來負責。所以出現了引用計數的概念,COM維持一個計數,記錄當前有多少人在用它,每多一次調用計數就加一,少一個客戶用它就減一,當最后一個客戶釋放它的時侯,COM知道已經沒有人用它了,它的使用已經結束了,那它就把它自己給釋放了。
引用計數是COM編程里非常容易出錯的一個地方,但所幸VC的各種各種的類庫里已經基本上把AddRef的調用給隱含了,在我的印象里,我編程的時侯還從來沒有調用過AddRef,我們只需在適當的時侯調用Release。至少有兩個時侯要記住調用Release,第一個是調用了QueryInterface以后,第二個是調用了任何得到一個接口的指針的函數以后,記住多查MSDN以確定某個函數內部是否調用了AddRef,如果是的話那調用Release的責任就要歸你了。
IUnknown的這三個函數的實現非常規范但也非常煩瑣,容易出錯,所幸的事我們可能永遠也不需要自己來實現它們。
IClassFactory的作用是創建COM組件。我們已經知道COM組件實際上就是一個類,那我們平常是怎么實例化一個類對象的?是用‘new’命令!很簡單吧,COM組件也一樣如此。但是誰來new它呢?不可能是客戶程序,因為客戶程序不可能知道組件的類名字,如果客戶知道組件的類名字那組件的可重用性就要打個大大的折扣了,事實上客戶程序只不過知道一個代表著組件的128位的數字串而已,這個等會再介紹。所以客戶無法自己創建組件,而且考慮一下,如果組件是在遠程的機器上,你還能new出一個對象嗎?所以創建組件的責任交給了一個單獨的對象,這個對象就是類廠。
每個組件都必須有一個與之相關的類廠,這個類廠知道怎么樣創建組件,當客戶請求一個組件對象的實例時,實際上這個請求交給了類廠,由類廠創建組件實例,然后把實例指針交給客戶程序。這個過程在跨進程及遠程創建組件時特別有用,因為這時就不是一個簡單的new操作就可以的了,它必須要經過調度,而這些復雜的操作都交給類廠對象去做了。
IClassFactory最重要的一個函數就是CreateInstance,顧名思議就是創建組件實例,一般情況下我們不會直接調用它,API函數都為我們封裝好它了,只有某些特殊情況下才會由我們自己來調用它,這也是VC編寫COM組件的好處,使我們有了更多的控制機會,而VB給我們這樣的機會則是太少太少了。
IDispatch叫做調度接口。它的作用何在呢?這個世上除了C++還有很多別的語言,比如VB、VJ、VBScript、JavaScript等等。可以這么說,如果這世上沒有這么多亂七八糟的語言,那就不會有IDispatch。:-) 我們知道COM組件是C++類,是靠虛函數表來調用函數的,對于VC來說毫無問題,這本來就是針對C++而設計的,以前VB不行,現在VB也可以用指針了,也可以通過VTable來調用函數了,VJ也可以,但還是有些語言不行,那就是腳本語言,典型的如VBScript、JavaScript。不行的原因在于它們并不支持指針,連指針都不能用還怎么用多態性啊,還怎么調這些虛函數啊。唉,沒辦法,也不能置這些腳本語言于不顧吧,現在網頁上用的都是這些腳本語言,而分布式應用也是COM組件的一個主要市場,它不得不被這些腳本語言所調用,既然虛函數表的方式行不通,我們只能另尋他法了。時勢造英雄,IDispatch應運而生。:-)
調度接口把每一個函數每一個屬性都編上號,客戶程序要調用這些函數屬性的時侯就把這些編號傳給IDispatch接口就行了,IDispatch再根據這些編號調用相應的函數,僅此而已。當然實際的過程遠比這復雜,當給一個編號就能讓別人知道怎么調用一個函數那不是天方夜潭嗎,你總得讓別人知道你要調用的函數要帶什么參數,參數類型什么以及返回什東西吧,而要以一種統一的方式來處理這些問題是件很頭疼的事。IDispatch接口的主要函數是Invoke,客戶程序都調用它,然后Invoke再調用相應的函數,如果看一看MS的類庫里實現Invoke的代碼就會驚嘆它實現的復雜了,因為你必須考慮各種參數類型的情況,所幸我們不需要自己來做這件事,而且可能永遠也沒這樣的機會。
IUnknown接口的三個函數就是QueryInterface,AddRef和Release。
IDispatch接口有4個函數,解釋語言的執行器就通過這僅有的4個函數來執行組件所提供的功能。IDispatch 接口用 IDL 形式說明如下:
[
??? object,
???uuid(00020400-0000-0000-C000-000000000046), // IDispatch 接口的 IID =IID_IDispatch
??? pointer_default(unique)
]
?
interface IDispatch : IUnknown
{
??? typedef [unique] IDispatch *LPDISPATCH;?? // 轉定義 IDispatch * 為 LPDISPATCH
?
??? HRESULT GetTypeInfoCount([out]UINT * pctinfo);?? // 有關類型庫的這兩個函數,咱們以后再說
??? HRESULT GetTypeInfo([in] UINTiTInfo,[in] LCID lcid,[out] ITypeInfo ** ppTInfo);
?
??? HRESULT GetIDsOfNames( // 根據函數名字,取得函數序號(DISPID)
??????????????? [in] REFIID riid,
??????????????? [in,size_is(cNames)] LPOLESTR * rgszNames,
??????????????? [in] UINT cNames,
???????????? ???[in] LCID lcid,
??????????????? [out,size_is(cNames)] DISPID * rgDispId
??????????? );
?
??? [local]???????? // 本地版函數
??? HRESULT Invoke( // 根據函數序號,解釋執行函數功能
??????????????? [in] DISPIDdispIdMember,
??????????????? [in] REFIID riid,
??????????????? [in] LCID lcid,
??????????????? [in] WORD wFlags,
??????????????? [in, out] DISPPARAMS* pDispParams,
??????????????? [out] VARIANT *pVarResult,
??????????????? [out] EXCEPINFO *pExcepInfo,
??????????????? [out] UINT *puArgErr
??????????? );
?
??? [call_as(Invoke)]????? // 遠程版函數
??? HRESULT RemoteInvoke(
??????????????? [in] DISPIDdispIdMember,
??????????????? [in] REFIID riid,
??????????????? [in] LCID lcid,
??????????????? [in] DWORD dwFlags,
??????????????? [in] DISPPARAMS *pDispParams,
??????????????? [out] VARIANT * pVarResult,
??????????????? [out] EXCEPINFO *pExcepInfo,
??????????????? [out] UINT *pArgErr,
??????????????? [in] UINT cVarRef,
??????????????? [in,size_is(cVarRef)] UINT * rgVarRefIdx,
??????????????? [in, out,size_is(cVarRef)] VARIANTARG * rgVarRef
??????????? );
}
?
3) dispinterface接口、Dual接口以及Custom接口
這是在ATL編程時用到的術語。在這里主要是想談一下自動化接口的好處及缺點,用這三個術語來解釋可能會更好一些,而且以后遲早會遇上它們,我將以一種通俗的方式來解釋它們,可能并非那么精確,就好象用偽代碼來描述算法一樣。
所謂的自動化接口就是用IDispatch實現的接口。我們已經講解過IDispatch的作用了,它的好處就是腳本語言象VBScript、JavaScript也能用COM組件了,從而基本上做到了與語言無關它的缺點主要有兩個,第一個就是速度慢效率低。這是顯而易見的,通過虛函數表一下子就可以調用函數了,而通過Invoke則等于中間轉了道手續,尤其是需要把函數參數轉換成一種規范的格式才去調用函數,耽誤了很多時間。所以一般若非是迫不得已我們都想用VTable的方式調用函數以獲得高效率。第二個缺點就是只能使用規定好的所謂的自動化數據類型。如果不用IDispatch我們可以想用什么數據類型就用什么類型,VC會自動給我們生成相應的調度代碼。而用自動化接口就不行了,因為Invoke的實現代碼是VC事先寫好的,而它不能事先預料到我們要用到的所有類型,它只能根據一些常用的數據類型來寫它的處理代碼,而且它也要考慮不同語言之間的數據類型轉換問題。所以VC自動化接口生成的調度代碼只適用于它所規定好的那些數據類型,當然這些數據類型已經足夠豐富了,但不能滿足自定義數據結構的要求。你也可以自己寫調度代碼來處理你的自定義數據結構,但這并不是一件容易的事。
考慮到IDispatch的種種缺點(現在一般都推薦寫雙接口組件,稱為dual接口,實際上就是從IDispatch繼承的接口。我們知道任何接口都必須從IUnknown繼承,IDispatch接口也不例外。那從IDispatch繼承的接口實際上就等于有兩個基類,一個是IUnknown,一個是IDispatch,所以它可以以兩種方式來調用組件,可以通過IUnknown用虛函數表的方式調用接口方法,也可以通過IDispatch::Invoke自動化調度來調用這就有了很大的靈活性,這個組件既可以用于C++的環境也可以用于腳本語言中,同時滿足了各方面的需要。
相對比的,dispinterface是一種純粹的自動化接口,可以簡單的就把它看作是IDispatch接口(雖然它實際上不是的),這種接口就只能通過自動化的方式來調用,COM組件的事件一般都用的是這種形式的接口。
Custom接口就是從IUnknown接口派生的類,顯然它就只能用虛函數表的方式來調用接口了。
4) COM組件有三種,進程內、本地、遠程。對于后兩者情況必須調度接口指針及函數參數。
COM是一個DLL,它有三種運行模式。它可以是進程內的,即和調用者在同一個進程內,也可以和調用者在同一個機器上但在不同的進程內,還可以根本就和調用者在兩臺機器上。
這里有一個根本點需要牢記,就是COM組件它只是一個DLL,它自己是運行不起來的,必須有一個進程象父親般照顧它才行,即COM組件必須在一個進程內.那誰充當看護人的責任呢?
先說說調度的問題。調度是個復雜的問題,以我的知識還講不清楚這個問題,我只是一般性的談談幾個最基本的概念。我們知道對于WIN32程序,每個進程都擁有4GB的虛擬地址空間,每個進程都有其各自的編址,同一個數據塊在不同的進程里的編址很可能就是不一樣的,所以存在著進程間的地址轉換問題。這就是調度問題。對于本地和遠程進程來說,DLL和客戶程序在不同的編址空間,所以要傳遞接口指針到客戶程序必須要經過調度。Windows經提供了現成的調度函數,就不需要我們自己來做這個復雜的事情了。對遠程組件來說函數的參數傳遞是另外一種調度。
DCOM是以RPC為基礎的,要在網絡間傳遞數據必須遵守標準的網上數據傳輸協議,數據傳遞前要先打包,傳遞到目的地后要解包,這個過程就是調度,這個過程很復雜,不過Windows已經把一切都給我們做好了,一般情況下我們不需要自己來編寫調度DLL。
我們剛說過一個COM組件必須在一個進程內。對于本地模式的組件一般是以EXE的形式出現,所以它本身就已經是一個進程。對于遠程DLL,我們必須找一個進程,這個進程必須包含了調度代碼以實現基本的調度。這個進程就是dllhost.exe。這是COM默認的DLL代理。實際上在分布式應用中,我們應該用MTS來作為DLL代理,因為MTS有著很強大的功能,是專門的用于管理分布式DLL組件的工具。
調度離我們很近又似乎很遠,我們編程時很少關注到它,這也是COM的一個優點之一,既平臺無關性,無論你是遠程的、本地的還是進程內的,編程是一樣的,一切細節都由COM自己處理好了,所以我們也不用深究這個問題,只要有個概念就可以了,當然如果你對調度有自己特殊的要求就需要深入了解調度的整個過程了,這里推薦一本《COM+技術內幕》,這絕對是一本講調度的好書。
5) COM組件的核心是IDL。
我們希望軟件是一塊塊拼裝出來的,但不可能是沒有規定的胡亂拼接,總是要遵守一定的標準,各個模塊之間如何才能親密無間的合作,必須要事先共同制訂好它們之間交互的規范,這個規范就是接口。我們知道接口實際上都是純虛類,它里面定義好了很多的純虛函數,等著某個組件去實現它,這個接口就是兩個完全不相關的模塊能夠組合在一起的關鍵試想一下如果我們是一個應用軟件廠商,我們的軟件中需要用到某個模塊,我們沒有時間自己開發,所以我們想到市場上找一找看有沒有這樣的模塊,我們怎么去找呢?也許我們需要的這個模塊在業界已經有了標準,已經有人制訂好了標準的接口,有很多組件工具廠商已經在自己的組件中實現了這個接口,那我們尋找的目標就是這些已經實現了接口的組件,我們不關心組件從哪來,它有什么其它的功能,我們只關心它是否很好的實現了我們制訂好的接口。這種接口可能是業界的標準,也可能只是你和幾個廠商之間內部制訂的協議,但總之它是一個標準,是你的軟件和別人的模塊能夠組合在一起的基礎,是COM組件通信的標準。
COM具有語言無關性,它可以用任何語言編寫,也可以在任何語言平臺上被調用。但至今為止我們一直是以C++的環境中談COM,那它的語言無關性是怎么體現出來的呢?或者換句話說,我們怎樣才能以語言無關的方式來定義接口呢?前面我們是直接用純虛類的方式定義的,但顯然是不行的,除了C++誰還認它呢?正是出于這種考慮,微軟決定采用IDL來定義接口。說白了,IDL實際上就是一種大家都認識的語言,用它來定義接口,不論放到哪個語言平臺上都認識它。我們可以想象一下理想的標準的組件模式,我們總是從IDL開始,先用IDL制訂好各個接口,然后把實現接口的任務分配不同的人,有的人可能善長用VC,有的人可能善長用VB,這沒關系,作為項目負責人我不關心這些,我只關心你最后把DLL拿給我。這是一種多么好的開發模式,可以用任何語言來開發,也可以用任何語言也欣賞你的開發成果。
6) COM組件的運行機制,即COM是怎么跑起來的。
這部分我們將構造一個創建COM組件的最小框架結構,然后看一看其內部處理流程是怎樣的。
IUnknown *pUnk=NULL;
IObject *pObject=NULL;
CoInitialize(NULL);
CoCreateInstance(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IUnknown,
(void**)&pUnk);
pUnk->QueryInterface(IID_IOjbect, (void**)&pObject);
pUnk->Release();
pObject->Func();
pObject->Release();
CoUninitialize();
CoCreateInstance身上,讓我們來看看它內部做了一些什么事情。以下是它內部實現的一個偽代碼:
CoCreateInstance(....)
{
.......
IClassFactory *pClassFactory=NULL;
CoGetClassObject(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory,(void**)&pClassFactory);
pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnk);
pClassFactory->Release();
........
}
這段話的意思就是先得到類廠對象,再通過類廠創建組件從而得到IUnknown指針。
繼續深入一步,看看CoGetClassObject的內部偽碼:
CoGetClassObject(.....)
{
//通過查注冊表CLSID_Object,得知組件DLL的位置、文件名
//裝入DLL庫
//使用函數GetProcAddress(...)得到DLL庫中函數DllGetClassObject的函數指針。
//調用DllGetClassObject
}?
DllGetClassObject是干什么的,它是用來獲得類廠對象的。只有先得到類廠才能去創建組件.
下面是DllGetClassObject的偽碼:
DllGetClassObject(...)
{
......
CFactory* pFactory= new CFactory; //類廠對象
pFactory->QueryInterface(IID_IClassFactory, (void**)&pClassFactory);
//查詢IClassFactory指針
pFactory->Release();
......
}
CoGetClassObject的流程已經到此為止,現在返回CoCreateInstance,看看CreateInstance的偽碼:
CFactory::CreateInstance(.....)
{
...........
CObject *pObject = new CObject; //組件對象
pObject->QueryInterface(IID_IUnknown, (void**)&pUnk);
pObject->Release();
...........
}?
上面是從COM+技術內幕中COPY來的一個例圖,從圖中可以清楚的看到CoCreateInstance的整個流程。
7) 一個典型的自注冊的COMDLL所必有的四個函數
DllGetClassObject:用于獲得類廠指針
DllRegisterServer:注冊一些必要的信息到注冊表中
DllUnregisterServer:卸載注冊信息
DllCanUnloadNow:系統空閑時會調用這個函數,以確定是否可以卸載DLL
DLL還有一個函數是DllMain,這個函數在COM中并不要求一定要實現它,但是在VC生成的組件中自動都包含了它,它的作用主要是得到一個全局的實例對象。
8) 注冊表在COM中的重要作用
首先要知道GUID的概念,COM中所有的類、接口、類型庫都用GUID來唯一標識,GUID是一個128位的字串,根據特制算法生成的GUID可以保證是全世界唯一的。
COM組件的創建,查詢接口都是通過注冊表進行的。有了注冊表,應用程序就不需要知道組件的DLL文件名、位置,只需要根據CLSID查就可以了。當版本升級的時侯,只要改一下注冊表信息就可以神不知鬼不覺的轉到新版本的DLL。
9)事件和通知
在實際開發中,COM 組件用線程方式下載網絡上的一個文件,當我完成任務后,需要通知調用者。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? 通知方式 ? | ? 簡單說明 ? | |
? 直接消息 ? | ? PostMessage() | ? 向窗口或線程發個消息 ? |
? SendMessage() ? | ? 馬上執行消息響應函數 ? | |
? SendMessage(WM_COPYDATA...) ? | ? 發消息的同時,還可以帶過去一些自定義的數據 ? | |
? 間接消息 ? | ? InvalidateRect() | ? 被調用的函數會發送相關的一些消息 ? |
? 回調函數 ? | ? GetOpenFileName()...... ? | ? 當用戶改變文件選擇的時候,執行回調函數 ? |
回調函數的方式,是設計 COM 通知方法的基礎。回調函數,本質上是預先把某一函數的指針告訴我,當我有必要的時候,就直接呼叫該函數了,而這個回調函數做了什么,怎么做的,我是根本不關心的。好了,問你個問題:啥是 COM 的接口?接口其實就是一組相關函數的集合(這個定義不嚴謹,但你可以這么理解哈)。因此,在COM中不使用“回調函數”而是使用“回調接口”(說的再清楚一些,就是使用一大堆包裝好的“回調函數”集) ,回調接口,我們也叫“接收器接口”。
(圖:客戶端傳遞接收器接口指針給COM。當發生事件時,COM調用接收器接口函數完成通知)
連接點,通過連接點可以實現事件的回調。
(圖:連接點組件原理圖。左側為客戶端,右側為服務端(組件對象))
1、一個 COM 組件,允許有多個連接點對象(IConnectionPoint)。也就是說可以有多個發生“事件”的源頭。上圖就有3個連接點;
2、管理這些連接點的接口叫“連接點容器”(IConnectionPointContainer)。連接點容器接口特別簡單,因為只有2個函數,一個是 FindConnectionPoint(),表示查找你想要的連接點;另一個是 EnumConnectionPoints(),表示列出所有的連接點,然后你去選擇使用哪個。在實際的應用中,查找法使用最多,占90%,而枚舉法使用只占 10%,一般在支持第三方的插件(Plug in)時才使用。
3、每一個連接點,可以被多個客戶端的接收器(Sink)連接
10)代理和存根
COM兩個組件之間不是直接通信,而是通過代理和存根來之間的通信來間接實現的。圖中的channel是com庫的一部分。 COM里,只有進程外組件才會用到代理(proxy)和存根(stub)。 代理在客戶的進程內創建,存根在組件com對象的進程中創建。 每個接口的每個函數都有自己的代理和存根。
為什么要用代理和存根 ?
客 戶為什么要用代理和存根,而不直接同對象連接呢?給你一個理由,對客戶來說,他與所有com對象的連接都是通過指針來調用的, 而對服務來說,調用對象的接口函數也是通過指針來完成的,然而,指針只有在同一進程內才會有效。這樣,代理和存根為了完成這個使命也就產生了。
代理和存根的作用不只這些,他還要打包所有的參數(包括接口指針),產生 RPC(遠程進程調用),通向另一個進程,或者對象運行所在的另一臺機器。
圖: 代理的結構
上圖所顯示的代理結構支持參數的標準列集。每個接口的代理實現了IRpcProxyBuffer接口,用于內聚各個部分之間的相互通信。當代理準備把已列集的參數傳遞過進程邊界時,他調用IRpcChannelBuffer?接口的方法(該接口由channel實現)。channel調用RPC運行庫使數據傳輸到目的地。
圖: 存根的結構
如上圖所示,每個接口的存根被連接到對象的相應接口上。chnnel分發傳入的消息到適當的接口的存根。所有的組件通過IRpcChannelBuffer接口與chnnel交流,這個接口提供了與RPC運行庫的連接。
列集(marshalling)
說到代理和存根,自然少不了列集,什么是列集?
列集,對函數參數進行打包處理得過程,因為指針等數據,必須通過一定得轉換,才能被另一組件所理解,列集完成后,RPC調用就會產生。可以說列集是一種數據格式的轉換方法。
列集有3種方式:
1. 類型庫列集
它可以列集與OLEAUTOMATION兼容的任何接口,意思是你的接口的返回值必須是HRESULT,所使用的參數的類型也應該是與C++的VARIANT結構兼容。
2. 通過創建Stub / proxy DLL
這個DLL的源代由MIDL產生。你必須在服務器和客戶機上都注冊這個DLL(這是標準的marshal 方式)使用吃方法時,最好把stub / proxy代碼編譯作為一個獨立的組件。
3. 自定義marshaling
自定義marshal要求在你的組件中必須實現IMarshal接口。當COM需要marchal時,他首先通過QueryInterface看你是否支 持IMarshal接口,如果你實現了該接口,也就是說,由你控制了你的COM的所有參數和返回值的打包、解包的方法模式。
代理和存根dll的建立
使用工具MIDL,對一個IDL文件,MIDL會分析自動產生相應的代理/存根 DLL的相關文件。
怎么使用代理和存根
COM組件對外輸出接口有兩種模式:TLB庫((TypeLibrary)模式和代理/存根(Proxy/stud)模式。如果COM組件是通過TLB庫模式輸出的時候,將會生成一個.tlb文件,這種方法客戶端調用也方便(直接導入即可),同時支持跨語言開發和調用,所以是VC默認的COM接口輸出模式。但是該模式將不支持部分IDL語言定義的接口屬性,例如:size_is,length_is,[]ref等相關屬性。關于tlb的說明請參考http://msdn.microsoft.com/en-us/library/aa366757(v=vs.85).aspx文檔。代理/存根(Proxy/stud)模式相對就比較麻煩,并且不能支持跨語言開發和調用,但是可以很好的支持IDL語言定義的各種屬性。下面我們主要說明一下代理/存根(Proxy/stud)模式的生產和組件調用。關于IDL接口的定義和說明請參考http://msdn.microsoft.com/en-us/library/aa367091(v=VS.85).aspx文檔。
代理/存根(Proxy/stud)模式下COM組件或者服務的生成:由于VC編譯器默認生成的是tlb庫模式,所以首先需要通過/notlb編譯選項告訴編譯器我們不需要tlb庫輸出,同時需要在資源文件(.rc)中將導入tlb庫的命令取消(TYPELIB "Interface.tlb")。如果為services服務,在執行前面步驟操作后需要編寫一個.mk文件。該文件主要用來將idl接口文件生產的.h, _i.c, _p.c,dlldata.c編譯生產代理/存根(Proxy/stud)需要的dll文件, 并通過nmake-f DataExchangeServer.mk 命令編譯和生產dll文件,最后通過regsvr32命令將dll注冊為服務。
10)COM使用線程
單線程單元(STA)、多線程單元(MTA)
單線程單元:只允許一個線程訪問組件,但是在一個進程中允許有多個單線程單元。
多線程單元:多個線程可以同時訪問組件。
11)錯誤處理
在舊COM技術中,錯誤是通過方法返回HRESULT值來定義的。HRESULT的值是S_OK,表示方法成功。
新COM組件就實現接口ISupportErrorInfo,該接口不但提供了錯誤信息,還提供了幫助文件的鏈接、錯誤源,在方法返回時還會返回一個錯誤信息對象。
?
四、VC調用COM的方式
原文出處:http://topic.csdn.net/t/20040417/16/2977524.html,此篇轉載稍有修改。
準備及條件: ??
? COM服務器為進程內服務器,DLL名為simpCOM.dll,該組件只有一個接口IFoo,該接口只有一個方法HRESULT ? SayHello(void)???
????
? 在SDK中調用 ??
? ===================================== ??
? 1、最簡單最常用的一種,用#import導入類型庫,利用VC提供的智能指針包裝類 ??
? 演示代碼:???
? #import ? "D:/Temp/vc/simpCOM/Debug/simpCOM.dll" ?no_namespace ??
? CoInitialize(NULL);???
? IFooPtr ? spFoo ? = ? NULL; ??
? spFoo.CreateInstance(__uuidof(Foo)); ??
? spFoo->SayHello(); ??
? spFoo.Release();/*暈死了,本來智能指針就是為了讓用戶不用關心這個的,可是我發現如果不手工調用一下的話,程序退出后會發生內存訪問錯誤,我是在console中做試驗的,哪位大俠知道怎么回事請一定指教*/ ?
? CoUninitialize();???
/*lynn注:確實是IXXXPtr智能指針所引起的問題,正確的方式有:
1).限制智能指針作用域:
CoInitialize(NULL);???
{
? IFooPtr ? spFoo ? = ? NULL;?//這樣寫也沒問題,IFooPtr應該重載了賦值操作,但是IFooPtr是個類,不要把它當成真的原始指針了
? spFoo.CreateInstance(__uuidof(Foo)); ??
? spFoo->SayHello(); ??
?// spFoo.Release();
}
??CoUninitialize();
2).跟原句差不多:
? CoInitialize(NULL);???
? IFooPtr ? spFoo ? = ? NULL; ??
? spFoo.CreateInstance(__uuidof(Foo)); ??
? spFoo->SayHello(); ??
//? spFoo.Release();?
? spFoo??= NULL;? // 網上找的,所以可以肯定IFooPtr定義了賦值操作
? CoUninitialize();???
*/
????
????
? 2、引入midl.exe產生的*.h,*_i.c文件,利用CoCreateInstance函數來調用???
? 演示代碼: ??
? /*在工程中加入*_i.c文件,例如本例的simpCOM_i.c,該文件定義了類和接口的guid值,如果不引入的話,會發生連接錯誤。*/???
? #include ? "D:/Temp/vc/simpCOM/simpCOM.h" ??
? CoInitialize(NULL);???
? IFoo* ? pFoo ? = ? NULL; ??
? HRESULT ? hr ? = ? CoCreateInstance(CLSID_Foo, ?NULL, ? CLSCTX_ALL, ? IID_IFoo, ? (void**)&pFoo); ?
? if ? (SUCCEEDED(hr) ? && ? (pFoo ? != ?NULL)) ??
? { ??
? pFoo->SayHello(); ??
? pFoo->Release(); ??
? }???
? CoUninitialize();???
/*
lynn注: 二適合有COM組件源碼的情況,比如自己編寫的COM組件,在VS項目里的“生成的文件”里的那兩個文件
*/
????
?3、不用CoCreateInstance,直接用CoGetClassObejct得到類廠對象接口,然后用該接口的方法CreateInstance來生成實例。 ?
? 演示代碼: ??
? /*前期準備如二方法所述*/ ??
? IClassFactory* ? pcf ? = ? NULL; ??
? HRESULT hr ? = ? CoGetClassObject(CLSID_Foo, ?CLSCTX_ALL, ? NULL, ? IID_IClassFactory, ? (void**)&pcf);?
? if ? (SUCCEEDED(hr) ? && ? (pcf ? != ?NULL)) ??
? { ??
? IFoo* ? pFoo ? = ? NULL; ??
? hr ? = ? pcf->CreateInstance(NULL, ? IID_IFoo, ?(void**)&pFoo); ??
? if ? (SUCCEEDED(hr) ? ? && ? (pFoo ? !=? NULL)) ??
? { ??
? pFoo->SayHello(); ??
? pFoo->Release(); ??
? } ??
? pcf->Release(); ??
? }???
????
? 4、不用CoCreateInstance ? or ? CoGetClassObject,直接從dll中得到DllGetClassObject,接著生成類對象及類實例(本方法適合于你想用某個組件,卻不想在注冊表中注冊該組件)???
? 演示代碼: ??
? /*前期準備工作如二方法所述,事實上只要得到CLSID和IID的定義及接口的定義就行*/???
? typedef ? HRESULT ? (__stdcall ? * ? pfnGCO) ?(REFCLSID, ? REFIID, ? void**); ?
? pfnGCO ? fnGCO ? = ? NULL; ??
? HINSTANCE ? hdllInst ? = ? LoadLibrary("D://Temp//vc//simpCOM//Debug//simpCOM.dll");?
? fnGCO ? = ? (pfnGCO)GetProcAddress(hdllInst, ?"DllGetClassObject"); ??
? if ? (fnGCO ? != ? 0) ??
? { ??
? IClassFactory* ? pcf ? = ? NULL; ??
? HRESULT ? hr=(fnGCO)(CLSID_Foo, ? IID_IClassFactory, ?(void**)&pcf); ??
? if ? (SUCCEEDED(hr) ? && ? (pcf ? != ?NULL)) ??
? { ??
? IFoo* ? pFoo ? = ? NULL; ??
? hr ? = ? pcf->CreateInstance(NULL, ? IID_IFoo, ?(void**)&pFoo); ??
? if ? (SUCCEEDED(hr) ? ? && ? (pFoo ? !=? NULL)) ??
? { ??
? pFoo->SayHello(); ??
? pFoo->Release(); ??
? } ??
? pcf->Release(); ??
? } ??
? } ??
? FreeLibrary(hdllInst);???
? 在MFC中調用 ??
? ===================================== ??
? 在MFC中除了上面的幾種方法外,還有一種更方便的方法,就是通過ClassWizard利用類型庫生成包裝類,不過有個前提就是com組件的接口必須是派生自IDispatch ?
? ??
? 具體方法: ??
? 1、按Ctrl+W調出類向導,按Add ? Class按鈕彈出新菜單,選From ? a ? type? libarary,然后定位到simpCOM.dll,接下來會出來該simpCOM中的所有接口,選擇你想生成的接口包裝類后,向導會自動生成相應的.cpp和.h文件. ?
? 這樣你就可以在你的MFC工程中像使用普通類那樣使用COM組件了.???
? 演示代碼:???
? CoInitialize(NULL);???
? IFoo ? foo; ??
? if ? (foo.CreateDispatch("simpCOM.Foo") ? != ?0) ??
? { ??
? foo.SayHello(); ??
? foo.ReleaseDispatch(); ??
? }???
? CoUninitialize();
?
五、COM使用的注意細節
1.ConvertStringToBSTR 也是需要釋放內存空間的
Example?
// ConvertStringToBSTR.cpp?
#include <comutil.h>?
#include <stdio.h>?
#pragma comment(lib, "comsupp.lib")?
#pragma comment(lib, "kernel32.lib")?
int main()?
{?
char* lpszText = "Test";?
printf("char * text: %s/n", lpszText);?
BSTR bstrText = _com_util::ConvertStringToBSTR(lpszText);?
wprintf(L"BSTR text: %s/n", bstrText);?
SysFreeString(bstrText);?
}?
或者:
??? char*?? pTemp;
??? CString csTemp;
??? pTemp =_com_util::ConvertBSTRToString(bsVal);
??? csTemp = pTemp;
??? delete pTemp;
??? pTemp = NULL;
2.對于throw()的函數,為了統一處理com錯誤,可以這樣
?inline?? void?? TESTHR(HRESULT??x)?? {if?? FAILED(x)?? _com_issue_error(x);};??
? ...???
? try???
? {???
? TESTHR(pConnection.CreateInstance(__uuidof(Connection)));?? //智能指針的CreateInstance是不拋異常的(根據HRESULTE判斷調用是否成功),這里加了個宏就使異常處理統一了
? pConnection->Open(strCnn,?? "",??"",?? adConnectUnspecified);???
? pConnection->Open(strCnn,?? "",??"",?? adConnectUnspecified);???
? }???
? catch(_com_error?? &e)???
? {???
? CString?? t;???
? t.Format("%s",?? e.ErrorMessage());???
? AfxMessageBox(t);???
? }
3、在stdafx.h文件導入dll能夠讓編譯器在運行時連接dll的類型庫,#import它能夠自動產生一個對GUIDs的定義,同時自動生成對clsado對象的封裝。同時能夠列舉它在類中所能找到的類型, ? VC++會在編譯的時候自動生成兩個文件:???
? 一個頭文件(.tlh),它包含了列舉的類型和對類型庫中對象的定義;???
? 一個實現文件(.tli)對類型庫對象模型中的方法產生封裝。???
? Namespace(名字空間)用來定義一個名字空間,使用unsing就可以將當前的類型上下文轉換名字空間所定地,讓我們可以訪問服務組件的方法。???
? 如果我們修改了服務組件程序,建議刪除這兩個文件后重新完整編譯工程,以便讓編譯器重新列舉類的屬性以及函數。??
4、COM接口類型
COM接口定義時可分為直接繼承IUnkown接口(vtable結構)、直接聲明為dispinterface和由IDispatch繼承三種形式,一般來說分別稱之為純接口、分發接口和雙接口
純接口只有C/C++語言可以聲明,理所當然是.h頭文件的形式,也只有C/C++語言可以感知,但是通常并不使用C/C++直接書寫.h頭文件,而是使用midl命令對idl文件(接口定義文件)生成C/C++同時兼容的.h頭文件。
VB和腳本語言只能識別分發接口的結構,這些語言的運行時庫或虛擬機可以生成和識別.tlb文件,而.tlb文件也可以通過使用midl命令編譯idl文件生成。?
可以看出.h頭文件和.tlb類型庫文件實際上是起到相同的作用,即定義和感知接口的二進制結構,只不過一個是使用文本形式,一個是使用字節碼形式。?
需要說明的是C/C++語言是可以感知分發接口的結構的,因為它的編譯器可以解析.tlb類型庫文件,方法是:#import"xxx.tlb" no_namespace,這就是說VB和腳本語言實現的COM組件可以被C/C++語言環境使用,而C/C++語言定義的接口(.h頭文件中定義)不能被VB和腳本語言感知,為了解決這個問題雙接口應運而生,雙接口的結構實際上就是純接口+分發接口。在使用C/C++語言開發COM組件時,使用雙接口定義就可以被VB和腳本語言使用了,當然雙接口的接口函數的參數必須服從和分發接口情況下一樣的限制,即必須是OLE變量兼容類型(Variant兼容類型)。
?
5、COM線程模型
COM提供的線程模型共有三種:Single-ThreadedApartment(STA 單線程套間)、MultithreadedApartment(MTA 多線程套間)和NeutralApartment/Thread Neutral Apartment/Neutral Threaded Apartment(NA/TNA/NTA中立線程套間,由COM+提供)。雖然它們的名字都含有套間這個詞,這只是COM運行時期庫(注意,不是COM規范,以下簡稱COM)使用套間技術來實現前面的三種線程模型,應注意套間和線程模型不是同一個概念。COM提供的套間共有三種,分別一一對應。而線程模型的存在就是線程規則的不同導致的,而所謂的線程規則就只有兩個:代碼是線程安全的或不安全的,即代碼訪問公共數據時會或不會發生訪問沖突。由于線程模型只是個模型,概念上的,因此可以違背它,不過就不能獲得COM提供的自動同步調用及兼容等好處了。
STA 一個對象只能由一個線程訪問(通過對象的接口指針調用其方法),其他線程不得訪問這個對象,因此對于這個對象的所有調用都是同步了的,對象的狀態(也就是對象的成員變量的值)肯定是正確變化的,不會出現線程訪問沖突而導致對象狀態錯誤。其他線程要訪問這個對象,必須等待,直到那個唯一的線程空閑時才能調用對象。注意:這只是要求、希望、協議,實際是否做到是由COM決定的。如上所說,這個模型很像Windows提供的窗口消息運行機制,因此這個線程模型非常適合于擁有界面的組件,像ActiveX控件、OLE文檔服務器等,都應該使用STA的套間。
MTA 一個對象可以被多個線程訪問,即這個對象的代碼在自己的方法中實現了線程保護,保證可以正確改變自己的狀態。這對于作為業務邏輯組件或干后臺服務的組件非常適合。因為作為一個分布式的服務器,同一時間可能有幾千條服務請求到達,如果排隊進行調用,那么將是不能想像的。注意:這也只是一個要求、希望、協議而已。
NA 一個對象可以被任何線程訪問,與MTA不同的是任何線程,而且當跨套間訪問時(后面說明),它的調用費用(耗費的CPU時間及資源)要少得多。這準確的說都已經不能算是線程模型了,它是結合套間的具體實現而提出的要求,它和MTA不同的是COM的實現方式而已。
STA套件的初始化方式(兩種方式等效):
1,CoInitialize(nil);
2,CoInitializeEx(nil,COINIT_APARTMENTTHREADED);
MTA套間的初始化方式:
1,CoInitializeEx(nil,COINIT_MULTITHREADED);
?
編寫可以工作的COM客戶端
要編寫可以工作的COM客戶端,需要遵循三條規則。牢記這些規則,你就可以在編寫COM客戶端時避免嚴重的錯誤。
規則1:客戶線程必須調用CoInitialize[Ex]
線程做任何與COM相關的操作之前,必須調用CoInitialize或者CoInitializeEx初始化COM。如果客戶程序有20個線程,其中10個使用COM,則這10個線程都應該調用CoInitialize或者CoInitializeEx。調用線程將在這兩個API中被分配給一個套間。對于沒有分配給套間的線程,COM是無法施行并發規則的。此外還要記住,成功調用了CoInitialize或者CoInitializeEx的線程應該在終止前調用CoUninitialize。否則,由CoInitialize[Ex]分配的資源將直到進程終止才釋放。
這條規則看起來很簡單,只是一個函數調用而已。但是你會驚奇地發現,這條規則經常被違背。違背這條規則的錯誤一般在調用CoCreateInstance或者其他COM API時展現。但是有時候問題直到很晚才出現,而且客戶端的錯誤似乎與沒有初始化COM沒有明顯的關系。
具有諷刺意味的是,有時候開發者不調用CoInitialize[Ex]的原因是,微軟告訴他們不需要調用。MSDN中有篇文章說COM客戶端有時候可以避免調用這個函數。但文章隨后說這可能會導致拒絕訪問。我近期收到一個開發者的電話,說客戶線程調用Release的時候會死鎖或者發生拒絕訪問異常。原因是?有些線程沒有調用CoInitialize[Ex]就發起方法調用了,結果調用Release的時候發生問題了。幸運地是,解決問題只需要簡單地加幾個CoInitialize[Ex]調用。
記住:調用CoInitialize[Ex]總是沒有壞處的。對于調用COM API或者以任何方式使用COM對象的線程,調用CoInitialize[Ex]應該說是必須的。
規則2:STA線程需要消息循環
如果不理解單線程套間機制,這條規則看起來不那么明顯。客戶調用基于STA的對象時,調用將被傳遞到STA中運行的線程。COM通過向STA的隱藏窗口投遞消息來完成這種傳遞。那么,如果STA中的線程不接收和分發消息將發生什么?調用將在RPC通道中消失,永遠也不返回。它將永遠凋謝在STA的消息隊列中(It will languish in the STA's messagequeue forever)。
開發者問我為什么方法調用不返回的時候,我首先問他們“你調用的對象是在STA中嗎?如果是,驅動STA的線程是否有消息循環?”。多半的回答是“我不知道”。如果你不知道,你就是在玩火。調用CoInitialize,或者使用參數COINIT_APARTMENTTHREADED調用CoInitializeEx,或者調用MFC的AfxOleInit的時候,線程被分配到一個STA中。如果隨后在這個STA中創建對象,而STA線程又沒有消息泵,那么對象不能接收來自其他套間的客戶的方法調用。消息泵可以這樣簡單:
MSG msg;
while (GetMessage(&msg, 0, 0, 0))
{
????? DispatchMessage (&msg)
}
如果缺少這些簡單的語句,把線程放入STA時要當心。一個常見的情況是MFC應用程序啟動工作線程(MFC工作線程的定義是,缺少消息泵的線程),而線程調用AfxOleInit將自身放入到STA中。如果STA不容納任何對象,或者雖然容納對象但是卻沒有來自其他套間的客戶,你不會遇到問題。但是如果STA容納導出接口指針到其他套間的對象,則對這些接口指針的調用將永遠不會返回。
規則3:不要在套間之間傳遞原始未列集的接口指針
設想編寫一個有兩個線程的COM客戶端。兩個線程都調用CoInitialize進入一個STA,然后其中一個線程——線程A,使用CoCreateInstance創建一個COM對象。線程A想要與線程B共享從CoCreateInstance返回的接口指針。所以線程A將接口指針賦值給一個全局變量,然后通知線程B指針已經準備好了。線程B從全局變量讀取接口指針并且對對象發起調用。這個過程有什么錯誤嗎?
這個過程會引發事故。問題是線程A向其他套間中的線程傳遞了原始未列集的接口指針。線程B應該只通過列集到線程B所屬套間的接口指針與對象通信。
這里“列集(Marshaling)”的意思是給COM在線程B所屬套間中創建新代理的機會,讓線程B可以安全地進行調用。在套間之間傳遞原始接口指針的后果可以從與時間極其相關(也很難重現)的數據損壞到完全死鎖。
如果線程A列集接口指針,則可以安全地與線程B共享接口指針。COM客戶端有兩種基本的方法將接口指針列集到其他套間:
使用COM API函數CoMarshalInterThreadInterfaceInStream和CoGetInterfaceAndReleaseStream。
線程A調用CoMarshalInterThreadInterfaceInStream列集接口指針,線程B調用CoGetInterfaceAndReleaseStream進行散集。通過函數CoGetInterfaceAndReleaseStream,COM在調用者套間中創建新的代理。如果接口指針不需要進行列集(比如說,兩個線程共享同一個套間時),CoGetInterfaceAndReleaseStream會智能地不創建代理。
使用在Windows NT4.0 Service Pack 3中首次引入的全局接口表(Global Interface Table,GIT)。
GIT是每個進程一個的表格,讓各個線程可以安全地共享接口指針。如果線程A想要與同一個進程中的其他線程共享接口指針,可以使用IGlobalInterfaceTable::RegisterInterfaceInGlobal來將接口指針放到GIT中。然后想要使用接口的線程可以調用IGlobalInterfaceTable::GetInterfaceFromGlobal來獲取接口指針。神奇之處在于線程從GIT獲取接口指針的時候,COM會將接口指針列集到獲取線程所屬的套間中。
有沒有不列集需要與其他線程共享的接口指針也OK的情況?有。如果兩個線程屬于同一個套間,則可以共享原始未列集的接口指針,而這只可能在兩個線程都屬于MTA時發生。如果不確定是否需要,請進行列集。調用CoMarshalInterThreadInterfaceInStream和CoGetInterfaceAndReleaseStream或者使用GIT總是無害的,因為COM只在必要的時候才進行列集。
編寫可以工作的COM服務器
編寫COM服務器時也應該遵守一些規則。
規則1:保護ThreadingModel=Apartment的對象的共享數據
標記對象的ThreadingModel=Apartment就可以不考慮線程安全問題?這是關于COM編程的一個最常見的錯誤想法。注冊進程內對象的ThreadingModel=Apartment暗示COM,對象(以及從DLL創建的其他對象)會以線程安全的方式訪問共享數據。這意味著已經使用臨界區或者其他線程同步原語來保證在任何時刻只有一個線程可以接觸到共享數據。對象之間數據共享通常有三種方式:
DLL中聲明全局變量
C++類中的靜態成員變量
靜態局部變量
為什么線程同步對于ThreadingModel=Apartment的對象是很重要的?考慮從同一個DLL創建兩個對象A和B的情況。假定兩個對象都讀寫在DLL中聲明的一個全局變量。因為標記為ThreadingModel=Apartment,對象可能分別在不同的STA中創建和運行,因此,也是在不同的線程中運行。但是兩個對象訪問的全局變量是共享的,只在進程內實例化一次。如果來自A和B的調用幾乎同時發生,而且A寫入那個變量,B讀取那個變量(或者相反),那么變量可能被破壞,除非串行化線程的操作。如果不提供同步機制,那么多數時候會遇到問題。最終兩個線程可能在共享數據上發生沖突,后果無法預知。
存在不需要同步機制就可以安全地訪問共享數據的情況嗎?存在。下列條件下可以不需要同步機制:
沒有為對象注冊ThreadingModel值(也稱作ThreadingModel=None或者ThreadingModel=Single)時,所有對象在相同STA(主STA)和相同線程中運行,因此不會在共享數據上發生沖突。
雖然標記為ThreadingModel=Apartment,但是確信對象將在相同的STA中運行(比如說,所有對象都由同一個STA線程創建)。
確信對象不會被并發地調用時。
對于除此之外的情況,要確保ThreadingModel=Apartment的對象以線程安全的方式訪問共享數據,只有這樣才是正確完成了任務。
規則2:標記為ThreadingModel=Free或者ThreadingModel=Both的對象應該是線程安全的。
標記對象是ThreadingModel=Free或者ThreadingModel=Both時,對象將被或者可能被放入到MTA中。記住:COM不會串行化對基于MTA的對象的調用。因此,毫無疑問地(beyond the shadow of a doubt),除非確信對象的客戶不會進行并發調用,對象應該是完全線程安全的。這意味著除了要同步由多個實例共享的數據之外,還必須同步對非靜態成員變量的訪問。編寫線程安全的代碼不容易,但是如果準備使用MTA,就必須這么做。
規則3:避免在標記為ThreadingModel=Free或者ThreadingModel=Both的對象里使用線程局部存儲(TLS)
一些Windows程序員使用線程局部存儲臨時保存數據。設想在實現一個COM方法時,需要緩存一些關于當前調用的信息,以備下次調用時使用。這時你可能很想使用TLS。在STA中,這樣做沒問題。但是如果對象在MTA中,就應該像躲避瘟疫那樣避免使用TLS。
為什么?因為進入MTA的調用被傳遞給RPC線程。每次調用可能被傳遞給不同的RPC線程,即使調用都是來自于同一個線程中的同一個調用者。一個線程不能訪問另一個線程的線程局部存儲。所以如果調用1到達線程A,對象將數據保存在TLS中;然后調用2到達線程B,對象試圖取出在調用1中存入TLS的數據時,會找不到數據。這個道理很簡單。
對于基于MTA的對象,在方法調用之間使用TLS緩存數據時要注意,這種方法只在所有的方法調用來自于對象所在的MTA中的同一個線程時才可以正確工作。
?
參考資料:
《COM技術內幕》
COM 組件設計與應用
http://www.vckbase.com/document/viewdoc/?id=1483
vc中調用Com組件方法
http://www.cppblog.com/woaidongmao/archive/2011/01/10/138250.html
理解COM套間:
http://www.cnblogs.com/Quincy/archive/2011/03/03/1969510.html