一般的通訊方式:
客戶 <==> COM(vbtl)接口 <==> COM組件
自動化通訊方式:
客戶(自動化控制器) <==> IDispatch::Invoke <==> 調度接口(或vbtl接口) <==> 實現IDispatch接口的COM組件 (自動化服務器)
自動化服務器 : COM組件
自動化控制器 :COM客戶
相關知識:IDispatch, 調度接口,雙重接口,類型庫,IDL, VARIANT, BSTR
調度接口(dispinterface) :IDispatch::Invoke的一個實現所能調用的函數集合,客戶只能通過IDispatch::Invoke使用組件
COM(vbtl)接口(custome) : 一個指針,指向一個函數指針數組,數組前三個元素是 QueryInterface,AddRef和Release
雙重接口(dual) :客戶既可以通過調度接口(IDispatch::Invoke),也可以直接通過COM接口(vbtl調用)使用組件
一般C++程序直接使用抽象接口調用COM組件,而編譯器會進行地址映射。例如:
pIX->Fx (msg) ;
實際會被編譯成這樣:
(*(pIX->vbtl [IndexOfFx]))(pIX, msg) ;
具體如下:
1. 獲取Fx在虛函數表中的索引 IndexOfFx = 4
2. 獲取Fx的函數地址 pAddressOfFx = pIX->vbtl [IndexOfFx]
3. 解引用,調用函數 (注意需要傳入this指針) (*pAddressOfFx)(pIX, msg)
上面三步合成就是 (*(pIX->vbtl [IndexOfFx]))(pIX, msg) ;了
但問題在於像VB, Javascript等沒有指針的概念,如何做到上面幾步,獲取vbtl中的函數指針呢?
可以編寫一個C++分析器處理 (相當於加入了一箇中間層)
中間層關鍵要處理三種信息 : 組件的ProgID, 函數名稱,參數
這個中間層通過IDispatch接口實現,其原型:
- IDispatch : public IUnknown
- {
- public:
- virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(
- /* [out] */ UINT *pctinfo) = 0;
- virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(
- /* [in] */ UINT iTInfo,
- /* [in] */ LCID lcid,
- /* [out] */ ITypeInfo **ppTInfo) = 0;
- virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(
- /* [in] */ REFIID riid,
- /* [size_is][in] */ LPOLESTR *rgszNames,
- /* [in] */ UINT cNames,
- /* [in] */ LCID lcid,
- /* [size_is][out] */ DISPID *rgDispId) = 0;
- virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke(
- /* [in] */ DISPID dispIdMember,
- /* [in] */ REFIID riid,
- /* [in] */ LCID lcid,
- /* [in] */ WORD wFlags,
- /* [out][in] */ DISPPARAMS *pDispParams,
- /* [out] */ VARIANT *pVarResult,
- /* [out] */ EXCEPINFO *pExcepInfo,
- /* [out] */ UINT *puArgErr) = 0;
- };
其中比較重要的有GetIDsOfNames 和 Invoke。
Invoke參數說明:
1. DISPID dispIdMember : 標誌客戶待調用的函數名,可由GetIDsOfNames獲得
2. REFIID riid : 必須爲 IID_NULL
3. LCID lcid : 用戶本地化信息,可用 GetUserDefaultLCID() 獲取
4. WORD wFlags : 一個函數名稱其實可以和四個函數關聯 (常規函數,設置屬性函數,通過引用設置屬性函數,獲取屬性函數),
它的值可以是DISPATCH_METHOD, DISPATCH_PROPERTYPUT, DISPATCH_PROPERTYPUTREF, DISPATCH_PROPERTYGET.
5. DISPPARAMS *pDispParams : 參數列表,其定義如下:
- typedef struct tagDISPPARAMS
- {
- /* [size_is] */ VARIANTARG *rgvarg; //與VARIANT相同. 所以自動控制程序能支持的類型有限
- /* [size_is] */ DISPID *rgdispidNamedArgs; //命名參數,C++中不用,VB支持
- UINT cArgs; //參數個數
- UINT cNamedArgs;
- } DISPPARAMS;
6. VARIANT *pVarResult :保存函數或propget的結果,沒有返回值時爲NULL
7. EXCEPINFO *pExcepInfo :保存例外情況的信息,可參考C++異常處理。當Invoke返回DISP_E_EXCEPTION,DISP_E_PARAMNOTFOUND等
時,可查詢pExcepInfo中相關信息。
VARIANT 其實是一個標準類型的大枚舉,定義大概如下:
- struct tagVARIANT
- {
- union
- {
- struct __tagVARIANT
- {
- VARTYPE vt;
- WORD wReserved1;
- WORD wReserved2;
- WORD wReserved3;
- union
- {
- LONGLONG llVal;
- LONG lVal;
- BYTE bVal;
- SHORT iVal;
- //......
- }
VARIANT 通過VariantInit初始化,VariantInit將vt設爲VT_EMPTY。
可通過VariantChangeType轉化VARIANT的類型
對調度接口中的可選參數,可設vt爲VT_ERROR,scode爲DISP_E_PARAMNOTFOUND。
VARIANT中比較特殊的BSTR和SAFEARRAY類型。
BSTR :它是(Basic String)或(binary string)的縮寫。定義如下:
- typedef wchar_t WCHAR;
- typedef WCHAR OLECHAR;
- typedef /* [wire_marshal] */ OLECHAR *BSTR;
但BSTR帶有字符計數值,這個值保存在字符數組開頭,所以BSTR字串中可以有多個'/0'。
所以下面方法錯誤:
- BSTR bStr = L"hello" ;
應該使用SysAllocString給BSTR賦值,使用SysFreeString釋放:
- BSTR bStr = SysAllocString (L"hello") ;
SAFEARRAY :包含邊界信息的數組
- typedef struct tagSAFEARRAY
- {
- USHORT cDims;
- USHORT fFeatures;
- ULONG cbElements;
- ULONG cLocks;
- PVOID pvData;
- SAFEARRAYBOUND rgsabound[ 1 ];
- } SAFEARRAY;
- typedef struct tagSAFEARRAYBOUND
- {
- ULONG cElements;
- LONG lLbound;
- } SAFEARRAYBOUND;
fFeatures 表示SAFEARRAY中數據的類型
自動化庫OLEAUT32.Dll中有一系列操作SAFEARRAY的函數,以SafeArray爲前綴
一個調度接口的可能實現:
一個雙重接口的可能實現
調度接口最好用雙重接口實現,這樣C++程序員可直接通過vbtl調用函數。
IDispatch::Invoke的兩個主要缺點:
1. 效率低,(進程內組件可能差幾個數量級,進程外甚至遠程組件就不明顯了)
2. 參數只能用標準參數
類型庫:
有了Invoke, VB或C++程序可以在不知道接口的任何類型信息下控制組件(當然程序員還是需要閱讀文檔知道接口的參數細節),
但這樣做需要運行時類型檢查和轉換,這樣開銷很大,並且可能隱藏錯誤。
所以COM提供類型庫,只是一種語言無關,適合解釋性語言的C++頭文件等價物。
類型庫提供組件,接口,方法,屬性,參數,接口等類型信息。
它是一個二進制文件,是IDL文件的一個編譯版本。
有了類型庫,VB也可以通過組件雙重接口的Vtbl部分訪問組件。
類型庫可由CreateTypeLib創建,他返回ICreatetypeLib接口,這種方式很少用。
類型庫可在IDL中聲明,通過MIDL編譯 (TLB後綴,也可包含在exe或dll中)
它包括一個GUID, 一個版本號和一個幫助字符串
coclass 定義一個組件
- library ServerLib
- {
- importlib("stdole32.tlb") ;
- // Component 1
- [
- uuid(0c092c29-882c-11cf-a6bb-0080c7b2d682),
- helpstring("Component 1 Class")
- ]
- coclass Component1
- {
- [default] interface IX ;
- interface IY ;
- interface IZ ;
- };
- }
類型庫的使用
1. 裝載
LoadRegTypeLib,從註冊表中裝載
LoadTypeLib, 從硬盤上裝載(裝載庫時會自動註冊,但若提供完整路徑名則不會註冊,需要調用RegisterTypeLib註冊)
LoadLibFromResource 從Exe/Dll中裝載
示例:
- HRESULT hr ;
- ITypeLib* pITypeLib = NULL ;
- hr = ::LoadRegTypeLib(LIBID_ServerLib, 1, 0, 0x00, &pITypeLib) ;
- if (FAILED(hr))
- {
- trace("LoadRegTypeLib Failed, now trying LoadTypeLib.", hr) ;
- // Get the fullname of the server's executable.
- char szModule[512] ;
- DWORD dwResult = ::GetModuleFileName(CFactory::s_hModule, szModule, 512) ;
- // Split the fullname to get the pathname.
- char szDrive[_MAX_DRIVE];
- char szDir[_MAX_DIR];
- _splitpath(szModule, szDrive, szDir, NULL, NULL) ;
- // Append name of registry.
- char szTypeLibName[] = "Server.tlb" ;
- char szTypeLibFullName[_MAX_PATH];
- sprintf(szTypeLibFullName, "%s%s%s", szDrive, szDir, szTypeLibName) ;
- // convert to wide char
- wchar_t wszTypeLibFullName[_MAX_PATH] ;
- mbstowcs(wszTypeLibFullName, szTypeLibFullName, _MAX_PATH) ;
- // if LoadTypeLib succeeds, it will have registered
- // the type library for us.
- // for the next time.
- hr = ::LoadTypeLib(wszTypeLibFullName, &pITypeLib) ;
- if(FAILED(hr))
- {
- trace("LoadTypeLib Failed.", hr) ;
- return hr;
- }
- // Ensure that the type library is registered.
- hr = RegisterTypeLib(pITypeLib, wszTypeLibFullName, NULL) ;
- if(FAILED(hr))
- {
- trace("RegisterTypeLib Failed.", hr) ;
- return hr ;
- }
- }
裝載完成後,得到一個ITypeLib接口指針,可以調用ITypeLib::GetTypeInfoOfGuid再獲取某組件或接口的信息,他返回一個ITypeInfo指針
ITypeInfo指針可以獲取組件,接口,方法,屬性,結構和其他類似的任何信息
不過一般C++組件程序員只將它用於實現IDispatch接口,實現IDispatch接口可以簡單的將GetIDsOfNames和Invoke轉發給對應的ITypeInfo指針
- // Get type information for the interface of the object.
- ITypeInfo *pITypeInfo = NULL;
- hr = pITypeLib->GetTypeInfoOfGuid(IID_IX, &pITypeInfo) ;
- pITypeLib->Release() ;
- if (FAILED(hr))
- {
- trace("GetTypeInfoOfGuid failed.", hr) ;
- return hr ;
- }
- HRESULT __stdcall CA::GetIDsOfNames( const IID& iid, OLECHAR** arrayNames, UINT countNames, LCID, DISPID* arrayDispIDs)
- {
- if (iid != IID_NULL)
- {
- return DISP_E_UNKNOWNINTERFACE ;
- }
- HRESULT hr = m_pITypeInfo->GetIDsOfNames(arrayNames, countNames, arrayDispIDs) ;
- return hr ;
- }
- HRESULT __stdcall CA::Invoke( DISPID dispidMember, const IID& iid, LCID, WORD wFlags,
- DISPPARAMS* pDispParams, VARIANT* pvarResult, EXCEPINFO* pExcepInfo, UINT* pArgErr)
- {
- if (iid != IID_NULL)
- {
- return DISP_E_UNKNOWNINTERFACE ;
- }
- ::SetErrorInfo(0, NULL) ;
- HRESULT hr = m_pITypeInfo->Invoke( static_cast<IDispatch*>(this), dispidMember, wFlags,
- pDispParams, pvarResult, pExcepInfo, pArgErr) ;
- return hr ;
- }
類型庫註冊
在註冊表的HKEY_CLASSED_ROOT/TypeLib下
異常的引發
給Invoke的EXCEPINFO結構參數填充信息
1. 組件實現ISupportErrorInfo接口
- class CA : public CUnknown,
- public IX,
- public ISupportErrorInfo
- {//......
- // ISupportErrorInfo
- virtual HRESULT __stdcall InterfaceSupportsErrorInfo(const IID& riid)
- {
- return (riid == IID_IX) ? S_OK : S_FALSE ;
- }
- }
2. IDispatch::Invoke實現中,調用ITypeInfo::Invoke前先調用
- SetErrorInfo (0, NULL);
3. 發生異常時,調用CreateErrorInfo獲取ICreateErrorInfo接口指針
使用ICreateErrorInfo接口指針填充錯誤信息
調用SetErrorInfo填充
- // Create the error info object.
- ICreateErrorInfo* pICreateErr ;
- HRESULT hr = ::CreateErrorInfo(&pICreateErr) ;
- if (FAILED(hr))
- {
- return E_FAIL ;
- }
- // pICreateErr->SetHelpFile(...) ;
- // pICreateErr->SetHelpContext(...) ;
- pICreateErr->SetSource(L"InsideCOM.Chap11") ;
- pICreateErr->SetDescription (L"This is a fake error generated by the component.") ;
- IErrorInfo* pIErrorInfo = NULL ;
- hr = pICreateErr->QueryInterface(IID_IErrorInfo, (void**)&pIErrorInfo) ;
- if (SUCCEEDED(hr))
- {
- ::SetErrorInfo(0L, pIErrorInfo) ;
- pIErrorInfo->Release() ;
- }
- pICreateErr->Release() ;
- return E_FAIL ;