在COM中使用数组参数-ICollection
关键字:DCOM、数组、自定义类型、Marshal、SafeArray、ICollection
1 使用ICollection
ICollection是从 IDispatch继承的接口。ICollection还需要一个IEnumVARIANT接口配合实现功能。IEnumVARIANT是从IUnknown继承的,而不是从IDispatch接口继承。
ICollection接口提供了最大的面向对象的设计灵活性和可重用性。在数组指针和SafeArray方法中,数组的每个元素必须事先计算出来,并且保存在特定的数据结构中。使用ICollection接口,可以设计出动态生成的数组,就是说数组的元素在需要的时候才进行计算,以便减少内存使用并加快处理速度。
1.1 ICollection和IEnumVARIANT
ICollection接口用于定义数组对象,而IEnumVARIANT接口用于定义枚举对象。枚举对象的作用是按顺序读取数组元素,有时,通过枚举对象可以获得更高的效率。
ICollection和IEnumVARIANT的定义如下:
interface ICollection : IDispatch
{
[propget, id(DISPID_LISTITEM)] HRESULT Item(
[in] const VARIANT varIndex,
[out, retval] VARIANT *pVal);
[propget, id(DISPID_LISTCOUNT)] HRESULT Count(
[out, retval] long *pVal);
[propget, id(DISPID_COLLCOUNT)] HRESULT length(
[out, retval] long *pVal);
[propget, id(DISPID_NEWENUM), restricted, hidden]
HRESULT _NewEnum([out, retval] IUnknown* *pVal);
... // 其它方法或属性
};
interface IEnumVARIANT : IUnknown
{
HRESULT Next(
unsigned long celt,
VARIANT * rgvar,
unsigned long * pceltFetched);
HRESULT Skip(unsigned long celt);
HRESULT Reset();
HRESULT Clone(IEnumVARIANT ** ppenum);
};
有的时候,COM对象不但要实现数组功能,而且还要实现其它功能。所以,大多数时候,COM对象实现的接口是从ICollection继承来的。
通过ICollection操纵数组大体上有两种方法。一种是通过Item属性用数组下标取得元素。这种方式,每次只能取得一个元素,而且要传递下标对象,所以效率比较低下。另一种方法是通过枚举器。数组对象的枚举器通过_NewEnum属性取得。通过枚举器只能按顺序获取元素,但每次可以取得任意多的元素,所以效率较高。ICollection对象可以只实现其中的一种访问方法,也可以两种都实现。ICollection中还有一个重要属性:Count。Count属性返回数组的长度,对于无法确定长度的数组,也可以不实现Count属性。
IEnumVARIANT接口用于定义枚举器。枚举器用于顺序读取数组元素。通过Next方法,可以一次读取任意多的元素。由于枚举器只可以按顺序访问数组元素,所以Next方法不需要传递下标。Skip方法用于跳过若干元素,而不读取。Reset把当前元素设置到数组头,这样就可以重新开始枚举。Clone用于获得一个新的枚举器。两个枚举器可以互不干扰的工作。
要注意的事,可能有某些数组对象的实现方法使用不同的属性名称。实际上ICollection中的属性名称是不重要的,重要的是Dispatch ID。只要通过Dispatch ID就可以取得正确的属性。
1.2 数组对象
数组对象是实现了ICollection接口的COM对象。数组对象的使用者通过ICollection接口取得数组中的数据,而完全不需要知道数组的具体实现方式。这种设计的好处是使用数组的代码可以完全不理会数组的实现方法,而当数组的实现发生变化时,使用数组的代码可以在二进制代码上保持兼容,也就是说目标代码不用编译就可以使用。
最简单制作数组对象的方法是使用ATL的模板。CComEnumOnSTL模板用于生成实现IEnumVARIANT接口的枚举对象。当然,如果要实现数组对象的所有优点,最好自己编写数组对象的代码。
1.3 ICollection参数的IDL声明
在IDL声明中。数组对象应该声明成IDispatch *。如果是输出或输入输出参数,则应该使用双重指针。
[id(0)] GetNumber([out] IDispatch ** ppObj);
[id(1)] SetNumber([in] IDispatch * pObj);
目前,我们看到的ICollection数组都是只读的。实际上ICollection完全可以设计成可读写的数组对象,只要把ICollection的Item属性设置成可读写的就可以了。关于可读写的ICollection对象请参考相关资料。
1.4 通过ATL实现数组对象
ATL通过两个模板实现对ICollection的支持。它们就是CComEnumOnSTL和ICollectionOnSTLImpl。CComEnumOnSTL用于实现基于STL对象的枚举器。ICollectionOnSTLImpl用于实现ICollection接口。下面详细描述这两个模板的功能和用法。
1.4.1 CComEnumOnSTL
CComEnumOnSTL的定义如下:
template <class Base,
const IID* piid,
class T,
class Copy,
class CollType,
class ThreadModel = CComObjectThreadModel>
class ATL_NO_VTABLE CComEnumOnSTL :
public IEnumOnSTLImpl<Base, piid, T, Copy, CollType>,
public CComObjectRootEx< ThreadModel >
模板参数中,Base是枚举器所实现的接口,通常是IEnumVARIANT。piid是枚举器接口的IID,通常是IID_IEnumVARIANT。T是枚举器输出数值的类型,通常是VARIANT。Copy是复制类,用于将STL对象中的值转换成枚举器输出参数。CollType是用于存储数据的STL类型。ThreadModel是线程模式参数,可以是CComSingleThreadModel或CcomMultiThreadModel,缺省值是当前缺省的线程模式。
假设使用vector类保存数组元素。而vector参数是long型数据。可以通过以下方法实现枚举器。
1. 定义CollType
typedef std::vector<long> CollType;
2. 定义Copy类
Copy类用于在STL类的元素类型和枚举器类型之间进行参数转换。每个Copy类必须有三个静态函数:init、copy、destroy。Init用于初始化枚举器类、copy用于把STL元素复制到枚举器参数、destroy用于销毁枚举器参数。
下面是用于在long和VARIANT之间转换的Copy类实例。
class CopyVariantLong
{
public:
static void init(VARIANT * p)
{
VariantInit(p);
}
static HRESULT copy(VARIANT * pTo, const LONG * pFrom)
{
pTo->vt = VT_I4;
pTo->lVal = *pFrom;
return S_OK;
}
static void destroy(VARIANT * p)
{
VariantClear(p);
}
};
3. 定义枚举器
通过以上定义的类就可以方便的定义枚举器类型了。
typedef CComEnumOnSTL<IEnumVARIANT,
&IID_IEnumVARIANT,
VARIANT,
CopyVariantLong,
CollType> EnumType;
1.4.2 ICollectionOnSTLImpl
ICollectionOnSTLImpl用于帮助实现ICollection接口。ICollectionOnSTLImpl定义如下:
template <class T,
class CollType,
class ItemType,
class CopyItem,
class EnumType>
class ICollectionOnSTLImpl : public T
在ICollectionOnSTLImpl模板中,T是要实现的接口,一般会使用从ICollection继承的接口。CollType参数是用于保存数据的STL类型,这个类型应该和枚举器中的相同。ItemType是ICollection中Item属性的类型,一般是VARIANT。CopyItem是Item属性的Copy类,和枚举器中的Copy类是相同的。EnumType是枚举器的类型。
可以通过以下步骤实现ICollection接口。
1. 定义ICollection类型
typedef ICollectionOnSTLImpl<INumberCollection,
CollType,
VARIANT,
CopyVariantLong,
EnumType> CollectionType;
2. 定义数组对象
定义数组对象和定义普通ATL的COM对象是类似的。只要把IDispatchImpl中的接口参数(第一个参数)变成刚刚完成的ICollectionOnSTLImpl参数就可以了。
class ATL_NO_VTABLE CNumberCollection :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CNumberCollection, &CLSID_NumberCollection>,
public IDispatchImpl<CollectionType,
&IID_INumberCollection,
&LIBID_COLLECTIONOBJLib>
{
...
}
1.5 使用数组对象
对于通用的ICollection对象,只能够通过IDispatch访问。也就是说通过IDispatch::Invoke方法访问数组中的元素。
另一方面,ICollection对象通常指通过VARIANT类型传递数据。所以,我们也必须了解如何访问VARIANT类型的变量。
1.5.1 调用IDispatch
IDispatch是Automation中定义的接口。通过IDispatch,COM客户可以取得接口中每个方法和属性的类型、参数和返回值等信息。通过IDispatch的Invoke方法,COM客户还可以直接调用接口中的方法和属性。IDispatch的内容非常丰富,这里不可能做全面地介绍,所以指对如何通过Invoke方法调用IDispatch做一个简单的说明。
1. Invoke方法的定义
HRESULT Invoke(
DISPID dispIdMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS FAR* pDispParams,
VARIANT FAR* pVarResult,
EXCEPINFO FAR* pExcepInfo,
unsigned int FAR* puArgErr
);
Invoke的参数如下:
l dispIdMember:所调用的属性或方法的dispatch id
l riid:保留,必须是IID_NULL
l lcid:语言环境。一般使用LOCALE_THREAD_DEFAULT
l wFlags:可以是以下四个参数之一:
DISPATCH_METHOD方法调用
DISPATCH_PROPERTYGET()读属性
DISPATCH_PROPERTYPUT()写属性
DISPATCH_PROPERTYPUTREF()通过引用写属性
l pDispParams:参数数组
l pVarResult:返回值
l pExcepInfo:被调用方法或属性内部异常(如果发生异常)
l puArgErr:当返回DISP_E_PARAMNOTFOUND或DISP_E_TYPEMISMATCH时,返回出错的参数序号。
以下是使用Invoke的例子。下例返回一个dispatch id是DISPID_LISTCOUNT的简单参数,实际上就是数组的长度。
VARIANT varResult;
DISPPARAMS DispParams;
EXCEPINFO excepInfo;
UINT errArg;
VariantInit(&varResult);
DispParams.cArgs = 0;
DispParams.cNamedArgs = 0;
DispParams.rgdispidNamedArgs = NULL;
DispParams.rgvarg = NULL;
hr = pObj->Invoke(
DISPID_LISTCOUNT,
IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_PROPERTYGET,
&DispParams,
&varEnum,
&excepInfo,
&errArg);
if (FAILED(hr))
{
goto CleanUp;
}
下例返回一个带参数的属性。
VARIANT varIndex;
VARIANT varResult;
DISPPARAMS DispParams;
EXCEPINFO excepInfo;
UINT errArg;
VariantInit(&varIndex);
VariantInit(&varResult);
DispParams.cArgs = 1;
DispParams.cNamedArgs = 0;
DispParams.rgdispidNamedArgs = NULL;
DispParams.rgvarg = &varIndex;
VariantClear(&varIndex);
VariantClear(&varResult);
varIndex.vt = VT_I2;
varIndex.iVal = (short) Index;
hr = pObj->Invoke(
DISPID_LISTITEM,
IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_PROPERTYGET,
&DispParams,
&varResult,
&excepInfo,
&errArg);
if (FAILED(hr))
{
...
}
1.5.2 使用IEnumVARIANT枚举数据
要使用IEnumVARIANT枚举数据,首先必须取得IEnumVARIANT指针。取得IEnumVARIANT指针是通过ICollection的_NewEnum属性。具体操作可以参考上一节关于Invoke的说明。
在取得了IEnumVARIANT之后,就可以通过IEnumVARIANT顺序读取数组元素了。
请参考以下代码枚举数据:这段代码是将数组中的元素相加求总和。
ULONG Result = 0;
ULONG res;
while (1)
{
hr = pEnum->Next(1, &var, &res);
if (FAILED(hr))
{
goto CleanUp;
}
if (hr != S_OK || res != 1)
{
break;
}
hr = VariantChangeType(&var, &var, 0, VT_I4);
if (FAILED(hr))
{
goto CleanUp;
}
Result += var.lVal;
}
1.5.3 使用Item和Count
除了使用枚举器,还可以使用Item和Count属性读取元素。和使用枚举器相比,使用Item和Count可以随时取得任一个元素,但是速度会比使用枚举器慢。
可以参考通过Invoke读取Automation属性的方法取得数组元素。
1.5.4 VARIANT类型
在ICollection中,大量使用VARIANT数据。这里把VARIANT的使用方法总结一下:
1. 直接使用VARIANT变量
a. 定义VARIANT变量
可以直接定义VARIANT类型的变量。
VARIANT val;
b. 初始化VARIANT变量
在使用VARIANT变量之前,一定要初始化。
VariantInit(&val);
c. 设置变量值
设置变量值前如果VARIANT变量中已经有值,先要清除原有数据。
VariantClear(&val);
val.vt = VT_I4; // 设置类型
val.lVal = 10; // 设置变量值
d. 清除VARIANT变量
在使用完VARIANT变量后,要清除变量,否则会发生内存泄漏。
VariantClear(&val);
e. 动态分配VARIANT变量
如果要动态分配VARIANT变量,应该使用标准的COM内存管理函数。
标准COM内存管理函数包括CoTaskMemAlloc、CoTaskMemFree和CoTaskMemRealloc。
VARIANT * pVal;
pVal = (VARIANT *)CoTaskMemAlloc(size_of(VARIANT));
VariantInit(pVal);
pVal->vt = VT_I4;
pVal->lVal = 10;
...
VariantClear(pVal);
CoTaskMemFree(pVal);
2. 通过CComVariant使用VARIANT变量
CComVariant是ATL对于VARIANT的简单包装。通过CComVariant可以更简单的使用VARIANT,而不必担心没有进行初始化或清除。如果没有特殊情况,应该尽量使用CComVariant而不要使用VARIANT。
以下是使用CComVariant的代码实例。
CComVariant Val;
Val.vt = VT_I4;
Val.lVal = 10;
// Val 不必清除
以下是使用CComVariant数组的例子。
CComVariant * pVal;
pVal = new CComVariant[10];
for (int i = 0; i < 10; ++i)
{
pVal[i].vt = VT_I4;
pVal[I].lVal = i + 1;
}
...
delete[] pVal;
2 后记
由于时间关系,以及COM规范本身的复杂性。本文不可能面面俱到,只能起到抛砖引玉的作用。我这里有关于本文内容的实例代码,大家可以通过email索取。我的email地址是nelsonc@online.sh.cn。
大家如果有什么不清楚的地方,也可以通过email探讨。如果大家想了解关于COM或dotNet的其它内容也可以告诉我。我以后会发表更多的文章,希望能对大家有所帮助。