客戶與組件通信的方法有很多種,而今天這裡將會詳細的總結自動化這種方法。自動化使得解釋性語言和宏語言訪問COM組件更為容易,同時用這些語言編寫組件也將更為容易。自動化關注的是運行時的類型檢查,這一點是以速度的犧牲和編譯時的類型檢查為代價的。
自動化不是獨立於COM的,而是建立在COM基礎之上的。一個自動化服務器實際上就是一個實現了IDispatch接口的COM組件。而一個自動化控制器則是一個通過IDispatch接口同自動化服務器進行通信的COM客戶。自動化控制器不會直接調用自動化服務器實現的那些函數,而是通過IDispatch接口中的成員函數實現對服務器中函數的間接調用。通過COM接口提供的任何服務都可以通過IDispatch接口來提供。
我們之前在客戶端調用COM組件時,都是通過查詢接口,而這依據的是虛函數表。而IDispatch將接收一個函數名稱並執行它。IDispatch接口的定義如下:
interface IDispatch : public IUnknown{ virtual HRESULT GetTypeInfoCount(unsigned int * pctinfo) = 0; virtual HRESULT GetTypeInfo(unsigned int iTInfo, LCID lcid, ITypeInfo ** ppTInfo ) = 0; virtual HRESULT GetIDsOfNames(REFIID riid, OLECHAR ** rgszNames, unsigned int cNames, LCID lcid, DISPID * rgDispId ) = 0; virtual HRESULT Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS * pDispParams, VARIANT * pVarResult, EXCEPINFO * pExcepInfo, unsigned int * puArgErr ) = 0;};
IDispatch中最令人感興趣的兩個函數是GetIDsOfNames和Invoke。GetIDsOfNames將讀取一個函數的名稱並返回其調度ID,也稱為DISPID。DISPID並不是一個GUID,而只是一個長整數,它標識的是一個函數。對於IDispatch的某一個特定的實現,DISPID是唯一的。
為執行某個函數,自動化控制程序將把DISPID傳給Invoke成員函數。Invoke可以將DISPID作為函數指針數組的索引。一個簡單的自動化服務器可以根據DISPID用一個case語句執行不同的代碼。
IDispatch::Invoke的一個實現所實現的函數集被稱作是一個調度接口。而COM接口是一個指向一個函數指針數組的指針,這個數組的前三個元素分別是QueryInterface、AddRef以及Release。IDispatch::Invoke的實現也是由一組函數構成的。下圖就是Invoke的一種可能的實現:
雙重接口是一種從IDispatch繼承的COM接口。此接口的成員函數可以通過Invoke和vtbl這兩種方式訪問。如下圖:
這種方法是讓實現IDispatch::Invoke的COM組件繼承IDispatch而不是IUnknown。雙重接口也是一種調度接口,它將使得通過Invoke能夠訪問的函數也能夠直接通過vtbl而訪問到。在實現調度接口時,更好的方法是使用雙重接口。C++程序員可以使用vtbl進行函數調用;宏語言和解釋性語言也可以使用實現雙重接口的組件提供的服務。
IDispatch的使用相對來說是比較簡單的,如以下代碼:
HRESULT hr = OleInitialize(NULL);// Get the CLSID for the applicationwhcar_t progid[] = L"InsideCOM.Chapl1"; CLSID clsid;::CLSIDFromProgID(progid, &clsid);// Create the componentIDispatch *pIDispatch = NULL;::CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IDispatch, (void **)&pIDispatch); DISPID dispid; OLECHAR *name = L"Fx"; pIDispatch->GetIDsOfNames( IID_NULL, // Must be IID_NULL &name, // Name of the function 1, // Number of names GetUserDefaultLCID(), // Localization info &dispid); // Dispatch ID// Prepare the arguments for Fx DISPPARAMS dispparamsNoArgs = { NULL, NULL, 0, // Zero arguments 0 // Zero named arguments};// Simplest Invoke call pIDispatch->Invoke( IID_NULL, // Must be IID_NULL dispid, // DISPID GetUserDefaultLCID(), DISPATCH_METHOD, // Method &dispparamsNoArgs, // Method arguments NULL, // Results NULL, // Exception NULL); // Arg error
使用規則如下:
上面的代碼中我們可以看到Invoke函數的參數是非常復雜的,我們非常有必要去總結一下這些參數的含義。前三個參數比較容易,第一個參數是待調用函數的DISPID;第二個參數是保留的,必須為IID_NULL。第三個參數保存的是位置信息。在說剩下的參數之前,先來說說在IDL中定義屬性。
說到屬性,會C#的人都知道,屬性有對應的get和set方法。我們都知道這樣對應的get和set都是編譯器去實現的,那麼在COM中是如何做到的呢?如果想讓自己的函數支持類似於get和set這種的方法,就應該在idl文件中像下面這種定義:
[ object, uuid(D03D9AF6-13BB-472B-89C4-D7016F490D57"), pointer_default(unique), dual ] interface IWindow : IDispatch { [propput] HRESULT Visible([in]VARIANT_BOOL bVisible); [propget] HRESULT Visible([out, retval]VARIANT_BOOL *pbVisible); }
就像上面定義了一個Visible屬性,當使用MIDL生成propput和propget函數的頭文件時,它將在函數名稱前加上get_或put_前綴。因此C++程序員可以使用如下的代碼來調用這些函數。
VARIANT_BOOL vb; get_Visible(&vb);if (vb == VARIANT_FALSE){ put_Visible(VARIANT_TRUE); }
現在再來說Invoke的第四個參數了,Invoke的第四個參數可以是以下值中的一個:
DISPATCH_METHOD // 常規函數
DISPATCH_PROPERTYGET // 獲取屬性的函數
DISPATCH_PROPERTYPUT // 設置屬性的函數
DISPATCH_PROPERTYPUTREF // 通過引用設置屬性的函數
我們可以看到,在上面的idl中,生成的get_Visible和put_Visible都具有相同的DISPID,在進行Invoke時,就是使用這裡定義的標識進行區分調用的。現在懂了吧。
Invoke的第五個參數包含的是傳給被調用函數的參數。這個參數是一個DISPPARAMS結構,它的定義如下:
typedef struct tagDISPPARAMS { VARIANTARG *rgvarg; // Array of arguments DISPID *rgdispidNamedArgs; // DISPIDS of named arguments unsigned int cArgs; // Number of arguments unsigned int cNamedArgs; // Number of named args}DISPPARAMS;
我們看到了命名參數,命名參數是通過提供參數的名稱而使程序員能夠以任意的次序將參數傳遞給某個函數。這個概念對於C++程序員來說是沒有任何用處的。所以,在這裡rgdispidNamedArgs將永遠為NULL,而cNamedArgs將永遠為0。rgvarg為一個參數數組,cArgs成員則是數組中元素的個數,每一個參數都是VARIANTARG,VARIANTARG和VARIANT是相同的,關於VARIANTARG,在我的下一篇博文會總結這個的。只有那些能夠被放到VARIANTARG結構中的類型才可以通過調度接口進行傳遞。
Invoke的第六個參數pVarResult為指向一個VARIANT結構的指針,此結構將被用於保存Invoke所執行的函數或propget的結果。對於沒有返回值的成員函數或propput以及propputref,此參數值可以為NULL。
Invoke的倒數第二個參數為指向一個EXCEPINFO結構的指針。若Invoke執行的函數或屬性遇到一個異常情況,則此結構體將被填入關於此異常的信息。EXCEPINFO的定義如下:
typedef struct FARSTRUCT tagEXCEPINFO { unsigned short wCode; // Error code. Unsigned short wReserved; // Reserved. BSTR bstrSource; // Exception source. BSTR bstrDescription; // Exception description. BSTR bstrHelpFile; // Help file path. Unsigned long dwHelpContext; // Help context ID. Void FAR* pvReserved; // Reserved. HRESULT (STDAPICALLTYPE FAR* pfnDeferredFillIn) // Pointer to a (struct tagEXCEPINFO FAR*); // function that // fills in help and // description info. SCODE scode; // Return value. } EXCEPINFO, FAR* LPEXCEPINFO;
在wCode或scode中必須包含一個標識錯誤的值,而另外一個則必須被設置為0。下面是一段示例代碼:
EXCEPINFO excepinfo; HRESULT hr = pIDispatch->Invoke(..., &excepinfo, NULL);if (FAILED(hr)){ if (hr == DISP_E_EXCEPTION) { if (excepinfo.pfnDeferredFillIn != NULL) { (*(excepinfo.pfnDeferredFillIn))(&excepinfo); } } /* * Now you can print the exception information */}
若Invoke的返回值為DISP_E_PARAMNOTFOUND或DISP_E_TYPEMISMATCH,此時Invoke將把與此錯誤相應的參數的索引保存在最後一個參數puArgErr中。這就是最後一個參數的作用。
到此,關於Invoke的所有講解都完畢了。
我們在使用自動化接口去調用COM組件時,那些解釋性或宏語言並不知道接口的信息,那麼它是如何完成調用的呢?這是因為有運行時類型檢查與轉換機制的存在。但是進行運行時類型檢查與轉換的開銷是非常大的,並且可能導致一些難以發現的錯誤。因此,我們需要一種與語言無關的、適合於解釋性語言和宏編程語言使用的C++頭文件的等價物。COM解決這一問題的方案是類型庫。類型庫將提供有關組件、接口、方法、屬性、參數及結構的類型信息。類型庫和頭文件是等價的,它實際上是IDL文件的一個編譯版本,並且可以使用編程的方法來訪問。
在開發中,我們都是使用的IDL建立類型庫。使用IDL建立類型庫的關鍵是library語句。library塊中出現的所有內容——library關鍵字之後的方括號中的內容——都將被編譯到類型庫中。一個類型庫包含一個GUID、一個版本號和一個幫助串。
在生成了類型庫之後,既可以將其作為一個單獨的文件發行,也可以將其作為一個資源包含在EXE或DLL中。我們一般都是及那個其同DLL一起發布的,以便在安裝的過程中得以簡化。對於單獨的一個類型庫文件,我們可以使用regtlib <.tlb文件的完整路徑>來進行注冊tlb文件。
在實際編程時,比如使用C#其編寫了一個COM組件,編譯時會生成一個tlb文件;我們在C++測調用此COM組件時,需要使用#import “xxx.tlb” no_namespace來倒入類型庫;編譯時,就會生成tlh和tli兩個包含COM接口信息的文件。如果DLL中直接包含了tlb文件,那麼我們就可以直接#import “xxx.dll”,也會生成tlh和tli兩個文件。
如果組件只被編譯型語言如C或C++訪問,則可以使用vtbl或常規的COM接口。vtbl接口比調度接口要快的多,並且使用起來也非常方便。若組件將被VB或Java訪問,則應該實現一個雙重接口。