活到老学到老

现学现卖

博客园 首页 新随笔 联系 订阅 管理

 

原文:http://blog.csdn.net/autoair/archive/2005/09/07/474068.aspx

 

我们将采用自底向上的方式来设计接口。 我们首先实现接口方法,然后再实现接口,最后再编写库的元信息。

为了考察MIDL中的各种概念及相关问题,我定义了一个名为“IMyExplore”的接口以作参考;实现此接口的组件被称为“CMyExplore”。

 

定义接口方法

接口方法在接口的作用域内定义。以下是一个具有一个名为“GeTRandomValue”的方法的接口:

 

interface IMyExplore : IUnknown

{

 [helpstring("Obtain a random value")]

 HRESULT GetRandomValue([out] long* pVal);

};

 

一个接口方法由下列元素构成:

一个返回值: 一般是HRESULT类型;

一个名称: 任何有效的C/C++标识符;

参数列表: 方法可以具有0个或多个参数,每个参数可以有自己的特性(attributes)列表。

另外每个方法可以可选地有自己的特性(attributes),例如最常见的helpstring, 用于为接口提供一个说明。

       下列代码展示了GetrandomValue方法的实现和在客户端的用法:

 

// Server side implementation

STDMETHODIMP CMyExplore::GetRandomValue(long *pVal)

{

 *pVal = rand();

 

 return S_OK;

}

 

// Client side usage

long lVal;

HRESULT hr = pMyExplore->GetRandomValue(&lVal);

 

参数方向特性

要封送(marshaling)方法实参,负责封送的代码(通常是自动生成的)必须知道这些参数的语义,即如何在方法调用时和调用结束时将数据在客户端和服务器端传输。MIDL规定了三种可能的方向:

[in] 说明参数所包含的数据需要从客户发送到服务器;

[out] 说明服务器通过设置此参数的内容来向客户发送数据;

[in, out] 数据通过此参数双向传输。

 

MIDL方法签名具有C风格的按址传递语义(被传输的始终是值),因此所有具有[out] 属性的参数必须是指针类型的。服务器通过修改指针指向的值来返回数据。

下面是方向特性的一个例子:

 

// Interface method definition

HRESULT DirectionDemo(

 [in] long val1,

 [out] long* pVal2,

 [in, out] long* pVal3,

 [out] long* pVal4,

 [in] long val5);

 

// Server implementation

STDMETHODIMP CMyExplore::DirectionDemo(long val1, long *pVal2,

 long *pVal3, long *pVal4, long val5)

{

 *pVal2 = val1 + 100;

 

 *pVal3 = *pVal3 + 200;

 

 *pVal4 = val5 + 300;

 

 return S_OK;

}

 

// Client implementation

void DirectionDemo(IMyExplore* pMyExplore)

{

 cout << "\n\nDirection demo test" << endl;

 

 long val1 = 1, val2 = 2, val3 = 3, val4 = 4, val5 = 5;

 cout << "Before: " << val1 << ", " << val2 << ", "

    << val3 << ", "

    << val4 << ", "

    << val5 << endl;

 HRESULT hr = pMyExplore->DirectionDemo(val1, &val2, &val3,

    &val4, val5);

 if (FAILED(hr)) {

    DumpError(hr);

    return;

 }

 cout << "After: " << val1 << ", " << val2 << ", "

    << val3 << ", "

    << val4 << ", "

    << val5 << endl;

}

 

// Output

Direction demo test

Before: 1, 2, 3, 4, 5

After: 1, 101, 203, 305, 5

 

除了标示参数传递方向,方向特性还说明了内存管理的规则:

      [in] 型参数的内存由客户分配和释放;

[out] 型参数的内存由服务器分配但是由客户释放;

      [in, out] 是以上两者的结合,输入的指针由客户管理但服务器可能分配新的内存;

 

内存分配主要通过COM's memory management APIs进行。封送机制会在必要的时候调用这些API。并不是所有参数都需要和内存管理打交道,但我们会在后面介绍数组和字符串的时候讨论它。

 

逻辑返回值

我们前面有一个返回随机值的接口方法:

 

long lVal;

HRESULT hr = pMyExplore->GetRandomValue(&lVal);

 

这个方法返回两个值,一个实际返回值hr和一个long类型的逻辑返回值。为了说明一个参数是方法的逻辑返回值, MIDL 支持 [retval]参数特性:

 

HRESULT GetRandomValue([out, retval] long* pVal);

 

在支持逻辑返回值的客户端,[retval]说明了当前参数应当被映射为操作的结果。例如VB将上面的方法映射成:

 

Function GetRandomValue() as long

 

Visual C++中的原生COM支持也可以进行这样的映射:

 

long lVal= pMyExplore->GetRandomValue();

 

自然一个方法至多只能由一个逻辑返回值,而且必须被标识成[out]

 

基本数据类型

MIDL具有一套基本数据类型,如下表:()

参数可以被指定为这些基本类型或者指向这些基本类型的指针。MIDL对这些类型具有原生支持,只需要指明传递方向特性即可自动完成参数封送。

 

C++不同,指向基本类型的指针并不被认为是一个数组而仅是一个值。必须借助其它特性才能指定数组参数。MIDL还允许使用所有整型数据的有符号和无符号版本以及指向这些类型的指针。

基本类型可以用来定义C风格的结构和联合,这些新类型称为扩展类型。

 

字符串参数

COM领域有许多种可用的字符串类型。Visual C++ 原生支持char*wchar_t*字符串类型。Win32 API 提供了对应的LPSTRLPWSTR,以及通用宏LPTSTRCOM SDK提供了通用的OLECHAR* OLESTR类型。MIDL支持被称为BSTR的扩展类型,以处理字符串。其它还有ATLC++CComBSTR Visual C++支持的COM_bstr_t。下面让我们来看看这些类型。

字符串通常用null结尾的字符数组表示,在Win32 SDK 用宏LPSTR来进行定义。每字符一字节对于美国英语是够用的,但对其他语言未必如此。因此一些语言不得不使用变长字符方案以适应LPST R。但程序员们更倾向于定长字符方案。一个可行的方案是每字符用2个字节表示,例如UTF-16。由此可以处理世界上大多数的语言。

微软的运行库定义了wchar_t 来表示UNICODE字符,而Win32 SDKWCHAR宏表示。为了使源代码同时支持UNICODE ANSI, SDK提供了两个宏,TCHAR LPTSTR。他们根据宏定义来决定是映射到单字节字符还是双字节字符。

现在看看COM中的字符表示。既然字符串就是null结尾的字符数组, 那么就很容易产生如下可以传递一个字符串的错觉;

 

HRESULT StringParam([in] char* pszString);

 

根据MIDL的语义, 指针总被认为是指向单一元素而不是数组。因此以上方法中只有串中的第一个字符被封送。为此,MIDL引入了[string]特性来说明传递的是一个字符串

 

HRESULT StringParam([in, string] char* pszString)

 

一般程序员会更倾向于用双字节字符表示字符串。由于wchar_t是微软平台专有的, COM SDK定义了OLECHAR类型来表示UNICODE字符. Win32平台下这其实就是wchar_t的另一个名称:

 

HRESULT StringParam([in, string] OLECHAR* pwszString)

 

上述MIDL语句说明pwszString是一个null结尾的UNICODE字符串。下面是该方法的实现即在WIN32平台下的用法:

 

STDMETHODIMP CMyExplore::StringParam(OLECHAR *pwszName)

{

 printf("String is %S\n", pwszName);

 return S_OK;

}

 

//using Visual C++:

wchar_t* pwszName = L"Alexander, the Great";

HRESULT hr = pMyExplore->StringParam(pwszName);

 

BSTR类型字符串则有点不同,暂时不讨论。

 

Enumeration Data Type

MIDL支持C风格的枚举,枚举类型可以被用作方法参数:

 

enum MYCOLOR {MYRED, MYGREEN, MYBLUE};

 

// Interface method definition

HRESULT GetEnum([out] enum MYCOLOR* pVal);

 

// Server code

STDMETHODIMP CMyExplore::GetEnum(enum MYCOLOR *pVal)

{

 *pVal = MYRED;

 return S_OK;

}

 

// Client code

enum MYCOLOR color;

HRESULT hr = pMyExplore->GetEnum(&color);

 

或者:

 

typedef enum { MYRED, MYBLUE, MYGREEN } MYCOLOR;

 

// Interface method definition

HRESULT GetEnum([out] MYCOLOR* pVal);

// Server code

STDMETHODIMP CMyExplore::GetEnum(MYCOLOR *pVal)

{

 *pVal = MYRED;

 return S_OK;

}

 

// Client code

MYCOLOR color;

HRESULT hr = pMyExplore->GetEnum(&color);

 

注意枚举类型的变量只能被赋予枚举值中的一个,否则将导致封送失败。可以使用long类型来实现标志位。可以给枚举类型制定一个唯一标识:

 

typedef

[

 uuid(2B930581-0C8D-11D3-9B66-0080C8E11F14),

] enum {MYRED, MYGREEN, MYBLUE } MYCOLOR;

 

枚举默认用16-bit unsigned short表示。为了提高速度,可以定义v1_enum特性定义32位枚举:

 

typedef

[

 v1_enum,

 uuid(2B930581-0C8D-11D3-9B66-0080C8E11F14),

] enum {MYRED, MYGREEN, MYBLUE } MYCOLOR;

 

枚举和枚举常量都可以使用 "helpstring" 特性:

 

typedef

[

 v1_enum,

 uuid(2B930581-0C8D-11D3-9B66-0080C8E11F14),

 helpstring("This is my color enumeration")

] enum {

 [helpstring("This is my red")] MYRED,

 [helpstring("This is my green")] MYGREEN,

 [helpstring("This is my blue")] MYBLUE

}MYCOLOR;

 

默认情况下枚举值从0开始,但也可以单独指定值:

 

typedef

[

 v1_enum,

 uuid(2B930581-0C8D-11D3-9B66-0080C8E11F14),

 helpstring("This is my color enumeration")

] enum {

 [helpstring("This is my red")] MYRED = 0x0001,

 [helpstring("This is my green")] MYGREEN = 0x0002,

 [helpstring("This is my blue")] MYBLUE = 0x0004

}MYCOLOR;

 

结构

MIDL中的结构几乎和C的一抹一样,但有以下不同:

       字段上可以声明特性;

能被封送的结构不能有位域和成员函数。

 

// Interface Definiton

typedef struct tagMYPOINT

{

 long lX;

 long lY;

}MYPOINT;

HRESULT StructDemo([out, retval] MYPOINT* pVal);

 

// Server side code

STDMETHODIMP CMyExplore::StructDemo(MYPOINT *pVal)

{

 pVal->lX = 10;

 pVal->lY = 20;

 

 return S_OK;

}

 

// Client side code

MYPOINT pt;

HRESULT hr = pMyExplore->StructDemo(&pt);

 

联合(略)

 

数组

MIDL中数组也可以做参数传递。数组可以使用指针语法或者数组语法表示。数组参数必须使用相关特性标识。这样的参数也叫做有大小的指针。

       如果数组的大小在定义接口时就可以确定,则可以使用定长数组语法:

 

// Interface method definition

HRESULT SimpleArrayDemoIn([in] long alVal[100]);

 

// Server side code

STDMETHODIMP CMyExplore::SimpleArrayDemoIn(long alVal[])

{

 long lLastVal = alVal[99];    // Get data from the last element

                                // of the array

 return S_OK;

}

 

// Client side code

long alVal[100];

alVal[99] = 25;    // Set data for the last element of the array

pMyExplore->SimpleArrayDemoIn(alVal);

如果不能确定数组长度这只能使用变长数组,MIDL为此提供了size_is 特性:

 

HRESULT ConformantArrayIn([in] long lCount,

 [in, size_is(lCount)] long aVal[];

HRESULT ConformantArrayIn([in] long lCount,

 [in, size_is(lCount)] long aVal[*];

HRESULT ConformantArrayIn([in] long lCount,

 [in, size_is(lCount)] long* aVal;

 

// Server side code

STDMETHODIMP CMyExplore::ConformantArrayIn(long lCount,

 long alVal[])

{

    long lFirstVal = alVal[0];             // Get data from the first

                                           // element in the array

    long lLastVal = alVal[lCount - 1];     // Get data from the last

                                           // element in the array

    return S_OK;

}

 

// Client side code

long alVal[100];

alVal[0] = 50;        // Set data for the first element in the array

alVal[99] = 25;       // Set data for the last element in the array

pMyExplore-> ConformantArrayIn(100, alVal);

 

max_issize_is的另一种形式,它给出的是数组最大下标值而不是数组长度:

 

// interface definition

HRESULT ConformantArrayIn2([in] long lCount,

 [in, max_is(lCount)] long aVal[];

 

// Server side code

STDMETHODIMP CMyExplore::ConformantArrayIn(long lCount,

long alVal[])

{

 long lFirstVal = alVal[0];

 long lLastVal = alVal[lCount];      // Get data from the last

                                      // element in the array

return S_OK;

}

 

// Client side code

long alVal[100];

alVal[0] = 50; // Set data for the first element in the array

alVal[99] = 25; // Set data for the last element in the array

pMyExplore-> ConformantArrayIn2(99, alVal);

 

如同定长数组,变长数组(conformant arrays)也可以很有效率的封送。服务器接收到的副本可以直接作为参数传递给方法实现。

前面的例子演示了如何从客户向服务器传送数据。变长数组也可以用来接收来自服务器的数据。客户虚拟地发送一个空数组到服务器,服务器填充之并发回。例如:

 

// interface method

HRESULT ConformantArrayOut([in] long lCount, [out, size_is(lCount)] long alVal[]);

 

// Server side

STDMETHODIMP CMyExplore::ConformantArrayOut(long lCount, long alVal[])

{

 alVal[0] = 25;                 // Set data for the first element

                                 // in the array

 alVal[lCount-1] = 50;          // Set data for the last element

                                 // in the array

 

 return S_OK;

}

// Client side

long alVal[100];

pMyExplore->ConformantArrayOut(100, alVal);

// dump the first and the last element of the array

cout << alVal[0] << ", " << alVal[99] << endl;

 

由于指定了[out],数组的内容不必从客户发送给服务器。如果服务器初始化整个数组,那么这是由效率的。但如果服务器只初始化数组的一部分呢?

考虑下列场景:一个学校的校长试图通过一个数组获取学生的分数:

 

HRESULT GetGrades1([in] long lSize,

 [out, size_is(lSize)] long alGrades[]);

 

假设有25个学生,校长如下调用此方法:

 

long alGrades[25];                    // A max of 25 students in the class

for(long i=0; i<25; i++){ alGrades[i] = 0;}   // initialize the array to 0

pMyExplore->GetGrades1(25, alGrades);                   // make the call

for(i=0; i<25; i++) {cout << alGrades[i] << endl;}      // dump the grades

 

如果学生数不到25呢?一个简单的办法是再加一个输出参数返回世纪的学生数目:

 

HRESULT GetGrades2([in] long lSize, [out] long* plActual, [out, size_is(lSize)] long alGrades[]);

 

客户端需要稍作修改来处理实际的学生数:

 

long lActual = 0;

pMyExplore->GetGrades2(25, &lActual, alGrades);

for(i=0; i<lActual; i++) { cout << alGrades[i] << endl;

}

 

这能解决问题,但是没有效率。封送器不知道知需要传送前五个元素,因此它仍然回传了整个数组。

 

我们需要告诉封送器只有一部分数组元素需要被发送。这可以通过使用length_is特性来完成:

 

HRESULT GetGrades3([in] long lSize,  [out] long* plActual, [out, size_is(lSize), length_is(*plActual)] long alGrades[]);

 

客户和以前一样来使用这个方法。不同的是现在封送器有足够的信息来确定只有前*plActual个元素需要被返回。接收到这些元素后,客户端代理会将剩余元素初始化为0

 

MIDL还支持另一种方式来指定数组的实际长度:last_is,表示组后一个需要回传的元素下标。如同size_ismax_is,如果起始下标为0则有: length_is() == last_is()+1

 

指定了length_is或者last_is的数组被称为部分数组(varying array),用length_is指定的值被称为部分长度(variance)。部分长度说明了数组内容的范围,而不是整个数组的长度。同时指定数组长度和部分长度的数组被称为开放数组(open array)

 

改变数组下标。我们定义上个方法时,序列化器假定数组内容是从第一个元素开始的。MIDL允许通过first_is特性指定一个新的起始元素:

 

HRESULT GetGrades4([in] long lSize,

 [out] long* plActual, [out] long* plFirst,

 [out, size_is(lSize), length_is(*plActual),

    first_is(*plFirst)] long alGrades[]);

 

//client code

long lActual = 0; long lFirst = 0;

pMyExplore->GetGrades4(25, &lActual, &lFirst, alGrades);

cout << "First is: " << lFirst << endl;

for(i=0; i<lActual; i++) { cout << alGrades[lFirst+i] << endl; } // Dump grades starting from lFirst

 

最后考虑一个特殊的情况。校长担心太多的学生不及格,出于慷慨,他决定给大家加几分。加分通过最后一个数组元素alVal[24]来表示。情况总结如下:

调用时:

数组大小为25

       内容长度为1

       起始元素为24

       返回时:

       数组长度为25//由客户决定,服务器无法改变

内容长度是5

       首元素索引为10

为了发挥最大效率,方法接口应如下定义:

 

HRESULT GetGrades5([in] long lSize,

 [in, out] long* plActual, [in, out] long* plFirst,

 [in, out, size_is(lSize),

 length_is(*plActual), first_is(*plFirst)] long alGrades[]);

 

重分配数组。前面例子中数组的大小都是由客户分配。这样客户不得不假定一个数组最大的大小。但是如果数组的长度不够呢?解决这个问题的一个方案是定义一个方法返回服务器端建议的数组大小,然后客户再分配这么大一个数组发给服务器。但这样会产生两次请求,而且所需要的数组大小可能会在两次请求间发生变化。如果你是一个C程序员,你的第一反应可能是让组件方法修改数组的大小,可以重定义接口如下:

 

HRESULT GetGrades6([out] long* plSize,

 [out, size_is(*plSize)] long alGrades[]);

 

但实际上数组的大小需要在封送的时候确定,因此服务器仍然无法改变其大小,只能修改其内容。上面的定义会产生一个MIDL编译错误,指出数组大小不能指定为一个输出参数。那么怎样才能修改数组的大小?实际上没法修改,至少不能直接修改。我们可以在服务器端创建一个新的数组返回给客户。这样的话方法定义必须改成返回一个数组:

 

HRESULT GetGrades6([out] long* plCount,

 [out, size_is(,*plCount)] long** palVal);

 

要特别注意size_is 特性参数列表中的逗号。size_is接受长度可变的、用逗号分隔的若干个参数,每个参数对应一个间接引用级别。如果一个参数被省略,对应的指针将被认为是指向单一值而不是数组。本例中,palVal是一个数组指针,其大小将由服务器方法指定:

 

STDMETHODIMP CMyExplore::GetGrades6(long *plCount, long **palVal)

{

 long lCount = 10;

 

 // allocate enough memory

 long* alVal = (long*) CoTaskMemAlloc(lCount * sizeof(long));

 for (int i=0;i<lCount; i++) {

    alVal[i] = i + 15;           // Set each value

 }

 *plCount = lCount;             // Set the count

 *palVal = alVal;               // Set the return array

 

 return S_OK;

}

 

//client

long* palGrades;

long lCount = 0;

pMyExplore->GetGrades6(&lCount, &palGrades);

for(long i=0; i<lCount; i++)

 { cout << palGrades[i] << endl; } // dump grades

CoTaskMemFree(palGrades);            // free server-allocated memory

 

这样通过一个额外的间接引用,客户就无需再指定数组的大小。

 

指针

C类似,MIDL指针指向内存中的一个位置:

 

[in] long* plVal;       // passing a long value as a pointer

 

[out] long** pplVal;    // obtaining one element of long* type

 

[out] BSTR* pbsVal;     // obtaining one element of BSTR type

 

但不同的是MIDL中的指针默认只指向单一的值,而不是一片不知道在哪里结束的内存空间。在COM中调用方和被调用方运行在不同的进程中。不同进程中的同一内存位置可能包含不同的数据。因此在进程间传递指针是没有意义的,可能造成访问违例。COM的解决方案很简单,在有指针的情况下传递指针指向的内容。接收方进程再将这块内容的指针传递给方法。

当指针用作方法参数的时候,封送器会确保指针指向的数据被传输出去,无论指向的数据的结构如何复杂。如果指针指向的数据结构本身也包含指针,那么这些指针指向的数据也会被收集并传输。这个过程会递归的进行下去,直到所有的非空指针都处理完毕。

这一机制产生了一些新的问题。为了分析方便,我们假设客户和服务器运行于不同的进程。考虑下面的MIDL方法定义:

 

HRESULT SetValue([in] long* pValue);

 

如果客户如下调用此方法:

 

long lVal = 100;

p->SetValue(&lVal);

 

代理必须保证100这个值被发送给Server,这就要求代理对这个指针进行解引用。

       如果客户这样调用方法就会导致代理产生错误:

 

p->SetValue(0);

 

代理和存根当然能够检测指针是否为null,但是是否总是需要对每个指针都执行这种检查呢?如果null是一个合法值又怎么办?很明显有些指针包含了不能为null的语义,另一些则可以为null。这些是由接口的设计者来决定的。为了说明一个指针不可能为null,需要用ref特性来标识它:

 

HRESULT SetValue([in, ref] long* pValue);

 

如果调用方误将null传递给参数,代理将返回错误: RPC_X_NULL_REF_POINTER (0x800706f4). MIDL生成的代理存根代码假定ref标识的指针不为null 因此它们跳过检查直接对指针解引用,从而导致内存访问违例。由于生成的存根和代理总是在异常处理块中执行代码,这一异常将被转换成错误码。

如果一个指针可以为null,则用[unique]来标识。此时代理会检查指针是否为null,还会在RPC包中插入一个标记指示传递的是否是null值。存根检测这个标记,如果传过来的是null,则重新构造一个null指针。

可以用point_default特性来指定作用域中的指针的默认类型。MIDL把指针分为两种,一种是命名方法参数指针,也被称为是顶级指针;其他的称为嵌套指针。point_default只能指定嵌套指针的默认值。考虑下面接口方法定义:

 

typedef struct tagMYSTRUCT {

 long lVal1;

 long* plVal2;

}MYSTRUCT;

HRESULT MyMethod([in] long* plVal3,

 [in, out] long** pplVal4, [out] MYSTRUCT* pVal5);

 

plVal3pVal5是顶级指针,而*pplVal4 pVal5->plVal2 是嵌套指针。MIDL采用下面的逻辑来决定指针的默认类型:

       顶级指针是ref型;

顶级输出指针只能是ref型,这是因为如果顶级输出指针为null,即意味着客户没有为将要输出的值分配内存空间;

       所有嵌套指针默认为unique

 

下面我们看一个长整形单链表作参数的例子:

 

typedef struct tagMYLONGLIST {

 long lVal;

 struct tagMYLONGLIST* pNext;

}MYLONGLIST;

 

定义中的嵌套指针pNext的类型为unique。对于这样递归的类型定义,MIDL不允许使用ref作为指针的类型。接口方法可以定义为:

 

HRESULT MyLinkList([in] MYLONGLIST* pList);

 

这也相当于:

 

HRESULT MyLinkList([in, ref] MYLONGLIST* pList);

 

如果可以传递null的话定义可以改为:

 

HRESULT MyLinkList([in, unique] MYLONGLIST* pList);

 

下面是服务器端代码和客户代码:

 

// Server-side

STDMETHODIMP CMyExplore::MyLinkList(MYLONGLIST *pList)

{

 long l;

 while(NULL != pList) {

    l = pList->lVal;

    pList = pList->pNext;

 }

 

 return S_OK;

}

 

// Client-side

MYLONGLIST lastItem;

lastItem.lVal = 100;

lastItem.pNext = NULL;

MYLONGLIST firstItem;

firstItem.lVal = 200;

firstItem.pNext = &lastItem;

 

HRESULT hr = pMyExplore->MyLinkList(&firstItem);

 

这会时整个链表被传递出去。如果传递的是循环链表又会怎样?尝试运行下面的例子:

...

firstItem.lVal = 200;

firstItem.pNext = &lastItem;

 

lastItem.pNext = &firstItem;

HRESULT hr = pMyExplore->MyLinkList(&firstItem);

 

结果是调用在代理中就失败了,根本就没有发送到服务器。根据方法返回的结果,可以知道试图传递有环路的数据结构会使代理发生堆栈溢出。因为代理递归地解析所有指针,但是却无法获得标志递归结束的null值。由于代理用了异常处理,所以程序没有崩溃,但方法调用失败了。如果代理能够识别已经解析过的指针,就可以避免这种情况的发生。怎样才能做到这一点呢?

侦测重复指针。考虑下面的接口方法和相应的客户代码:

 

// Interface method

HRESULT MyMethod([in] long* pl1, [in] long* pl2);

 

// Client code

long lVal = 10;

p->MyMethod(&lVal, &lVal);

 

在这个例子中,我们给两个参数传递了同一指针。代理会如何处理这种情况呢?它不会进行特殊处理,而将pl1pl2当作两个独立的指针。因此不仅10这个值被传递了两次,服务器端方法还会接受到两个不同的指针值:

 

STDMETHODIMP CMyExplore::MyMethod(long* pl1, long* pl2)

{

 if (pl1 == pl2) {

    ...

 }

}

 

       为了解决这个问题,MIDL设计了ptr属性:

 

HRESULT MyMethod([in, ptr] long* pl1, [in, ptr] long* pl2);

 

指定此属性会使封送器做额外的检查以识别相同的指针。被多个指针指向的值只会被传递一次。存根会重建所有指针,如果传递前它们指向同一位置,传递后仍然指向同一位置。用ptr标识的指针被称为全指针,它也可以有null值。

需要使用全指针的情况并不多见。但全指针对于传递循环链表这样的数据结构至关重要。这样前面的对栈溢出错误可以这样解决:

 

// Interface definition

typedef struct tagMYCIRCULARLIST {

 long lVal;

 [ptr] struct tagMYCIRCULARLIST* pNext;

}MYCIRCULARLIST;

HRESULT MyCircularList([in, ptr] MYCIRCULARLIST* pList);

// Server side code

STDMETHODIMP CMyExplore::MyCircularList(MYCIRCULARLIST *pList)

{

 if (NULL == pList) {

    return S_OK;

 }

 

 long l;

 MYCIRCULARLIST* pFirst = pList;

 do {

    l = pList->lVal;

    pList = pList->pNext;

 }while(pList != pFirst);

 return S_OK;

}

 

// Client side code

MYCIRCULARLIST lastItem;

lastItem.lVal = 100;

lastItem.pNext = NULL;

 

MYCIRCULARLIST firstItem;

firstItem.lVal = 200;

firstItem.pNext = &lastItem;

 

lastItem.pNext = &firstItem;

 

HRESULT hr = pMyExplore->MyCircularList(&firstItem);

 

在上面的代码中,ptr被使用了两次。用在pNext上显然是必须的,但为什么在方法参数上也需要? 这是因为如果参数没有被声明为ptr,那么参数中的指针就不回被放到已访问指针列表中。这样firstItem.pNext就不知道firstItem已经被打包了,于是重新打包一个firstItem,这个firstItempNext能够发现自己指向的地址已经被打包,从而结束打包的过程。结果发送的数据将是firstItem->firstItem’->firstItem’

ref, uniqueptr 是互斥的。

 

接口指针

前一节我们学习了封送数据指针。COM IDL具有对封送接口指针的原生支持。要传递一个接口IFoo作为输入参数,可以简单的把它声明为[in] IFoo*

 

Interface IBar : IUnknown

{

 HRESULT MyMethod([in] IFoo* pIFoo);

}

 

如果客户和服务器处于不同的上下文中,上面的方法将将为客户加载IFoo的存根,并在服务器端加载IFoo的代理。

刚开始看起来在客户端加载存根、服务器端加载代理可能有点奇怪。关键在于记住代理的任务是打包接口方法的参数数据,而存根则是还原这些数据。在我们的例子中,接口指针被交给服务器也就意味着服务器可能会调用接口上的方法。这样开来,服务器就成了客户,而客户反而成了服务器。更精确地说,在我们的例子中,客户持有IBar接口的代理以及IFoo接口的存根,而服务器持有IBar接口的存根和IFoo接口的代理。

 

一个作为输入参数的接口指针总是unique类型,也就是说这样的指针可以具有null值,并且不能有别名。要获取IFoo的接口指针, 必须使用 IFoo**类型的输出参数:

 

HRESULT MyMethod([out] IFoo** ppIFoo);

 

这样IFoo的代理会在客户段创建,其存根会在服务器端创建。这是在COM中最常见的情况。

歧义接口。有的时候一个接口指针被作为其基接口指针来传递。考虑这个例子,接口IBar继承自接口IFoo,服务器已指向IFoo接口指针的形式返回一个指向IBar接口的指针:

//

// IDL definition

//

interface IBar : IFoo

{

 ...

}

HRESULT MyMethod([out] IFoo** ppIFoo)

 

//

// Server implementation

//

class CBar : public IBar

{

 ...

}

 

STDMETHODIMP CMyServer::MyMethod(IFoo** ppIFoo)

{

 CBar* pBar = CreateBar();

 *ppIFoo = pBar;

 ...

}

 

当客户调用MyMethod方法时,封送器并不知道应当加载IBar接口的代理存根而不是IFoo接口的。如果客户将返回的指针作为IBar类型,那么程序的运行将不可预期。在这种情况下,为了使封送器知道实际被传递的接口类型,我们需要向其提供实际接口的标识符。MIDL通过iid_is特性来实现这一点:

 

HRESULT MyMethod([in] REFIID riid,

 [out, iid_is(riid)] IFoo** ppIFoo)

 

这里客户显式通知封送器返回接口是riid类型,以便加载正确的代理/存根。

 

定义MIDL接口

一个COM接口暴露一组相关函数,类似于C++的类定义。下面是一个例子:

 

[

 object,

 uuid(5E7A5F3E-F4F4-11D2-9B37-0080C8E11F14),

 helpstring("This Interface can get and set color of the object"),

 pointer_default(unique)

]

interface IFoo : IUnknown

{

 typedef enum { MYRED, MYGREEN, MYBLUE} MYCOLOR;

 

 HRESULT SetColor([in] MYCOLOR val);

 HRESULT GetColor([out] MYCOLOR* pVal);

};

 

接口的定义通过interface关键字标识,后跟接口的逻辑名称IFoo。所有COM接口,除了IUnknown接口,都从另一个接口派生。

注意MIDL不支持多继承;也就是一个接口不能从一个以上的接口派生。但是组件可以同时实现多个接口,只要相应的编程语言支持。接口定义的主体部分只是一个方法定义和相关类型的集合。接口中的元素都是public的。

 

接口特性

可以在中括号中为接口定义一组特性,特性间用逗号分隔。每一个COM接口要求至少两个MIDL特性,[object][uuid()][object]标识接口是一个COM接口,没有此标识接口将被看作是一个DCE RPC接口。所以定义COM接口的时候千万不要忘了[object]

      另一个必须的特性是[uuid],用来指定一个GUID来唯一标识接口。接口的GUID也被称为IID,即接口ID。例如:

 

uuid(5E7A5F3E-F4F4-11D2-9B37-0080C8E11F14),

 

MIDL编译器处理接口时会为其指定一个C风格的符号名来表示此GUID IID_<interfacename>。对于IFoo接口,这个符号名是IID_IFoo

 

MIDL提供了一个接口特性pointer_default来指定其范围内的嵌套指针的默认类型:

 

pointer_default(unique)

 

还可以为接口添加注释,以使程序员明白这个接口的作用。通过helpstring特性来完成这一点:

 

helpstring("This Interface can get and set color of the object"),

 

local特性通知MIDL不要为当前接口产生任何封送代码并且允许接口方法返回HRESULT之外的值。

 

定义COM

COM类定义是可实例化的具体类型所实现的接口的命名列表,通常出现在库定义的作用域中:

 

[

 uuid(5E7A5F40-F4F4-11D2-9B37-0080C8E11F14),

 helpstring("My Foo Class")

]

coclass MyFoo

{

 interface IFoo;

};

 

COM类的定义用coclass关键字标识,后跟类的逻辑名称。主体部分是其可以暴露的接口的列表,每个接口用interface dispinterface关键字标识。coclass声明暴露的是一个潜在的接口列表,不是实际的接口列表。COM对象实际实现的接口可能比这多或者笔者少,也可能完全不同。这里列出的是COM对象可能支持的接口列表,客户不能依据此信息判断COM对象上的接口信息,而是必须使用QueryInterface来查询。

如同接口一样,一个COM类要求一个唯一标识,这通过uuid关键字来实现。MIDL 编译器处理COM类定义的时候会为其给出一个C风格符号名称来表示此唯一标识,例如CLSID_<coclassname> 对于MyFoo此符号名是CLSID_MyFoo

 

定义MIDL

定义库以在编译时生成类型库,下面是一个例子:

 

[

 uuid(5E7A5F3F-F4F4-11D2-9B37-0080C8E11F14),

 version(1.0),

 helpstring("My Foo 1.0 Type Library")

]

library MyFooLib

{

 importlib("stdole32.tlb");

 importlib("stdole2.tlb");

 

 [

    uuid(5E7A5F40-F4F4-11D2-9B37-0080C8E11F14),

    helpstring("My Foo Class")

 ]

 coclass MyFoo

 {

    [default] interface IFoo;

 };

};

 

使用library关键字来定义MIDL库,后跟库的逻辑名称。库的主体部分包括其他库的引用、接口定义和用户数据类型。另外还可以定义任意多个COM类。

 

移除冗余

任何在类型库中被引用的接口定义和数据类型定义会被包含在类型库中,即使他们中的一些已经被保存在了其他的类型库里。例如,看一下MyFooLib的库定义,它引用了IFooIFoo又引用了IUnknown…。像IUnkownGUID这样的定义几乎出现在每个IDL文件中,如果在每个类型库中都保存同样的信息那就太浪费了。应当为这些定义单独创建一个类型库。实际上,SDK已经包括了一个包含所有COM系统接口和数据定义的类型库,最新版本是stdole2.tlb。另外一个标准类型库stdole32.tlb包含了自动化接口。要排除已经出现在另一个类型库中的定义,可以使用importlib关键字,此特性以类型库名称作为关键字:

 

importlib("stdole2.tlb");

 

IDL编译期间,MIDL保证任何能在指定类型库中找到的定义不会被保存到生成的类型库中,因而缩小了类型库文件的大小。

 

 

ps

我一开始以为原文由csdn上的牛人撰写,后来发现实际上好像是某本书的一章。希望知情的好心人告知这本书的名字,谢谢!

posted on 2010-02-06 20:06  John Rambo  阅读(5474)  评论(1编辑  收藏  举报