COM技術內幕
第一章 組件
將單個應用程序分隔成多個獨立的部分,即組件。
對組件的需求:動態連接,信息封裝。
對組件的限制:1。語言無關;2.升級不妨礙使用;3.位置透明.
COM是一個說明如何建立可動態互變組件的規範。
第二章 接口
對com來說,接口是一個包含一個函數指針數組的內存結構。每一個數組元素包含的是一個由組件所實現的函數的地址。對於com而言,接口就是此內存結構,其他東西均是一個com並不關心的實現細節。
接口的目的是封裝,優點是模塊複用,多態。
用__stdcall標記的函數將使用標準的調用約定,即這些函數將在返回到調用者之前將參數從棧中刪除。Pascal函數對於棧的處理使用的也是同一種方式。在常規的C/C++調用約定中,棧的清理工作則是由調用者完成的。大多數其他編程語言。如VB缺省情況下使用的也是標準的調用約定。標準調用約定名稱的由來在於所有的win32API函數,除了那些帶有變參的外,使用的都是這種調用方式。帶有變參的函數所用的仍然是C調用約定,即__cdecl。Windows採用標準的調用約定的原因在於這種約定可以減少代碼的大小。
COM接口在C++中使用純抽象基類實現的。一個C++類可以使用多繼承來實現一個可以提供多個接口的組件。因此引起的命名衝突改名即可,因COM是二進制標準。
#define STDMETHODCALLTYPE __stdcall
要實現多態,就需要使用vtable(vtbl),再加一級pVtable(vtpr)就更爲靈活,還可以讓多個對象共享vtbl。
第三章 QueryInterface函數
QueryInterface的實現:支持則返回S_OK,不支持則指針置爲NULL,返回E_NOINTERFACE。非虛擬繼承:注意IUnknown並不是虛擬基類,所以COM接口並不能按虛擬方式繼承IUnknown,這是由於會導致與COM不兼容的vtbl。若COM接口按虛擬方式繼承IUnknown,那麼COM接口的vtbl中的頭三個函數指向的將不是IUnknown的三個成員函數。
通常將一種類型的指針轉換成另外一種類型並不會改變它的值。但爲了支持多重繼承,在某些情況下,C++必須改變類指針的值。
第四章 引用計數
客戶應該當作計數是在接口一級實現的,COM並沒有做要求,組件可自己選擇。
第五章 動態鏈接
在函數的定義前加上extern “C”可防止C++編譯器在函數名稱上加上類型信息。
第六章 關於HRESULT、GUID、註冊表及其他細節
HRESULT
HRESULT的最高位表示函數調用是否成功,低16位表示返回值,其餘15位包含的是此類型及返回值起源的更詳細的信息。
常用HRESULT值:
S_OK:成功並返回真。
NOERROR:同上。
S_FALSE:成功並返回假。
E_UNEXPECTED:無法預知的失敗。
E_NOIMPLE:函數沒實現。
E_NOINTERFACE:接口不支持。
E_OUTOFMEMORY:分配內存失敗。
E_FAIL:沒有指定的失敗。
一般不能直接將HRESULT值同某個成功代碼(如S_OK)進行比較以決定某個函數是否成功也不能直接將其同某個失敗代碼(如E_FAIL)進行比較以決定函數調用是否失敗。應該使用SECCEEDED和FAILED宏。
當前所定義的設備代碼:
FACILITY_WINDOWS 8
FACILITY_STORAGE 3
FACILITY_SSPI 9
FACILITY_RPC 1
FACILITY_WIN32 7
FACILITY_CONTROL 10
FACILITY_NULL 0
FACILITY_ITF 4 接口相關
FACILITY_DISPATCH 2
FACILITY_CERT 11
WINERROR.H中包含當前由系統產生的所有COM狀態代碼,但是若某個具有FACILITY_WIN32設備代碼HRESULT值,通常它將是一個被映射成HRESULT值的Win32錯誤代碼,我們應該查找其低16位對應的十進制數。可使用ErrorMessage函數得到錯誤信息。
關於定義自己的HRESULT的一些一般性規則:
(1)不要將0X0000及IX01FF範圍內的值作爲返回代碼。這些值是爲COM所定義的FACILITY_ITF代碼而保留的。只有遵循這一規則,纔不致使用戶自己定義的代碼同COM所定義的代碼相混淆。
(2)不要傳播FACILITY_ITF錯誤代碼。
(3)儘可能使用通用的COM成功及失敗代碼。
(4)避免定義自己的HRESULT,而可以在函數中使用一個輸出參數。
用MAKE_HRESULT宏來定義一個HRESULT值,此宏可根據所提供的嚴重級別、設備代碼及返回代碼生成一個HRESULT值。如:
MAKE_HRESULT(SEVERITY_ERROR,FACILITY_ITF,100);
GUID
爲了讓GUID的聲明和定義使用同一個頭文件,可使用DEFINE_GUID來定義GUID,若沒有包含INITGUID.H,它是一個聲明,否則便是定義。
註冊表
COM只使用了註冊表的一個分支:HKEY_CLASSES_ROOT,它下面的CLSID記錄了組件的相關信息,缺省值爲組件名,InprocServer32記錄了DLL位置。還有許多的擴展名,在擴展名之後,可以看到許多其它名字,此類名字大多被稱作是ProgID,還有一些其它的特殊關鍵字。
一些特殊關鍵字:
(1)AppID:此關鍵字下的子關鍵字的作用是將某個APPID(應用程序ID)隱射成某個遠程服務器名稱。分佈式COM將用到此關鍵字。
(2)組件類別:註冊表的這一分支可以將CATID(組件類別ID)映射成某個特定的組件類別。
(3)Interface:用於將IID映射成與某個接口相關的信息。
(4)Licenses:保存授權使用COM組件的一些許可信息。
(5)TypeLib:類型庫關鍵字所保存的是關於接口成員函數所用參數的信息等。
組件類別可使用Component Category Manager來完成註冊,它實現的ICatRegister和ICatInformation分別管理組件類別的註冊和信息查詢。
COM庫
對每一個進程,COM庫函數只需初始化一次。這並不是說不能多次調用CoInitailize,但需保證每一個CoInitialize都有一個相應的CoUninitialize調用。當進程已經調用過CoInitialize後,再次調用此函數所得到的返回值將是S_FALSE而不再是S_OK.
OLE是建立在COM基礎之上的,它增加了對類型庫、剪貼板、拖放、ActiveX文檔、自動化以及ActiveX控件的支持。在OLE庫中包含對這些特性的額外的支持。在需要使用這些特性時,應調用OleInitailize及OleUninitialize,而不是CoInitailize和 CoUninitialize。Ole*函數將調用Co*函數。但若程序中沒有用到那些額外的功能,使用Ole*將會造成資源的浪費。
COM中分配和釋放內存的標準方法:任務內存分配器。使用此分配器,組件可以給客戶提供一塊可以由客戶刪除的內存。COM庫爲此提供了一些方便的函數:CoTaskMemAlloc和CoTaskMemFree。
StringFromCLSID 將CLSID轉化成文本串
StringFromIID 將IID轉化成文本串
StringFromGUID2 將GUID轉化成文本串。此函數由調用者分配內存
CLSIDFromString 將一個文本串轉化成CLSID
IIDFromString 將一個文本串轉化成IID
定義接口可以使用宏DECLARE_INTERFACE、STDMETHOD。
第七章 類廠
CoCreateInstance的聲明:
HRESULT __stdcall CoCreateInstance(
const CLSID& clsid,
IUnknown* pIUnknownOuter,
DWORD dwClsContext,
const IID& iid,
void** ppv
);
CoCreateInstance有四個輸入參數和一個輸出參數。第一個參數是待創建組件的CLSID。第二個參數是用於聚合組件的。第三個參數的作用是限定所創建的組件的執行上下文。第四個參數iid爲組件上待使用的接口的IID。CoCreateInstance將在最後一個參數中返回此接口的指針。
CoCreateInstance的第三個參數dwClsContext(類上下文)可以控制所創建的組件是在與客戶相同的進程中運行,還是在不同的進程中運行,或者是在另外一臺機器上運行。其取值爲下列各值的組合:
CLSCTX_INPROC_SERVER
客戶希望創建在同一進程中運行的組件。爲能夠同客戶在同一進程中運行,組建必須是在DLL中實現的。
CLSCTX_INPROC_HANDLER
客戶希望創建進程中處理器。一個進程中處理器實際上是一個只實現了某個組件一部分的進程中組件。該組件的其他部分將由本地或遠程服務器上的某個進程外組件實現。
CLSCTX_LOCAL_SERVER
客戶希望創建一個在同一機器上的另外一個進程中運行的組件。本地服務器是由EXE實現的。
CLSCTX_REMOTE_SERVER
客戶希望創建一個在遠程機器上運行的組件。此標誌需要分佈式COM正常工作。
CoCreateInstance不夠靈活,COM提供了類廠用來創建組件,客戶可以通過類廠所支持的接口來對類廠創建組件的過程加以控制。
HRESULT __stdcall CoGetClassObject(
const CLSID& clsid,
DWORD dwClsContext,
COSERVERINFO *pServerInfo, // reserved for DCOM
const IID& iid,
void** ppv
);
此函數返回相應組件的類廠指針。
通常使用CoCreateInstance創建組件,但若想用不同於IClassFactory的某個創建接口如IClassFactory2(增加了權限或許可功能)來創建組件,則必須使用CoGetClassObject,一次創建多個實例時用它還可以提高效率。
CoGetClassObject調用DLL的DllGetClassObject來創建類廠。
COM庫實現了一個名爲CoFreeUnusedLibraries函數,以釋放那些不再需要的庫所佔用的內存空間。在程序的空閒期間,客戶應週期性地調用這個函數。
CoFreeUnusedLibraries將調用DllCanUnloadNow函數以詢問DLL是否可被卸載掉。在此函數中我們應該判斷COM對象的計數,但在進程外服務的情況下無法對類廠對象進行計數,所以需要使用類廠的LockServer方法。
第八章 包容和聚合
包容即外部組件包含指向內部組件接口的指針,外部組件使用內部組件的接口來實現它自己的接口,它可以直接轉發也可以加以改造。
聚合是包容的一個特例,它直接把內部組件的接口指針返回給客戶,但客戶並不知道另一個組件的存在。
聚合時在接口查詢函數中將直接返回內部組件的接口指針,但如此一來就有了兩個IUnknown接口,爲解決此問題,COM在創建對象時提供了pUnknownOuter參數指示內部對象使用外部對象的IUnknown接口。爲此,內部對象需要兩個IUnknown接口,一個爲代理IUnknown接口,將所有調用轉發給外部對象IUnknown接口,另一個爲非代理IUnknown接口,供它的直接客戶使用。
聚合後外部對象無法取得內部對象的非代理IUnknown接口,所以必須在創建時取得。
因爲聚合後對內部對象的計數操作被轉到外部對象,因此在外部對象中查詢內部對象的任何接口都將導致外部組件計數增加,爲還原計數,應該將傳給內部對象的接口釋放一次。在析構函數中,應該按相反的順序恢復計數,以免外部對象又成爲客戶的內部對象時等情況下出問題,同時爲了避免外部對象被再次釋放,需要先增加計數。
第九章 編程工作的簡化
可使用智能指針來簡化COM對象的使用。
對特定接口可使用C++包裝類來簡化。
第十章 EXE中的服務器
要實現進程外組件,需要使用LPC來調用進程外函數,實現IMarshal接口來調整參數,還需要代理DLL和存根DLL。
IDL
用MIDL編譯IDL文件可得到代理DLL和存根DLL。
import "unknwn.idl";
// Interface IX
[
object,
uuid(32bb8323-b41b-11cf-a6bb-0080c7b2d682),
helpstring("IX Interface"),
pointer_default(unique)
]
interface IX : IUnknown
{
HRESULT FxStringIn([in, string] wchar_t* szIn) ;
HRESULT FxStringOut([out, string] wchar_t** szOut) ;
} ;
import 表示導入,可以使用任意多次,而不會引起重複定義的問題。
IDL使用方括號作爲信息分隔符,在每一個接口定義的前面,都有一個屬性列表或稱接口頭,其中object表示所定義的接口爲一個COM接口,uuid爲相應接口的IID,第三個關鍵字用於將一個幫助串放到一個類型庫中,pointer_default告訴MIDL編譯器在沒有爲指針指定其它屬性時如何處理此指針,它有三個選項:
ref:將指針當成引用。此指針將總是指向一個合法的地址,並可被反引用。這種指針不能爲空,在函數內部不能修改,也不能指定別名。
unique:此類指針可以爲空也可以修改,但不能指定別名。
ptr:C指針。上述操作都可以。
接口聲明中的in,out表示輸入或輸出參數,以供編譯器優化。
string表示參數爲字符串,查找空字符決定其長度。
COM接口必須返回HRESULT類型,以標識隨時可能出現的網絡方面的錯誤。
傳遞數組時將使用size_is修飾符來指定數組大小。
IDL必須精確地知道每一個指針所指的內容,所以絕不能傳遞void指針,若要傳遞一個一般性的接口指針,可以使用IUnknown*接口,示例如下:
HRESULT GetIface([in]const IID& iid, [out, iid_is(iid)]IUnknown **ppI);
MIDL編譯後將生成4個文件,以FOO爲例:
FOO.H 一個同C和C++兼容的頭文件,可用/h改名。
FOO_I.C 定義GUID的C文件,可用/iid改名。
FOO_P.C 實現IDL文件中接口的代理和存根的C文件,可用/proxy改名。
DLLDATA.C實現包含代理及存根的DLL的C文件。可用/dlldata改名。
我們應該使用IDL文件來定義接口和IID,以免需要在多個地方維護。
使用宏REGISTER_PROXY_DLL編譯dlldata.c和proxy.c,再連接生成的三個文件,加上相應的DEF文件就可以生成代理DLL。
本地服務器的實現
進程外組件不需要DllCanUnloadNow,DllRegisterServer,DllUnregisterServer,只需要處理RegServer和UnRegServer,登記時文件位置改成LocalServer32而不是InprocServer32.。
爲了得到類廠,COM維護了一個關於被登記的類廠的內部表格,當客戶以適當的參數調用CoGetClassObject時,COM將首先檢查此表,找不到則查找註冊表並啓動相應的EXE。此EXE將完成相應類廠的登記,它可以調用CoRegisterClassObject來完成登記,它必須登記它支持的所有類廠。
CoRegisterClassObject的第一個參數爲被登記的類的CLSID,其後是一個指向其類廠的指針,第五個參數傳回一個句柄。第三個參數指定組件類型,第四個參數表示EXE的單個實例能否支持一個組件的多個實例。這兩個參數配合使用,只能提供單個組件的EXE服務器應使用CLSCTX_LOCAL_SERVER和REGCLS_SINGLEUSER,能提供多個實例的則用REGCLS_MULTI_SEPARATE,若組件本身要使用自己登記的組件,應該再加入CLSCTX_INPROC_SERVER或將後一個參數改成REGCLS_MULTIPLEUSE。
類廠的釋放使用註冊時返回的句柄調用CoRevokeClassObject即可。
EXE中的類廠對象不進行計數,這也是之前進程中組件對類廠單獨計數的原因,沒有活動組件對象時將由客戶調用的LockServer控制EXE的卸載。
服務器本身也是一個客戶,如果是客戶進程啓動的,它將帶有Embedding參數,否則應該增加鎖定計數,用戶關閉服務器時如果還有其它客戶在使用,它應該只關閉界面而不退出消息循環。
遠程訪問能力
運行DCOMCNFG.EXE可以將本地服務器配置成遠程服務器。配置程序在CLSID下增加了AppID,它的值也是一個GUID,HKEY_CLASSES_ROOT下的AppID下記錄了遠程服務信息。
此外,使用CoCreateInstanceEx也可以指定訪問某個遠程服務器,其中COSERVERINFO結構指定服務器信息,此外,通過它提供的MULTI_QI結構還可以一次查詢多個接口。
要決定DCOM是否可用,可以先檢查OLE32.DLL是否支持組件能夠供所有的線程訪問,再檢查註冊表項HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/ Ole/EnableDCOM的值是否爲’y’。
第十一章 調度接口與自動化
調度接口
COM使用虛函數表來實現接口,由編譯器利用頭文件解釋函數名和函數索引之間的關係。調度接口提供另一種方式將函數名和函數對應起來。它給每一個函數分配一個唯一的DISPID(一個長整數),GetIDsOfNames函數實現函數名到DISPID之間的轉換,Invoke函數根據DISPID調用對應的函數,實現方式有用DISPID作爲索引的函數名稱數組和函數指針數組,使用接口,使用雙重接口等。
HRESULT Invoke(
[in] DISPID dispIdMember,
[in] REFIID riid,
[in] LCID lcid,
[in] WORD wFlags,
[in, out] DISPPARAMS * pDispParams,
[out] VARIANT * pVarResult,
[out] EXCEPINFO * pExcepInfo,
[out] UINT * puArgErr
);
第一個參數是待調用函數的DISPID,第二個參數保留,必須爲IID_NULL,第三個參數是位置信息。
在VB之類的語言中,支持一種名爲屬性的概念,IDL中使用propget和propput來表示,如:
[propput]
HRESULT Visible([in] VARIANT_BOOL bVisible);
[propget]
HRESULT Visible([out, retval]VARIANT_BOOL *pbVisible);
編譯成C頭文件時會在函數名稱前加上get_或put_前綴。
Invoke的第四個函數就是用來支持屬性的,根據類型不同,可以對同一個DISPID表示的函數採用四種完全不同的操作:
DISPATCH_METHOD,
DISPATCH_PROPERTYGET,
DISPATCH_PROPERTYPUT,
DISPATCH_RPOPERTY_PUTREF。
第五個參數是傳給被調用函數的參數,參數使用VARIANT類型,它是一個許多不同類型值的聯合,因此,調度接口只能使用VARIANT類型可以表示的類型的參數。
第六個參數用來保存函數返回值或propget的結果。
第七個參數是一個指向EXCEPINFO結構的指針,用來填充意外情況信息。
typedef struct tagEXCEPINFO {
WORD wCode; // Error code
WORD wReserved;
BSTR bstrSource;
BSTR bstrDescription;
BSTR bstrHelpFile;
DWORD dwHelpContext;
ULONG pvReserved;
ULONG pfnDeferredFillIn; // Function fo fill in structure
SCODE scode; // Return value
}EXCEPINFO;
錯誤代碼和返回值中必須包含一個標識錯誤的值,而另一個必須爲0。當Invoke函數返回DISP_E_EXCEPTION時,若pfnDeferredFillIn非空,則用它填充意外信息結構,否則直接使用。
當Invoke返回DISP_E_PARAMNOTFOUND或DISP_E_TYPEMISMATCH時,出錯的參數的索引將保存在最後一個參數中。
好的接口應該自動處理各種類型之間的轉換,自動化提供了一個名爲VariantChangeType函數來完成這種轉換。
調度接口中的方法可能有一些可選的參數,若不想給這些參數提供一個值,則只需傳遞一個vt域被設置爲VT_ERROR而scode域被設置爲DISP_E_PARAMNOTFOUND的VARIANT結構即可。
BSTR是BASIC字符串或二進制字符串的縮寫,它實際上是指向一個寬字符串的指針,它在字符數組的開頭保存了字符計數,因而串中可以含有多個結束符。給BSTR變量賦值,可以使用SysAllocString,釋放時應該使用SysFreeString。
調度接口可以傳遞一種特殊的數據類型SAFEARRAY,它是一個包含有邊界信息的數組,其中fFeatures域表示的是保存在SAFEARRAY中的數據的類型,它的值可以爲:FADF_BSTR、FADF_UNKNOWN、FADF_DISPATCH、FADF_VARIANT,而下列值描述了數組的分配方式:FADF_AUTO、FADF_STATIC、FADF_EMBEDDED、FADF_FIXEDSIZE。
類型庫
類型庫是C++頭文件的替代物,以供其它語言使用,它還可以包含幫助串,使用對象瀏覽器可以獲取任意屬性或方法的幫助說明。
自動化庫函數CreateTypeLib可以創建一個類型庫,該函數將返回一個可以用相應的信息填充類型庫的ICreatetypeLib接口。但一般使用IDL自動生成。
使用IDL建立類型庫的關鍵是library語句,library塊中出現的所有內容都將被編譯到類型庫中。示例如下:
[
uuid(D3011EE1-B997-11CF-A6BB-0080C7B2D682),
version(1.0),
helpstring("Inside COM, Chapter 11 1.0 Type Library")
]
library ServerLib
{
importlib("stdole32.tlb") ;
// Component
[
uuid(0C092C2C-882C-11CF-A6BB-0080C7B2D682),
helpstring("Component Class")
]
coclass Component
{
[default] interface IX ;
} ;
} ;
Coclass語句將定義一個組件。這段語句中引用了IX接口,它也將被添加到類型庫中。
使用類型庫的第一步是裝載它,首先可以使用LoadRegTypeLib,或失敗則試一下LoadTypeLib或LoadTypeLibFromResource。LoadTypeLib將在裝載時爲之註冊,但若提供了一個完整的路徑名稱,那麼必須自己調用RegisterTypeLib完成註冊。裝載函數返回一個ITypeLib指針,它的GetTypeInfoOfGuid函數可以取得指定組件或接口的信息,得到一個ITypeInfo指針,使用這個指針可獲得所有相關信息。使用Object Browser或OleView都可以讀取類型庫中的信息,後者還可以建立與IDL相似的文件。
IDispatch接口的實現
常用的方法是將GetIDsOfNames及Invoke轉發給與我們的接口相應的ITypeInfo接口指針。
異常處理:P250
自動化接口使用OLEAUT32.DLL進行參數調整,不需要另外的代理DLL。
第十二章 多線程
COM中的線程分爲套間線程和自由線程。套間線程擁有組件和消息循環,COM負責同步對套間線程中組件的調用,外面的線程訪問套間線程的組件,需要調整參數。自由線程創建的組件由所有線程共享,沒有消息循環,需要組件處理同步。
同Windows窗口過程類似,套間線程的組件只能運行於一個線程中(SendMessage?),跨越套間邊界時必須對接口進行調整。DLL入口可能被多個不同線程同時訪問,所以必須是類型安全的,類廠若不是爲每一個組件都建立一人不同的類廠,則也需是類型安全的。當跨越套間邊界但又不通過COM通信時,必須手工調整接口指針。調整方法是使用CoMarshalInterface和CoUnMarshalInterface或CoMarshalInterThreadInterfaceInStream和CoGetInterfaceAndReleaseStream。
實現自由線程需要用COINIT_MULTITHREADED來調用CoInitializeEx。
傳給套間線程的接口需要進行調整,COM還會同步其對組件的調用,要優化掉這些功能,可使用CoCreateFreeThreadedMarshaler。
COM需要知道進程中組件支持的線程模型以便能在跨越線程邊界時對其接口進行合適的調整與同步。爲登記進程中組件的線程模型,可以給組件的InprocServer32關鍵字加上一個名爲ThreadingModel的項。一個進程中服務器提供的所有組件都應具有相同的線程模型。
第十三章 一個完整的例子