在COM中使用数组参数-数组指针
关键字:DCOM、数组、自定义类型、Marshal、SafeArray、ICollection
1 使用数组指针
数组指针使用标准的C/C++数组表示方式。数组中的每个元素按照顺序在内存中依次排放。数组的下标从0开始计算。数组的第一个元素(下标为0的元素)的地址就是数组的指针,数组中每个元素所占的内存空间大小必须是固定的,只和数组类型有关。计算数组中某个元素的指针时,使用元素所占的字节数乘上元素下标就可以得出这个元素和数组指针之间的偏移量[1]。根据这个偏移量就可以计算出这个元素的地址指针了。
使用数组指针最重要的是确定数组长度,使序列化程序可以正确地复制内存。为了生成代理和存根程序,我们要使用接口定义语言(IDL)描述接口和COM对象类型。MIDL读取IDL文件中的描述,生成TLB和代理存根的代码。
1.1 IDL声明
1.1.1 参数传递方向
为了尽量提高序列化的效率,在IDL中可以确定参数的传递方向。参数的传递方向有三种:输入型(in)、输出型(out)、和输入输出型(in, out)。输入型参数从调用者传递到被调用者,被调用者对输入型参数的更改不传回调用者。输出型参数正相反,从被调用者分回调用者,而被调用者不关心参数的初始值。输入输出型参数在调用的时候传到被调用者,同时,被调用者可以对参数进行修改,这个修改在调用返回的时候会复制回调用者。
非指针类型一定是输入型参数。输出型参数和输入输出型参数一定是指针类型。
1.1.2 数组长度和复制长度
在IDL声明中,应该正确的设置数组长度和复制长度。数组长度用size_is或min_is、max_is属性定义,复制长度则使用length_is或first_is、last_is属性定义。在目前的IDL实现中,min_is并没有实现,所以,min_is只能是0。
size_is属性用来设置数组在内存中的长度。数组在内存中的长度也可以通过max_is和min_is共同定义。他们的关系是:size = max – min + 1。由于min只能是0,所以,size = max + 1。
length_is属性用来设置在序列化时需要复制的元素数量。需要复制的元素数量也可以通过first_is和last_is共同定义。他们的关系是:length = last – first + 1。可以同时定义first_is和length_is来定义复制的范围,也可以同时定义first_is和last_is来定义复制的范围。如果只定义了first_is,则last_is和有效的max_is值相同。如果没有定义first_is,使用缺省值0。但是,length_is和last_is不可以同时定义。复制元素的范围不能够超出数组本身的范围。如果没有指定复制范围,缺省的复制范围是整个数组。
复制范围一般不在单纯的输入参数或输出参数上使用,而只在输入输出型参数中使用[2]。在IDL中指定复制范围会影响到哪部份的内存数据会从客户端复制到COM端;当COM端的方法返回时,也是根据复制范围,把数据复制回客户程序。数组从客户端复制到COM端和从COM端复制回客户端的范围可以是不同的。
数组用作输出参数时,有两种方案:调用方建立数组和被调用方建立数组。当输出数组的长度是可预知的时候,应该使用调用方建立数组的方式。数组用作输出时,需要把属性中的in改成out。参见例2和例3。
以下是使用数组指针的例子。
例1:计算数组中数字的和,使用辅助变量确定需要传递的数据长度。
HRESULT Sum (
[in, size_is(Count)] long * pNums,
long Count,
[out, retval] long * pResult);
也可以使用数组风格的定义
HRESULT Sum (
[in, size_is(Count)] long Numbers[],
long Count,
[out, retval] long * pResult);
例2:计算数组中前Count个数的平方,保存到数组的后Count个元素中,注意数组长度和复制长度是不同的。
HRESULT Square (
[in, out, size_is(Count*2), length_is(Count)] long * pArray,
long Count);
或使用数组风格的定义:
HRESULT Square (
[in, out, size_is(Count*2), length_is(Count)] long Array[],
long Count);
例3:取得前n个质数。由调用方申请内存。这里仅列出指针风格的定义。
HRESULT Prime (
long n,
[out, size_is(n)] long * pResult]);
1.1.3 双重指针
在IDL中,可以使用双重或多重指针类型的参数。这里只介绍用于数组输出用的双重指针,关于多重指针的使用,请参考相关文档。
双重指针的size_is属性中包含用逗号分隔的两个长度值,分别是顶级指针数组的长度和次级指针的长度。在长度值空缺的情况下,使用缺省值1。例如:size_is(m,n)的含义是顶级指针长度是m,次级指针长度是n。size_is(,n)相当于size_is(1,n)。
在使用数组指针传递数组时,常用的是顶级指针长度是1的双重指针。也就是指向数组指针的指针。
1.1.4 被调用方建立数组
如果输出的数组长度无法预知,就需要被调用的函数动态申请内存,建立数组。这个时候,由于调用方不知道数组的长度,不可能事先申请内存。所以,需要被调用方申请内存,并且把所申请的内存的地址传给调用方。也就是说,调用方要传递存放数组指针地址的地址给被调用方,也就是通过数组指针的指针传递参数。
例1:取得所有员工编号,员工编号用长整形表示,员工的数量和每个员工编号都是输出参数。
HRESULT GetStaffId (
[out] long * pNumber,
[out, size_is(, *pNumber)] long ** pResult]);
例2:返回传入数组中质数元素的指针。输出是一个指针数组,所以所传递的参数是一个三重指针,但本质上却是一个二重指针,所以size_is参数仍然使用二重指针的型式。
HRESULT Prime2 (
long n,
[in, size_is(n)] long * pNums,
[out, size_is(, * pCount)] long *** pPrimeNums,
[out] long * pCount);
1.1.5 多维数组
在IDL中,没有定义多维数组,如果要使用多维数组,必须转换成一维数组来处理。一维数组的长度是多维数组中每个维度长度的乘积。比如说3*5的数组可以用一个长度为15的数组表示。
例1:计算行列式的值。
HRESULT Determinant (
long Order,
[in, size_is(Order * Order)] double * pNumbers,
[out] double * pResult);
调用时使用类型转换把多维数组转换成一维数组。
double Num[10][10];
double Result;
// set the item in array Num
HRESULT hr = obj->Determinant (10, (double*) Num, &Result);
例2:计算逆矩阵,输出数组由调用方建立。
HRESULT AntiMatrix (
long Order,
[in, size_is(Order * Order)] double * pMatrix,
[out, size_is(Order * Order)] double * pAntiMatrix);
例3:取得转换表(一个二维数组)。
HRESULT GetConvertTable (
[out] long * pLineNum;
[out] long * pColNum;
[out, size_is(, *pLineNum * *pColNum)] bool ** pTable);
在例3的输出中,取得数组元素时比较复杂,可以使用如下方法取得第i行第j列的元素:
if (i < *pLineNum && j < *pColNum)
{
bPass = (*pTable)[i*(*pColNum) + j];
}
1.1.6 字符串
字符串是一种特殊的数组形式,这种数组不定义长度,而是以特殊标志——数值0结尾。由于在C语言中字符串都是以0结尾的方式存储,所以这种类型常被用于传递字符串参数。在IDL中规定字符串的元素类型只能是单字节或wchar_t类型,而且不能是多维的。
定义字符串的属性是string。字符串数组只能是一维的。如果字符串是输出参数,应该使用双重指针,或者使用size_is属性指定buffer的长度。
例1:字符串作为输入参数
HRESULT PutString (
[in, string] char * pStr);
例2:字符串作为输出参数,被调用方申请内存。
HRESULT GetString (
[out, string] char ** pStr);
例3:输出字符串,调用方申请内存。
HRESULT GetString2 (
[in] long nMaxSize,
[out, size_is(nMaxSize), string] char * pStr);
1.1.7 固定长度数组
在上面提到的数组指针方法中,size_is中的参数也可以是常量,但是效率比较低。如果数组长度是一个常量,可以使用固定长度数组的定义。固定长度的数组可以是多维的,可以是输入、输出或输入输出类型。
例1:
HRESULT Add2 (
long Nums[10],
[out, retval] long * pResult);
例2:
HRESULT GetNumbers (
[out] long Nums[10]);
1.2 数组指针的内存管理
根据COM规范,输入型参数由调用方申请和释放内存。输出型参数由被调用方申请内存,由调用方释放内存。输入和输出型参数,由调用方申请内存,被调用方可以释放并重新申请内存,最终由调用方释放内存。
由于涉及到代理和存根,跨套间的调用时代理和存根也参与内存管理,所以,COM和客户端必须使用相同的内存管理方式。在COM中,系统提供了一套内存管理函数,凡是涉及到COM接口参数的内存块都必须通过这几些函数进行管理,这里列出这些函数原型,具体说明请参考相关文档。
LPVOID CoTaskMemAlloc (ULONG cb);
void CoTaskMemFree (LPVOID pv);
LPVOID CoTaskMemRealloc (LPVOID pv, ULONG cb);
[1] 准确地说,数组元素所占内存的字节数和编译时的对齐参数有关。
[2] 复制范围还大量使用在自定义类型中的数组成员,这已经超出了本文的范围,请大家参考相关资料。