1. IDispatch接口
(1) 自动化与IDispatch接口
自动化是基于COM的IDispatch接口的,IDispatch接口提供了一系列的方法,使得客户机程序能够在基于自动化的服务器上动态访问组件的功能。这种动态调用与COM的自定义接口技术不同:使用自定义接口时,客户机程序需要了解组件接口的一些编译时信息,这些信息要么通过类型库里获取,要么通过编译时包含组件接口声明的头文件来获取(如imath.h),这种是组件接口的前绑定(编译时绑定)。如果一个组件在实现它的方法时使用IDispatch接口而不是自定义的接口,则能够提供更多功能。客户机程序与服务器应用程序之间交互,必须对组件的接口方法使用后绑定。这种安排方式使得服务器组件在更改它的接口时不需要客户机程序重新编译和链接。使用IDispatch接口的另一个重要的好处是它的通用调度机制(universal marshaling),并不需要创建并随之附带一个proxy/stud DLL.
(2) IDispatch接口结构
大多数COM接口与IMath的例子非常相似,它们提供了一个结构,该结构需要一个严格按规定实现的抽象类。 IDispatch略有不同,它给Vtable格式的接口添加了一个间接层次,客户机程序不用通过Vtable指针去访问组件的功能,例如我们使用IMath接口访问组件功能时就需要一个IMath指针。


如左图所示,现在Vtable不必直接访问IMath接口,相反,第一次对接口的调用必须是IDispatch::Invoke,该方法包含一个参数,用于把对方法的调用映射到派发映射中的一个入口。 方法 说明
Invoke Invoke方法提供了IDispatch接口大部分功能,8个参数中最重要的是DISPID,此参数被映射为 派发表中一个确定的偏移量,用来确定哪个接口方法将被调用。
GetIDsOfNames 为客户机程序提供了一种将基于文本的自动化服务器属性或方法名(如Add)映射到DISPID 数字值的功能。经映射以后就可以使用DISPID和Invoke函数来访问组件的方法了。
//Step1 — Create an instance of the math component and QI for IDispatch
CLSIDFromProgID(“COM.Learning.Note.Math”, &clsid);
IDispatch pDispatch = 0;
CoCreateInstance(clsid, … IID_IDispatch, (void**)&pDispatch);
//Step2 — Get the DISPID for Add
DISPID dispid;
pDispatch->GetIDsOfNames(“Add”, &dispid);
//Step3 — Build the parameters based on the context of the call and then invoke the method
pDispatch->Invoke(dispid, parameters, &lresult, …);
//Step4 — Release the pointer
pDispatch->Release();
(3) 双向接口
一个双向接口(Dual interface)相当于把一个自定义接口(如IMath)和一个标准的IDispatch接口结合在了一起。如上面右图所示。如果服务器有一个进程内的实现(DLL),则不需要调度。此时客户机陈故乡可以直接绑定到自定义接口方法,而且调用时效率很高。儿当客户机程序需要后绑定功能时,就可以使用IDispatch实现。
2. 自动化数据类型
自动化为参数和返回值在不同进程和计算机之间进行传输提供了标准的调度方法,它是通过调度器OLEAUT32.DLL实现的。
(1) VARIANT数据类型
VARIANT数据类型提供了一种十分有效的机制,可以实现各种不同的自动化数据的传输,因为VARIANT中既包含了数据本身,还包含了数据的类型。VARIANT数据类型实际上就是一个包含不同数据类型的大的union,使用一个标识符来表示什么类型的数据正存放在union中。e.g.
VARIANT varResult;
VariantInit(&varResult);
varResult.vt = VT_l4;
varResult.lVal = 1045;
相关API: 函数 使用目的
VariantInit 将变量初始化为VT_EMPTY
VariantClear 通过释放变量所占用的内存来初始化VARIANT类型变量, 当客户端程序需要从服务器传来的VARIANT类型变量清除时,可使用
VariantChangeType 将变量从一种数据类型强制转换为另一种类型
使用VARIANT变量可以实现自动化方法的重载。e.g.
STDMETHODIMP CMath::Add(VARIANT varOp1, VARIANT varOp2, VARIANT *pvarResult)
{
HRESULT hr;
hr = VariantChangeType(&varOp1, &varOp1, 0, VT_l4);
if (FAILED(hr))
return(DISP_E_TYPEMISMATCH);
hr = VariantChangeType(&varOp2, &varOp2, 0, VT_l4);
if (FAILED(hr))
return(DISP_E_TYPEMISMATCH);
if (pvarResult)
{
VariantInit(pvarResult);
pvarResult->vt = VT_l4;
pvarResult->lVal = varOp1.lVal + varOp2.lVal;
}
return S_OK;
}
再举一个不需要转换的例子:
//If we have two strings, append them
if (varOp1.vt == VT_BSTR && varOp2.vt == VT_BSTR)
{
VariantInit(pvarResult);
CComBSTR bstr(varOp1.bstrVal);
bstr.AppendBSTR(varOp2.bstrVal);
//return the concatenated string
pvarResult->vt = VT_BSTR;
pvarResult->bstrVal = bstr.Copy();
return S_OK;
}
(2) SafeArray数据类型
SafeArray最早是一个VB数据类型,后来成为COM中的一员。一个SafeArray可以有多个维,它使用一个引用计数器来实现跨进程的内存管理。使用SafeArray时不能直接访问它的数据成员,应使用COM所提供的响应API函数。 函数 使用目的
SafeArrayCreate 创建一个SafeArray
SafeArrayDestroy 删除一个SafeArray
SafeArrayGetElement 从数组中获取指定的元素(成员)
SafeArrayPutElement 将一个元素加到数组中
SafeArrayGetLBound 获取数组的下界
SafeArrayGetUBound 获取数组的上界
需要注意的是安全数组并不是从0或1开始的,所以下边界和上边界是必须的。使用示例:
STDMETHODIMP CMath::Sum(VARIANT varOp1, long *plResult)
{
//Make sure we have an array of longs
if (!(varOp1.vt & VT_l4))
return DISP_E_TYPEMISMATCH;
if (!(varOp1.vt & VT_l4))
return DISP_E_TYPEMISMATCH;
//The parameter may be a reference
SAFEARRAY *psa;
if (varOp1.vt & VT_BYREF)
psa = *(varOp1.pparray);
else
psa = varOp1.parray;
//Get the lower and upper bounds
long lLBound, lUBound;
SafeArrayGetLBound(psa, 1, &lLBound);
SafeArrayGetUBound(psa, 1, &lUBound);
//Sum the elements of the array
long lSum = 0;
for (long i = lLBound; i &le lUBound; i++)
{
long lValue;
SafeArrayGetElement(psa, &i, lValue);
lSum +=lValue;
}
*plResult = lSum;
return S_OK;
}
而传送SafeArray的代码如下实例:
//Try calling our Sum method
//We first build a safe array whose values are 0, 1, 2, 3, 4
SAFEARRAY *psaARrray = 0;
SAFEARRAYBOUND rgsabound[1];
rgsabound[0].lLbound = 0;
rgsabound[0].cElements = 10;
psaArray = SafeArrayCreate(VT_l4, 1, rgsabound);
//Fill the array with values
for (int i = 0; i < 10; i++)
SafeArrayPutElement(psaArray, (long*)&i, &i);
VARIANT varArray;
(&varArray)->vt = VT_ARRAY | VT_l4;
(&varArray)->pArray = psaArray;
//Call the method
long lResult;
pMath2->Sum(varArray, &lResult);
3. 实现一个派发接口
我们首先讲述一个服务器端的组件,它提供了一个派发接口和双向接口,之后讨论客户机程序中如何访问该方法的功能。如果一个组件通过IDispatch接口向外公开它的功能,则它被看作一个automation component.
// access to the global variables
extern DWORD g_dwObjs;
extern DWORD g_dwLocks;
DEFINE_GUID( CLSID_Math, 0xA988BD40,0x9F1A,0x11CE,0x8B,0x9F,0×10,0×00,0x5A,0xFB,0x7D,0×30);
const DISPID_ADD = 1;
const DISPID_SUBTRACT = 2;
const DISPID_MULTIPLY = 3;
const DISPID_DIVIDE = 4;
class Math : public IDispatch
{
protected:
// Reference count
DWORD m_dwRef;
public:
Math();
~Math();
public:
// IUnknown
STDMETHOD(QueryInterface)( REFIID, void** );
STDMETHOD_(ULONG, AddRef());
STDMETHOD_(ULONG, Release());
// IDispatch
STDMETHOD(GetTypeInfoCount)( UINT* pctinfo );
STDMETHOD(GetTypeInfo)( UINT itinfo, LCID lcid, ITypeInfo** pptinfo );
STDMETHOD(GetIDsOfNames)( REFIID riid, OLECHAR** rgszNames, UINT cNames, LCID lcid, DISPID* rgdispid );
STDMETHOD(Invoke)( DISPID dispid,REFIID riid,LCID lcid,WORD wFlags,DISPPARAMS FAR* pDispParams,
VARIANT FAR* pvarResult,EXCEPINFO FAR* pExcepInfo,unsigned int FAR* puArgErr );
};
对于内部自动化实现,只需要实现GetIDsOfNames和Invoke两个方法即可。
STDMETHODIMP Math::GetIDsOfNames( REFIID riid,
OLECHAR** rgszNames,
UINT cNames,
LCID lcid,
DISPID* rgdispid )
{
// To make things simple, we only support 1 name at a time
if ( cNames > 1 )
return( E_INVALIDARG );
// Convert the member name to ANSI
CHAR szAnsi[128];
long lLen = WideCharToMultiByte( CP_ACP,
0,
rgszNames[0],
wcslen( rgszNames[0] ),
szAnsi,
sizeof( szAnsi ),
0,
0 );
szAnsi[lLen] = ‘\0′;
// Compare the member name to see if it’s one that we have
// and return the correct DISPID
if ( strncmp( “Add”, szAnsi, 3 ) == 0 )
rgdispid[0] = DISPID_ADD;
else if ( strncmp( “Subtract”, szAnsi, 8 ) == 0 )
rgdispid[0] = DISPID_SUBTRACT;
else if ( strncmp( “Multiply”, szAnsi, 8 ) == 0 )
rgdispid[0] = DISPID_MULTIPLY;
else if ( strncmp( “Divide”, szAnsi, 6 ) == 0 )
rgdispid[0] = DISPID_DIVIDE;
else
return( DISPID_UNKNOWN );
return S_OK;
}
STDMETHODIMP Math::Invoke( DISPID dispid,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS FAR* pDispParams,
VARIANT FAR* pvarResult,
EXCEPINFO FAR* pExcepInfo,
unsigned int FAR* puArgErr )
{
// All of our methods take two parameters
if ( !pDispParams ||
pDispParams->cArgs != 2 )
return( DISP_E_BADPARAMCOUNT );
// We don’t support named arguments
if ( pDispParams->cNamedArgs > 0 )
return( DISP_E_NONAMEDARGS );
// Break out the parameters and coerce them
// to the proper type
HRESULT hr;
VARIANT varOp1;
VARIANT varOp2;
// Coerce the variant into the desired type
// In this case we would like a long
VariantInit( &varOp1 );
hr = VariantChangeType( &varOp1,
&(pDispParams->rgvarg[1]),
0,
VT_I4 );
// If we can’t get a long return invalidate argument
if ( FAILED( hr ))
return( DISP_E_TYPEMISMATCH );
// Coerce the variant into the desired type
// In this case we would like a long
VariantInit( &varOp2 );
hr = VariantChangeType( &varOp2,
&(pDispParams->rgvarg[0]),
0,
VT_I4 );
// If we can’t get a long return invalidate argument
if ( FAILED( hr ))
return( DISP_E_TYPEMISMATCH );
// Initialize the return value
// If there isn’t one, then just return
if ( pvarResult )
{
VariantInit( pvarResult );
pvarResult->vt = VT_I4;
}
else
return S_OK;
switch( dispid )
{
case DISPID_ADD:
pvarResult->lVal = varOp1.lVal + varOp2.lVal;
return S_OK;
case DISPID_SUBTRACT:
pvarResult->lVal = varOp1.lVal – varOp2.lVal;
return S_OK;
case DISPID_MULTIPLY:
pvarResult->lVal = varOp1.lVal * varOp2.lVal;
return S_OK;
case DISPID_DIVIDE:
pvarResult->lVal = varOp1.lVal / varOp2.lVal;
return S_OK;
default:
return( DISP_E_MEMBERNOTFOUND );
}
}
总而言之,客户机程序必须把所有方法参数(他们存储在VARIANT类型变量中)填充到 DISPPARAMS结构,并且如果方法需要返回值则必须提供pvarResult参数。接下来必须对参数进行检查,以确保参数的内容与你期望的一致。然后把VARIANT类型变量强制转换为long类型。最后把所得到的运算结果封装到方法提供的VARIANT变量中,并把它返回给客户机程序。为了让客户机程序能够查询组件的派发接口(IDispatch)或Vtable(IMath)接口,从而进一步访问组件的功能,需要实现QueryInterface().
STDMETHODIMP Math::QueryInterface( REFIID riid, void** ppv )
{
*ppv = NULL;
if ( riid == IID_IUnknown ||
riid == IID_IDispatch ||
riid == IID_IMath)
*ppv = this;
if ( *ppv )
{
( (LPUNKNOWN)*ppv )->AddRef();
return( S_OK );
}
return E_NOINTERFACE;
}
上述代码对组件实现的三个接口提出的任何请求都由一个Vtable来处理,原因是这三个接口被组织在一个Vtable中了。