變體(Variant)與Dispatch調用(IDispatch)
許式偉 (著作權聲明)
2007-3-14
在上一篇,我們解釋了COM技術基礎:COM組件(Component)與介面(Interface)。這裡我們聊聊COM技術中的經常會遇到的兩個概念:變體(Variant)和IDispatch介面。
變體(Variant)
Variant類型理論上可以存放任何類型的資料,這也是中文很多人稱之為“變體”的原因。對於C++這種強型別語言的程式員來說,存在變體(Variant)這樣的類型是奇怪的。但是對於哪些淡化類型概念的語言(如Visual Basic等)來說,Variant是它們預設的類型。在VB中,如果沒有用As語句聲明變數,那麼這個變數就是Variant類型的。對於C++程式員來說,Variant不過是一個超複雜的結構體:
typedef /* [wire_marshal] */ struct tagVARIANT VARIANT;
struct tagVARIANT
{
union
{
struct __tagVARIANT
{
VARTYPE vt;
WORD wReserved1;
WORD wReserved2;
WORD wReserved3;
union
{
LONGLONG llVal;
LONG lVal;
BYTE bVal;
SHORT iVal;
FLOAT fltVal;
DOUBLE dblVal;
VARIANT_BOOL boolVal;
_VARIANT_BOOL bool;
SCODE scode;
CY cyVal;
DATE date;
BSTR bstrVal;
IUnknown *punkVal;
IDispatch *pdispVal;
SAFEARRAY *parray;
BYTE *pbVal;
SHORT *piVal;
LONG *plVal;
LONGLONG *pllVal;
FLOAT *pfltVal;
DOUBLE *pdblVal;
VARIANT_BOOL *pboolVal;
_VARIANT_BOOL *pbool;
SCODE *pscode;
CY *pcyVal;
DATE *pdate;
BSTR *pbstrVal;
IUnknown **ppunkVal;
IDispatch **ppdispVal;
SAFEARRAY **pparray;
VARIANT *pvarVal;
PVOID byref;
CHAR cVal;
USHORT uiVal;
ULONG ulVal;
ULONGLONG ullVal;
INT intVal;
UINT uintVal;
DECIMAL *pdecVal;
CHAR *pcVal;
USHORT *puiVal;
ULONG *pulVal;
ULONGLONG *pullVal;
INT *pintVal;
UINT *puintVal;
struct __tagBRECORD
{
PVOID pvRecord;
IRecordInfo *pRecInfo;
} __VARIANT_NAME_4;
} __VARIANT_NAME_3;
} __VARIANT_NAME_2;
DECIMAL decVal;
} __VARIANT_NAME_1;
} ;
Variant類型在解釋型語言和指令碼語言中應用甚廣。在Visual Basic,JavaScript等身上,處處可見其身影。但是如果沒有語言本省的支援,對Variant操作是複雜的。不幸的是,C/C++就是屬於這種情況。這應該說與C++對新技術的謹慎,以及是一種非純商業公司控制的語言有關。其他語言如Delphi,一定要與時俱進,是一定要加Variant的內建支援的。
IDispatch與雙介面
在我看來, Dispatch調用(IDispatch)的存在主要是指令碼語言的需要。指令碼語言多數屬於解釋型語言,其代碼並不產生機器指令,而是邊解釋邊執行(或者翻譯成為中間代碼後解釋執行),這種語言通常有這樣一個需求:就是要在不知道類(或者說組件)的詳細規格情況下調用類的方法。
Dispatch調用(IDispatch)就是滿足這種需求的一個技術規格。它用一個dispid或者名字(字串)表示要調用的方法(或者屬性),其原理和Windows視窗的訊息機制挺類似(你可以把視窗訊息中的uMsg參數和這裡的dispid對應起來)。IDispatch的實現者對dispid進行指派,完成具體的功能調用。有些指令碼語言也許未必採用 IDispatch 作為它的調用標準,但是通常是一種和 IDispatch 類似的東西。
這種在不知道類(或者說組件)的詳細規格情況下調用類的方法,我們稱之為“晚綁定”。這是相對於C++這類編譯型語言中基於虛函數機制的調用機制而言的,後者我們成為“早綁定”。對於虛函數機制,它要求組件的介面是已知的,如果你不知道組件的介面,也就不知道又哪些方法可用,更談不上如何去調用。
IDispatch的定義如下:
interface IDispatch : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(
/* [out] */ UINT __RPC_FAR *pctinfo) = 0;
virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(
/* [in] */ UINT iTInfo,
/* [in] */ LCID lcid,
/* [out] */ ITypeInfo __RPC_FAR *__RPC_FAR *ppTInfo) = 0;
virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(
/* [in] */ REFIID riid,
/* [size_is][in] */ LPOLESTR __RPC_FAR *rgszNames,
/* [in] */ UINT cNames,
/* [in] */ LCID lcid,
/* [size_is][out] */ DISPID __RPC_FAR *rgDispId) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke(
/* [in] */ DISPID dispIdMember,
/* [in] */ REFIID riid,
/* [in] */ LCID lcid,
/* [in] */ WORD wFlags,
/* [out][in] */ DISPPARAMS __RPC_FAR *pDispParams,
/* [out] */ VARIANT __RPC_FAR *pVarResult,
/* [out] */ EXCEPINFO __RPC_FAR *pExcepInfo,
/* [out] */ UINT __RPC_FAR *puArgErr) = 0;
};
最後一個問題是,什麼是“雙介面”? 一個誤區是,也許有人會把“雙介面”和從IDispatch繼承的介面等同起來。不過,這種理解有點片面了。
所謂“雙介面”,是指哪些既實現了IDispatch介面,又實現了基於虛表調用的普通介面的特殊介面。雙介面的好處在於它既適應了C++這種支援虛表(vtbl)、追求高效的語言,也支援了指令碼語言。在idl文法中,雙介面以dual關鍵字表示:
[dual]
interface IFoo : IDispatch
{
HRESULT foo(int arg1, int arg2);
};
這裡 IFoo 是一個雙介面。一個雙介面一定是 IDispatch 介面,但是反之則不一定。