1. 一種新的通訊方式
IDispatch為客戶和組件提供了另外一種通訊方式,有了IDispatch後,COM組件可以通過一個標準的介面提供它所支援的服務,而無需提供多個特定與服務的介面。
1.1舊的通訊方式
客戶和組件之間的通訊是通過介面完成的,介面擁有一個有函數指標構成的數組。客戶代碼需要包含一個以抽象基類形式描述介面的標頭檔。編譯器將讀取此標頭檔,然後為抽象基類中的每一個成員函數分配一個索引。此索引實際上就是相應函數的指標在函數指標數組中的索引。
pIX->FxStringOut(msg);
將解釋成:
(*(pIX->pvtbl[indexOfFxStringOut]))(pIX,msg);
其中pvtbl為指向相應類的vtbl的指標,而IndexOfFxStringOut則為FxStringOut的指標在函數指標表中的索引。
在開發宏語言是,宏語言如可能擷取到函數在vtbl中的指標的索引以便能夠調用它們呢?宏語言調用COM組件中的一個函數時,可以使用三種類型的資訊:實現被調用函數的組件ProID、函數名稱以及傳給函數的參數。我們需要在宏運行時系統能夠提供一種通過函數的名稱老執行函數的簡單方法。這種方法就是IDispatch介面。
1.2IDispatch介面
簡單來說,IDispatch將接收一個函數的名稱並執行它。IDispatch的IDL描述
interfaceIDispatch : IUnknown
{
typedef [unique] IDispatch * LPDISPATCH;
HRESULT GetTypeInfoCount(
[out] UINT * pctinfo
);
HRESULT GetTypeInfo(
[in] UINT iTInfo,
[in] LCID lcid,
[out] ITypeInfo ** ppTInfo
);
HRESULT GetIDsOfNames(
[in] REFIID riid,
[in, size_is(cNames)] LPOLESTR* rgszNames,
[in] UINT cNames,
[in] LCID lcid,
[out, size_is(cNames)] DISPID *rgDispId
);
[local]
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
);
[call_as(Invoke)]
HRESULT RemoteInvoke(
[in] DISPID dispIdMember,
[in] REFIID riid,
[in] LCID lcid,
[in] DWORD dwFlags,
[in] DISPPARAMS * pDispParams,
[out] VARIANT * pVarResult,
[out] EXCEPINFO * pExcepInfo,
[out] UINT * pArgErr,
[in] UINT cVarRef,
[in, size_is(cVarRef)] UINT *rgVarRefIdx,
[in, out, size_is(cVarRef)]VARIANTARG * rgVarRef
);
IDispatch最令人感興趣的是GetIDsOfNames和Invoke.GetIDsOfNames將讀取一個函數的名稱並返回其調度ID(DISPID)。DISPID是一個長整數,它標識一個函數。自動化控製程序將把DISPID傳給Invoke成員函數。Invoke可以將DISPID作為函數指標數組的索引。
IDispatch::Invoke的實現與vtbl還有另外一方面是相似的:他們都可以定義介面。IDispatch::Invoke的一個實現所實現是我函數集被稱為一個調度介面。而COM介面是一個指向一個函數指標數組的指標,此數組的前三個元素分別是:Queryinterface、AddRef和Release。如下給出調度介面的一個圖示:
左邊是一個傳統的COM介面IDispatch,他是以vtbl的方式實現,右邊是一個調度介面,Invoke能識別的DISPID對於調度介面至關重要。給出的時一種可能實現,GetIDsOfNames通過函數名稱擷取其DISPID,Invoke通過DISPID來找到相應的函數指標,來執行函數。
同時也可以用一個COM介面實現IDispatch::Invoke:
不是用COM組件實現調度介面的唯一方法。也可以使用如下的方法。讓實現IDispatch::Invoke的COM組件繼承IDispatch而不是IUnknown。這樣是稱作雙重介面的方法,通過Invoke能夠訪問的函數也能夠直接通過vtbl訪問到。
2.IDispatch的使用
考慮下面的VB程式:
Dim Cmpnt As Object
Set Cmpnt = CreateObject("InsideCom.Cmpnt1")
Cmpnt.Fx
通過COM組件實現的IDispatch介面調用Fx,下面看看C++如何?這個工作。
HRESULT hr = OleInitialize(NULL);
wchar_tprogid[] = L"InsideCom.Cmpnt1";
CLSID clsid;
CLSIDFromProgID(progid, &clsid);
IDispatch *pDispach = NULL;
CoCreateInstance(clsid,
NULL,CLSCTX_INPROC_SERVER,
IID_IDispatch,(void**)&pDispach);
DISPID dispid;
OLECHAR* name = L"Fx";
pDispach->GetIDsOfNames(
IID_NULL,&name,
1,GetUserDefaultLCID(),&dispid);
DISPPARAMS dispparamsNoArgs = {
NULL,NULL,0,0
}
pDispach->Invoke(dispid,
IID_NULL,
GetUserDefaultLCID(),
DISPATCH_METHOD,
&dispparamsNoArgs,
NULL,NULL, NULL);
通過CoCreateInstance返回一個IDispatch指標,通過IDispatch::GetIDsOfNames可以將函數名稱轉換成一個DISPID在通過IDispatch::Invoke調用相應的函數。
2.1Invoke函數的參數
第一個參數是控製程序待調用函數的DISPID,第二個是保留的必須為IID_NULL。第三個是儲存的位置資訊。其他的下面討論。
COM介面中所有成員都是函數,使用“Set”和“Get”類函數來類比歲成員變數的訪問。例如SetVisible可以設定一個視窗的可見度。則GetVisible可以擷取這一屬性。
If(pIWindows->GetVisible() == FALSE)
pIWindows->SetVisible(TRUE);
對於VB用Set和Get就不太理想,在vb中如下調用
If Window.Visible = False Then
Window.Visible= True
End If
IDL中的propget核propput屬性可以將COM函數說明成將其當成一個屬性對待。
IDispatch::Invoke的第五個參數包含的時傳給被調用函數的參數。結構如下
typedef
struct tagDISPPARAMS
{
VARIANTARG *rgvarg;
DISPID *rgdispidNamedArgs;
UINT cArgs;
UINT cNamedArgs;
} DISPPARAMS;
結構的第一個參數是一個參數數組,cArgs成員則是數組中元素的個數。每一個參數類型都是VARIANTARG類型。VARIANT是許多不同類型的一個大聯合。
第六個參數pVarResult指向一個VARIANT結構的指標。儲存Invoke所執行函數或propget的結果。
IDispatch::Invoke的倒數第二個參數為指向EXCEPINFO結果指標。若Invoke執行函數或屬性遇到一個異常情況,則此結構被填入關於此異常情況的一些資訊。
typedef
struct tagEXCEPINFO {
WORD wCode;
WORD wReserved;
BSTR bstrSource;
BSTR bstrDescription;
BSTR bstrHelpFile;
DWORD dwHelpContext;
PVOID pvReserved;
HRESULT (__stdcall *pfnDeferredFillIn)(struct tagEXCEPINFO *);
SCODE scode;
} EXCEPINFO, * LPEXCEPINFO;
在錯誤碼(wCode)或傳回值(scode)中必須包含一個標識的值。而另外一個為0;
EXCEPINFO excepinfo;
HRESULT hr = pDispach->Invoke(...,&excepinfo);
if(FAILED(hr))
{
if(hr == DISP_E_EXCEPTION)
{
if(excepinfo.pfnDeferredFillIn( != NULL))
(*(excepinfo.pfnDeferredFillIn))(&excepinfo);
cout<<"Exception information: "<<endl
<<"Source: "<<excepinfo.bstrSource<<endl
<<"Description: "<<excepinfo.bstrDescription<<ends;
}
}
3.類型庫
類型庫將提供有關組件、介面、方法、屬性、參數及結構的類型資訊。類型庫是一個二進位檔案。
3.1類型庫的建立
自動化庫函數CreateTypeLib可以建立一個類型庫。該函數將返回一個可以用相應的資訊填滿類型庫的ICreatetypeLib介面,可以使用IDL組建代理程式/存根DLL代碼。
使用IDL建立類型庫的關鍵是library語句,library關鍵字之後的方括弧的內容都將邊緣到類型庫中。
[
object,
uuid(7AAAE05F-283E-4D79-B98C-5D3DCBE733BB),
//IID_IPoint
helpstring("IPointinterface"),
oleautomation,
dual
]
interface IPoint : IDispatch
{
[propget,
helpstring("Returns and sets X coordinate")]
HRESULTx([out,retval]int *retval);
[propput,
helpstring("Returns and sets X coordinate")]
HRESULTx([in]intValue);
[propget,
helpstring("Returns and sets Y coordinate")]
HRESULTy([out,retval]int *retval);
[propput,
helpstring("Returns and sets Y coordinate")]
HRESULTy([in]intValue);
[helpstring("Displaythe Point.")]
HRESULTdisplay();
}
[
uuid(13BA7D38-8F44-4E80-9DAE-6E01776BEE16),//LIBID_Point
helpstring("PointComponent Type Library"),
version(1.0)
]
library Point
{
importlib("stdole32.tlb");
interface IPoint;
[
uuid(0615EA9A-0ED9-458D-9F0D-A9E3645338AB),//CLSID_Point
helpstring("PointClass"),
appobject
]
coclass Point
{
[default]
dispinterfaceIPoint;
}
}
在產生了類型庫之後,既可以將其作為一個單獨的檔案發行,也可以將其作為一個資源套件含在EXE或DLL中。
3.2類型庫的使用
使用類型庫的第一步是裝載它,LoadRegTypeLib他將試圖從Windows的註冊表中裝載指定的類型庫。若失敗則可以使用LoadTypeLib從磁碟裝載指定的類型庫或者LoadTypeLibFromResource從EXE/dll裝載指定的類型庫。
HRESULThr;
LPTYPELIBpTypeLib = NULL;
hr= LoadRegTypeLib(LIBID_Point,1,0,0,&pTypeLib);
if(FAILED(hr))
{
hr= LoadTypeLib(OLESTR("Point.tlb"),&pTypeLib);
}
if(FAILED(hr))
return hr;
hr= pTypeLib->GetTypeInfoOfGuid(IID_IPoint,&s_pTypeInfo);
pTypeLib->Release();
if(FAILED(hr))
return hr;
s_pTypeInfo->AddRef();
return hr;
3.3註冊表中的類型庫
在HKEY_CLASS_ROOT\TypeLib關鍵字下地LIBID類表。
4.類型庫的實現
STDMETHODIMPCPoint::GetTypeInfoCount(__RPC__out UINT *pctinfo)
{
*pctinfo= 1;
return S_OK;
}
STDMETHODIMP CPoint::GetTypeInfo(UINTiTInfo, LCID lcid, ITypeInfo **ppTInfo)
{
HRESULThr;
if(iTInfo != 0)
return TYPE_E_ELEMENTNOTFOUND;
if(ppTInfo == NULL)
return E_POINTER;
if(s_pTypeInfo)
{
s_pTypeInfo->AddRef();
hr= S_OK;
}
else
{
hr = LoadMyTypeLib();
}
if (!hr)
*ppTInfo= s_pTypeInfo;
return hr;
}
STDMETHODIMP CPoint::GetIDsOfNames(REFIIDriid, LPOLESTR *rgszName, UINT cNames, LCID lcid, DISPID *rgDispId)
{
HRESULThr;
if(IsEqualIID(riid,IID_NULL))
return DISP_E_UNKNOWNINTERFACE;
if (!s_pTypeInfo)
{
hr= LoadMyTypeLib();
if(FAILED(hr))
return hr;
}
hr= s_pTypeInfo->GetIDsOfNames(rgszName,cNames,rgDispId);
return hr;
}
STDMETHODIMP CPoint::Invoke(DISPIDdispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams,VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
HRESULThr;
if(IsEqualIID(riid,IID_NULL))
return DISP_E_UNKNOWNINTERFACE;
if (!s_pTypeInfo)
{
hr= LoadMyTypeLib();
if(FAILED(hr))
return hr;
}
hr= s_pTypeInfo->Invoke(this,dispIdMember,wFlags,pDispParams,pVarResult,pExcepInfo,puArgErr);
return hr;
}
HRESULT CPoint::LoadMyTypeLib()
{
HRESULThr;
LPTYPELIBpTypeLib = NULL;
hr= LoadRegTypeLib(LIBID_Point,1,0,0,&pTypeLib);
if(FAILED(hr))
{
hr= LoadTypeLib(OLESTR("Point.tlb"),&pTypeLib);
}
if(FAILED(hr))
return hr;
hr= pTypeLib->GetTypeInfoOfGuid(IID_IPoint,&s_pTypeInfo);
pTypeLib->Release();
if(FAILED(hr))
return hr;
s_pTypeInfo->AddRef();
return hr;
}