热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

c6011取消对null指针的引用_COM编程攻略(二十二IDL中的枚举,指针,数组)

上一篇:Froser:COM编程攻略(二十一异步)​zhuanlan.zhihu.com本篇主要讲idl的一些语法特性。id

上一篇:

Froser:COM编程攻略(二十一 异步)​zhuanlan.zhihu.com
8785fabf36d56a1684bf5ebe74c39777.png

9f40d235cc549a062e9c363fcc07e02e.png
常见基本类型

三、指向性属性

假设我们有一个这样的接口,并且它是一个跨进程的服务(注意:本篇文章中的例子都是需要通过代理的例子,而不是处于同一个套间可以直接调用的例子):

interface IMessage : IUnknown
{HRESULT AddOne(int*);
};

先看第一个问题:从上面的idl上来看,我们知道AddOne接受一个int指针。如果AddOne和客户端在同一个进程,那么没有关系,我们可以通过int指针来访问整型。但是,如果AddOne是实现在了一个远程计算机或者另外一个进程,那么我们客户端传递int指针那就不起作用了,因为它们不能共享内存。因此,在这种情况下的指针传递,实际上是对其值的传递,我们需要解引用这个int指针,然后再传递给服务端。服务端拿到这个int指针后,将开辟一个int大小的空间进行操作——这类似于深拷贝。

第二个问题是,我们不知道服务端如何使用这个指针。在C编程中,我们通过传递指针到另外一个函数,例如传递int*,非常有可能的情况是这个值会在函数中被改变;如果我们传递const int*,那么它仅仅是作为一个参数来防止拷贝。

这样的问题,在COM的远程调用中表现得尤为明显。如果这个int是会被修改的,那么客户端列集之后发送给服务端,服务端需要在修改完毕后,重现发回客户端。但是,如果这个int*不会被修改,那么客户端发送给服务端后,服务端便不需要返回它了,从而节约带宽,提高效率。

如何知道这个参数是否需要服务端返回或者需要发送给服务器?idl为了解决这个问题,提供了[in][out]属性。

一般来说,[in]表示入参,一个参数被标记为[in]之后,COM不会讲其从服务端返回:

57fb391efd0d00716fc8dc0342f52c4b.png

[out]表示出参,这个参数结果由服务端生成:

1e9974f1afe9aabf95184ff9221b6d4d.png

如果不指定,那么它默认就是[in, out],表示它会被服务端来回传递:

3728b2aff6fed220ddfa8b96139c3764.png

下面是一个具体的例子。假设我们的AddOne的实现是这样的:

virtual HRESULT __stdcall AddOne(int* in) override
{*in = *in + 1;return S_OK;
}

它接受来自客户端的指针,并且在原有基础上+1,然后返回给客户端。客户端测试代码如下:

int main()
{CoInitialize(NULL);{CComPtr pMsg;HRESULT hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_LOCAL_SERVER, IID_IMessage, (void**)&pMsg);int i = 5;hr = pMsg->AddOne(&i);}CoUninitialize();
}

情况1: 在idl中,参数被标记为[in]:

interface IMessage : IUnknown
{HRESULT AddOne([in] int*);
};

我们跑出来的结果是i=5,原因是[in]只是把指针解引用后传递给了服务端,尽管服务端+1,但是它并不会传回给客户端。

情况2: 在idl中,参数被标记为[out]:

interface IMessage : IUnknown
{HRESULT AddOne([out] int*);
};

跑出来的结果是i=1,原因是我们的&i并不会传递给服务端,服务端参数中的i,解引用后得到0,然后加1返回给客户端。

情况3: 在idl中,参数被标记为[in, out],或者不指定

interface IMessage : IUnknown
{HRESULT AddOne([in, out] int*);
};

此时我们总算得到正确的结果了,参数先被传递到服务端stub,然后加一之后,再传回客户端,我们最终得到i=6。

因此,正确地区分[in],[out]至关重要,它决定代码的正确性,以及对网络带宽的优化。

四、指针

在COM中,由于要考虑远程调用的问题,指针总会带来“无尽的麻烦”,其根本原因是传递地址对于跨套间跨进程的服务来说是无效的——它们不在同一个进程地址空间内。

COM中,对指针的用途进行了详细的分类,对它们作用进行一些限制。这些限制,会体现在代理中——也就是它会影响传递给Stub的行为。

1. 带有引用语义的指针 (Reference pointer)

C++中的引用,其实相当于一个别名。它最重要的特性就是,它不能为空,且永远指向一个实体。在COM中,如果一个指针它指向了一个合法地址,并且它不会改变指向的地址(但是地址中的内容可能改变),那么它就是具有引用语义的,本质上和C++的引用一样。

这样的指针,idl中提供了属性[ref],表示某个指针是引用语义。

引用语义的特点是,它不能接受一个空指针,因为引用不得为空。假设上面的例子中的idl是这样的:

interface IMessage : IUnknown
{HRESULT AddOne([in, out, ref] int*);
};

我们的客户端代码是这样的:

int main()
{CoInitialize(NULL);{CComPtr pMsg;HRESULT hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_LOCAL_SERVER, IID_IMessage, (void**)&pMsg);int i = 5;hr = pMsg->AddOne(NULL); // 错误!hr=RPC_X_NULL_REF_POINTER,传递了空的索引指针 (A null reference pointer was passed to the stub)}CoUninitialize();
}

原因是我们的参数标记为了ref,但是我们传递了NULL,那么在列集的时候,它会检查这个指针是否为空,如果是则返回一个错误。

2. 单值指针 (Unique Pointer)

如果一个为一个指针传入NULL是合法行为,那么它就可以被称为单值指针。例如,某个函数接受一个参数NULL,表示取默认值,Win32 API就经常这么干。

这种情况,我们需要用[unique]属性标记出来,这样就可以向代理传递空指针了。

我们现在将idl改成如下:

interface IMessage : IUnknown
{HRESULT AddOne([in, unique] int*);
};

此时表明,客户端代码中AddOne(NULL)是合法的,这也意味着实现者必须要对参数进行判空。为了能让代理感知unique属性,你一定要注册自己的PS模块(https://stackoverflow.com/questions/17013719/right-way-to-pass-a-null-pointer-to-a-out-of-process-com-method-in-an-atl-projec/17118542),否则oleautomation是无法帮你感知unique的,你仍然会得到一个错误。

测试代码:

int main()
{CoInitialize(NULL);{CComPtr pMsg;HRESULT hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_LOCAL_SERVER, IID_IMessage, (void**)&pMsg);hr = pMsg->AddOne(NULL); // hr = S_OK}CoUninitialize();
}

3. 全指针 (Full pointer)

在说全指针之前,我们先看下面一个接口定义:

interface IMessage : IUnknown
{HRESULT Inc([in, out, ref] int* a, [in, out, ref] int* b);
};

Inc的实现是,将a和b自增1:

virtual HRESULT __stdcall Inc(int* a, int* b) override
{(*a)++;(*b)++;return S_OK;
}

测试代码:

int main()
{CoInitialize(NULL);{CComPtr pMsg;HRESULT hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_LOCAL_SERVER, IID_IMessage, (void**)&pMsg);int a = 0;hr = pMsg->Inc(&a, &a); // 传入相同地址,我们期望得到a=2}CoUninitialize();
}

我们给Inc传入的是同一个地址,那么问题来了,服务端的Inc实现中,a和b是指向同一个地址吗?答案是否定的。按照MSDN的说法,无论是unique还是ref,它不会对指针进行等价的判断,也就是说,服务端Inc的形参a和b,虽然内容都是由客户端传入,但是实际上它们指向两个地址,在网络中它们也会被传输2次,因此它们只是各自加1,所以我们最终的结果是a=1。

这显然与我们对指针的认识背道而驰,因为如果是同一个地址,那么它应该增加2次,最终a应该为2。其原因是unique和ref不会进行指针的判断,它们带有自己的语义。为了能表达出最原始的指针的语义,微软提供了[ptr]属性,表示一个最接近C语言指针语义的全指针

interface IMessage : IUnknown
{HRESULT Inc([in, out, ptr] int* a, [in, out, ptr] int* b);
};

使用以上[ptr]属性后,注册PS模块,然后运行上面的测试代码,我们发现服务端的Inc形参a和b都是指向同一个地址了,最终我们得到了a=2。

需要说明的是,[ptr]属性并不是微软建议我们首选的属性,毕竟传入相同的地址也是比较少见的。虽然[ptr]可以防止一个相同地址的数据多次传递,但是它需要PS模块的支持,实际上最常见的语义还是unique和ref。

4. 默认指针语义

我们在接口中使用pointer_default,可以定义未显示标识指针语义的指针的默认语义:

pointer_default attribute - Win32 apps​docs.microsoft.com
8785fabf36d56a1684bf5ebe74c39777.png

五、数组

1. 固定长度的数组

在idl中,我们简单地把固定的长度写在数组后面,则规定了数组的长度:

interface IMessage : IUnknown
{HRESULT Get([in, out] int array[8]);
};

然后测试代码:

int main()
{CoInitialize(NULL);{CComPtr pMsg;HRESULT hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_LOCAL_SERVER, IID_IMessage, (void**)&pMsg);int arr[10]{ 1,2,3,4,5,6,7,8,9,10 };pMsg->Get(arr);}CoUninitialize();
}

我们想强行传递10个int给服务端,不过实际上代理只会传递8个int,因为它感知到了Get方法中的array只有8个元素。这样就能保证我们在实现Get方法时,只需要考虑8个元素了。这一点,它和C/C++中的传递数组不太一样。

2. 适应性数组(Conformant Array)

由于C/C++中,数组传递会退化为指针,失去了其元素个数的信息,因此我们往往需要传递数组的元素个数。在idl中也类似。如果需要指定我传递的数组有多少个元素,则要使用size_is属性。例如:

interface IMessage : IUnknown
{HRESULT Get([in] int count, [in, out, size_is(count)] int* array);
};

我们仍然需要注册自己的PS模块,才能让上面idl生效。下面是测试代码:

int main()
{CoInitialize(NULL);{CComPtr pMsg;HRESULT hr = CoCreateInstance(CLSID_Message, NULL, CLSCTX_LOCAL_SERVER, IID_IMessage, (void**)&pMsg);int arr[10]{ 1,2,3,4,5,6,7,8,9,10 };pMsg->Get(_countof(arr), arr);}CoUninitialize();
}

这一次,我们通过_countof,把10传给了Get作为第一个参数,代理感知到这个是作为了数组的元素个数,因此它会传递arr中的10个元素到Stub。

如果没有size_is会怎么样?arr会作为一个普通指针被传递,那么Stub只能拿到它的第一个元素1。

需要注意的是,size_is中是一个算数表达式,所以可以是变量名、数字字面值,或者一个表达式。与size_is类似的属性是max_is,它表示最大合法索引值,所以[size_is(n)]和[max_is(n-1)]是等价的。

另外一种情况:假设你需要服务端修改一个非常大的数组,例如int[1024],但是其实里面只有一小部分数组需要被修改,将数组全部传递过去显然太浪费带宽了,所以idl提供了另外一些属性[length_is], [first_is], [last_is],它提示了代理应该究竟传哪些数据过去:

[length_is]:一共要传递多少数据?
[first_is]:从第几个数据传起?
[last_is]:传到第几个数据?

举例如下:

interface IMessage : IUnknown
{HRESULT Get([in, out, first_is(10), length_is(5)] int array[1024]);
};

在客户端调用Get传递array给服务端stub过程中,只有第9个元素之后的5个元素(10, 11, 12, 13, 14)会被实际传输到服务端。服务端会构造1024个int,但是只有10, 11, 12, 13, 14这5个位置会用客户端的array填充,其它地方用0来填充,以此来充分节约带宽。

我们在实际过程中,可以将length_is和size_is相结合,来形成一个开放数组,充分发挥最高空间效率。

3. 多维数组

多维数组size_is中间可以有多个参数,用逗号分隔,分别表示它某一围上面的边界,如:

[in, size_is(5, 6)]** array 表示一个array[5][6]的二维数组。

4. SAFEARRAY

SAFEARRAY的出现是一个历史原因,为了能让Visual Basic也支持非固定的数组。

idl中,使用SAFEARRAY(类型)表示一个数组:

interface IMessage : IUnknown
{HRESULT PrintArray([in] SAFEARRAY(int)* array);
};

对应地C++代码是这样的:

IMessage : public IUnknown
{
public:virtual HRESULT STDMETHODCALLTYPE PrintArray( /* [in] */ SAFEARRAY * *array) = 0;
};

在C++中,SAFEARRAY(int)被展开成了SAFEARRAY**。SAFEARRAY结构如下:

typedef struct tagSAFEARRAY {USHORT cDims; // 多少维?USHORT fFeatures; // 什么特征?比如是IDispatch*数组,或者其它ULONG cbElements; // 每个元素大小?ULONG cLocks; // SAFEARRAY对象被锁定次数PVOID pvData; // 实际元素数据SAFEARRAYBOUND rgsabound[1];
} SAFEARRAY;

在实现层,我们不需要手动获取SAFEARRAY成员,因为COM提供了一套API来操作SAFEARRAY,例如SafeArrayCreate, SafeArrayAccessData, SafeArrayUnaccessData, SafeArrayGetLBound, SafeArrayGetUBound, SafeArrayCreateVector等。

SafeArrayCreate function (oleauto.h) - Win32 apps​docs.microsoft.com
8785fabf36d56a1684bf5ebe74c39777.png
SafeArrayAccessData function (oleauto.h) - Win32 apps​docs.microsoft.com
8785fabf36d56a1684bf5ebe74c39777.png
SafeArrayUnaccessData function (oleauto.h) - Win32 apps​docs.microsoft.com
8785fabf36d56a1684bf5ebe74c39777.png
SafeArrayGetLBound function (oleauto.h) - Win32 apps​docs.microsoft.com
8785fabf36d56a1684bf5ebe74c39777.png
SafeArrayGetUBound function (oleauto.h) - Win32 apps​docs.microsoft.com
8785fabf36d56a1684bf5ebe74c39777.png
SafeArrayCreateVector function (oleauto.h) - Win32 apps​docs.microsoft.com
8785fabf36d56a1684bf5ebe74c39777.png

SAFEARRAY通常在脚本语言,例如Visual Basic中自动生成。例如:

Sub PrintArray(ByVal pas As Integer())

表示一个SAFEARRAY。

我下面例子中,我们用C++来测试和实现一个SAFEARRAY的例子。我们实现上面PrintArray的方法,打印出所有传入的元素的值:

virtual HRESULT __stdcall PrintArray(SAFEARRAY** array) override
{long lBound, uBound;HRESULT hr &#61; SafeArrayGetLBound(*array, 1, &lBound); // 下界hr &#61; SafeArrayGetUBound(*array, 1, &uBound); // 上界int* data &#61; nullptr;hr &#61; SafeArrayAccessData(*array, (void**)&data); // 获取数据for (int i &#61; 0; i <&#61; uBound - lBound; &#43;&#43;i){std::cout <}

在测试代码中&#xff0c;我们创建一个SAFEARRAY并进行测试&#xff1a;

int main()
{CoInitialize(NULL);{CComPtr pMsg;HRESULT hr &#61; CoCreateInstance(CLSID_Message, NULL, CLSCTX_LOCAL_SERVER, IID_IMessage, (void**)&pMsg);SAFEARRAY* array &#61; SafeArrayCreateVector(VT_I4, 0, 10); // 创建一个1维int数组&#xff0c;从0开始&#xff0c;一个10个元素int* data &#61; nullptr;hr &#61; SafeArrayAccessData(array, (void**)&data);for (int i &#61; 0; i <10; &#43;&#43;i){data[i] &#61; i; // 写入内容}hr &#61; SafeArrayUnaccessData(array);hr &#61; pMsg->PrintArray(&array); // 调用SafeArrayDestroy(array); // 不需要用了&#xff0c;释放它}CoUninitialize();
}

如果我们的服务进程有控制台界面的话&#xff0c;就可以看到它确实把客户端的0, 1, 2, ..., 9全部打印了出来&#xff0c;说明我们SAFEARRAY使用方法正确。

六、常用属性

除了上面列举的一些属性(v1_enum, in, out, unique, ptr, ref...)&#xff0c;下面说一下一些常用的属性&#xff1a;

[object]&#xff1a;所有的COM接口必须要标记此属性&#xff0c;并且使用[uuid]来指定一个唯一标识&#xff1a;

[object,uuid(f39553d7-1cc7-4ca3-9295-5adbfe2b9146)
]
interface IMessage : IUnknown
{HRESULT PrintArray([in] SAFEARRAY(int)* array);
};

常用的接口属性如下&#xff1a;

[oleautomation]: 标记接口为可自动化的(https://docs.microsoft.com/en-us/windows/win32/midl/oleautomation)&#xff0c;那么可以使用这个属性。不过&#xff0c;不受支持的结构&#xff0c;特性(unique, ptr)等&#xff0c;还是需要注册自己的PS模块。本质上oleautomation就是为接口设置微软默认提供的PS模块而已。

[async_uuid]: 上篇文章讲过&#xff0c;为接口生成一个异步接口。

[nonextensible]: 不可拓展的IDispatch接口。默认地我们可以在运行时为IDispatch接口增加或者删除接口[1]&#xff0c;但是有了这个属性后我们就不可以这么做了。

[pointer_default]: 定义默认指针语义。

[dual]: 是否为一个双接口(IDispatch) (https://zhuanlan.zhihu.com/p/128124471)

[helpstring]: 对接口或者库的一段描述文本

[propget], [propput], [propputref]: 指定某个方法是个获取属性、设置属性的方法。

[retval]: 表明方法中某个属性是返回值&#xff0c;它在脚本语言中会直接作为返回值返回。

[optional]: 表明方法中某个属性是可选的。用于脚本语言中&#xff0c;如Visual Basic。

[defaultvalue]: 指明方法中某个属性的默认值。

还有一些属性&#xff0c;例如[default], [source]用于IConnectionPoint&#xff0c;coclass用于创建类对象&#xff0c;请参考之前的文章。

全部属性请参考&#xff1a;

MIDL Language Reference - Win32 apps​docs.microsoft.com
8785fabf36d56a1684bf5ebe74c39777.png

参考

  1. ^Recently, the IDispatch interface was extended through the definition of a new interface named IDispatchEx. The IDispatchEx interface derives from IDispatch, as you can see in the IDL definition below. In addition to the inherited IDispatch methods, IDispatchEx offers seven new methods that support the creation of dynamic objects (sometimes called "expando" objects) in which methods and properties can be added and removed at run time. In addition, unused parameters in the methods of IDispatch have been removed from IDispatchEx. For example, the unused interface identifier parameter passed to the IDispatch::Invoke and IDispatch::GetIDsOfNames methods has been removed from their respective methods in IDispatchEx (IDispatchEx::InvokeEx and IDispatchEx::GetDispID).7 https://www.thrysoee.dk/InsideCOM&#43;/ch05c.htm


推荐阅读
author-avatar
书友55218170
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有