COM學習筆記8_IDispatch (調度接口) 自動化

一般的通訊方式:
客戶 <==> 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接口實現,其原型:

  1. IDispatch : public IUnknown  
  2. {  
  3. public:  
  4.     virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(   
  5.         /* [out] */ UINT *pctinfo) = 0;  
  6.       
  7.     virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(   
  8.         /* [in] */ UINT iTInfo,  
  9.         /* [in] */ LCID lcid,  
  10.         /* [out] */ ITypeInfo **ppTInfo) = 0;  
  11.       
  12.     virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(   
  13.         /* [in] */ REFIID riid,  
  14.         /* [size_is][in] */ LPOLESTR *rgszNames,  
  15.         /* [in] */ UINT cNames,  
  16.         /* [in] */ LCID lcid,  
  17.         /* [size_is][out] */ DISPID *rgDispId) = 0;  
  18.       
  19.     virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke(   
  20.         /* [in] */ DISPID dispIdMember,  
  21.         /* [in] */ REFIID riid,  
  22.         /* [in] */ LCID lcid,  
  23.         /* [in] */ WORD wFlags,  
  24.         /* [out][in] */ DISPPARAMS *pDispParams,  
  25.         /* [out] */ VARIANT *pVarResult,  
  26.         /* [out] */ EXCEPINFO *pExcepInfo,  
  27.         /* [out] */ UINT *puArgErr) = 0;      
  28. };  

 

其中比較重要的有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 : 參數列表,其定義如下:

  1. typedef struct tagDISPPARAMS  
  2.     {  
  3.     /* [size_is] */ VARIANTARG *rgvarg; //與VARIANT相同. 所以自動控制程序能支持的類型有限  
  4.     /* [size_is] */ DISPID *rgdispidNamedArgs; //命名參數,C++中不用,VB支持  
  5.     UINT cArgs; //參數個數  
  6.     UINT cNamedArgs;  
  7.     }   DISPPARAMS;  

6. VARIANT *pVarResult :保存函數或propget的結果,沒有返回值時爲NULL
7. EXCEPINFO *pExcepInfo :保存例外情況的信息,可參考C++異常處理。當Invoke返回DISP_E_EXCEPTION,DISP_E_PARAMNOTFOUND等
                           時,可查詢pExcepInfo中相關信息。


VARIANT 其實是一個標準類型的大枚舉,定義大概如下:

  1. struct tagVARIANT  
  2.     {  
  3.     union   
  4.         {  
  5.         struct __tagVARIANT  
  6.             {  
  7.             VARTYPE vt;  
  8.             WORD wReserved1;  
  9.             WORD wReserved2;  
  10.             WORD wReserved3;  
  11.             union   
  12.                 {  
  13.                 LONGLONG llVal;  
  14.                 LONG lVal;  
  15.                 BYTE bVal;  
  16.                 SHORT iVal;  
  17.                 //......  
  18.     }  

 

VARIANT 通過VariantInit初始化,VariantInit將vt設爲VT_EMPTY。
可通過VariantChangeType轉化VARIANT的類型
對調度接口中的可選參數,可設vt爲VT_ERROR,scode爲DISP_E_PARAMNOTFOUND。

VARIANT中比較特殊的BSTR和SAFEARRAY類型。
BSTR :它是(Basic String)或(binary string)的縮寫。定義如下:

  1. typedef wchar_t WCHAR;  
  2. typedef WCHAR OLECHAR;  
  3. typedef /* [wire_marshal] */ OLECHAR *BSTR;  

但BSTR帶有字符計數值,這個值保存在字符數組開頭,所以BSTR字串中可以有多個'/0'。
所以下面方法錯誤:

[c-sharp] view plaincopy
  1. BSTR bStr = L"hello" ;  

應該使用SysAllocString給BSTR賦值,使用SysFreeString釋放:
[c-sharp] view plaincopy
  1. BSTR bStr = SysAllocString (L"hello") ;  


SAFEARRAY :包含邊界信息的數組

  1. typedef struct tagSAFEARRAY  
  2.     {  
  3.     USHORT cDims;  
  4.     USHORT fFeatures;  
  5.     ULONG cbElements;  
  6.     ULONG cLocks;  
  7.     PVOID pvData;  
  8.     SAFEARRAYBOUND rgsabound[ 1 ];  
  9.     }   SAFEARRAY;  
  10. typedef struct tagSAFEARRAYBOUND  
  11.     {  
  12.     ULONG cElements;  
  13.     LONG lLbound;  
  14.     }   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 定義一個組件

  1. library ServerLib  
  2. {  
  3.     importlib("stdole32.tlb") ;  
  4.     // Component 1  
  5.     [  
  6.         uuid(0c092c29-882c-11cf-a6bb-0080c7b2d682),  
  7.         helpstring("Component 1 Class")  
  8.     ]  
  9.     coclass Component1  
  10.     {  
  11.         [default] interface IX ;  
  12.         interface IY ;  
  13.         interface IZ ;  
  14.     };  
  15. }  

 

類型庫的使用
1. 裝載
LoadRegTypeLib,從註冊表中裝載
LoadTypeLib, 從硬盤上裝載(裝載庫時會自動註冊,但若提供完整路徑名則不會註冊,需要調用RegisterTypeLib註冊)
LoadLibFromResource 從Exe/Dll中裝載

示例:

  1. HRESULT hr ;  
  2. ITypeLib* pITypeLib = NULL ;  
  3. hr = ::LoadRegTypeLib(LIBID_ServerLib, 1, 0, 0x00, &pITypeLib) ;  
  4. if (FAILED(hr))   
  5. {  
  6.     trace("LoadRegTypeLib Failed, now trying LoadTypeLib.", hr) ;  
  7.     // Get the fullname of the server's executable.  
  8.     char szModule[512] ;  
  9.     DWORD dwResult = ::GetModuleFileName(CFactory::s_hModule, szModule, 512) ;   
  10.     // Split the fullname to get the pathname.  
  11.     char szDrive[_MAX_DRIVE];  
  12.     char szDir[_MAX_DIR];  
  13.     _splitpath(szModule, szDrive, szDir, NULL, NULL) ;  
  14.     // Append name of registry.  
  15.     char szTypeLibName[] = "Server.tlb" ;  
  16.     char szTypeLibFullName[_MAX_PATH];  
  17.     sprintf(szTypeLibFullName, "%s%s%s", szDrive, szDir, szTypeLibName) ;  
  18.     // convert to wide char  
  19.     wchar_t wszTypeLibFullName[_MAX_PATH] ;  
  20.     mbstowcs(wszTypeLibFullName, szTypeLibFullName, _MAX_PATH) ;  
  21.     // if LoadTypeLib succeeds, it will have registered  
  22.     // the type library for us.  
  23.     // for the next time.    
  24.     hr = ::LoadTypeLib(wszTypeLibFullName, &pITypeLib) ;  
  25.     if(FAILED(hr))          
  26.     {  
  27.         trace("LoadTypeLib Failed.", hr) ;  
  28.         return hr;     
  29.     }  
  30.     // Ensure that the type library is registered.  
  31.     hr = RegisterTypeLib(pITypeLib, wszTypeLibFullName, NULL) ;  
  32.     if(FAILED(hr))          
  33.     {  
  34.         trace("RegisterTypeLib Failed.", hr) ;  
  35.         return hr ;     
  36.     }  
  37. }  

 

裝載完成後,得到一個ITypeLib接口指針,可以調用ITypeLib::GetTypeInfoOfGuid再獲取某組件或接口的信息,他返回一個ITypeInfo指針
ITypeInfo指針可以獲取組件,接口,方法,屬性,結構和其他類似的任何信息
不過一般C++組件程序員只將它用於實現IDispatch接口,實現IDispatch接口可以簡單的將GetIDsOfNames和Invoke轉發給對應的ITypeInfo指針

[c-sharp] view plaincopy
  1. // Get type information for the interface of the object.  
  2. ITypeInfo *pITypeInfo = NULL;  
  3. hr = pITypeLib->GetTypeInfoOfGuid(IID_IX, &pITypeInfo) ;  
  4. pITypeLib->Release() ;  
  5. if (FAILED(hr))    
  6. {   
  7.     trace("GetTypeInfoOfGuid failed.", hr) ;  
  8.     return hr ;  
  9. }     
  10. HRESULT __stdcall CA::GetIDsOfNames( const IID& iid, OLECHAR** arrayNames, UINT countNames, LCID, DISPID* arrayDispIDs)  
  11. {  
  12.     if (iid != IID_NULL)  
  13.     {  
  14.         return DISP_E_UNKNOWNINTERFACE ;  
  15.     }  
  16.     HRESULT hr = m_pITypeInfo->GetIDsOfNames(arrayNames, countNames, arrayDispIDs) ;  
  17.     return hr ;  
  18. }  
  19. HRESULT __stdcall CA::Invoke( DISPID dispidMember, const IID& iid, LCID, WORD wFlags,  
  20.       DISPPARAMS* pDispParams, VARIANT* pvarResult, EXCEPINFO* pExcepInfo, UINT* pArgErr)  
  21. {          
  22.     if (iid != IID_NULL)  
  23.     {  
  24.         return DISP_E_UNKNOWNINTERFACE ;  
  25.     }  
  26.     ::SetErrorInfo(0, NULL) ;  
  27.     HRESULT hr = m_pITypeInfo->Invoke( static_cast<IDispatch*>(this), dispidMember, wFlags,   
  28.         pDispParams, pvarResult, pExcepInfo, pArgErr) ;   
  29.     return hr ;  
  30. }  

 

類型庫註冊
在註冊表的HKEY_CLASSED_ROOT/TypeLib下

異常的引發
給Invoke的EXCEPINFO結構參數填充信息
1. 組件實現ISupportErrorInfo接口

  1. class CA : public CUnknown,  
  2.            public IX,  
  3.            public ISupportErrorInfo  
  4. {//......  
  5.     // ISupportErrorInfo  
  6.     virtual HRESULT __stdcall InterfaceSupportsErrorInfo(const IID& riid)  
  7.     {  
  8.         return (riid == IID_IX) ? S_OK : S_FALSE ;  
  9.     }  
  10. }  

 

2. IDispatch::Invoke實現中,調用ITypeInfo::Invoke前先調用

  1. SetErrorInfo (0, NULL);  

 

3. 發生異常時,調用CreateErrorInfo獲取ICreateErrorInfo接口指針
   使用ICreateErrorInfo接口指針填充錯誤信息
   調用SetErrorInfo填充

  1. // Create the error info object.  
  2. ICreateErrorInfo* pICreateErr ;  
  3. HRESULT hr = ::CreateErrorInfo(&pICreateErr) ;  
  4. if (FAILED(hr))  
  5. {  
  6.     return E_FAIL ;  
  7. }  
  8. // pICreateErr->SetHelpFile(...) ;  
  9. // pICreateErr->SetHelpContext(...) ;  
  10. pICreateErr->SetSource(L"InsideCOM.Chap11") ;  
  11. pICreateErr->SetDescription (L"This is a fake error generated by the component.") ;  
  12. IErrorInfo* pIErrorInfo = NULL ;  
  13. hr = pICreateErr->QueryInterface(IID_IErrorInfo, (void**)&pIErrorInfo) ;  
  14. if (SUCCEEDED(hr))  
  15. {  
  16.     ::SetErrorInfo(0L, pIErrorInfo) ;  
  17.     pIErrorInfo->Release() ;  
  18. }  
  19. pICreateErr->Release() ;  
  20. return E_FAIL ;  

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