上一篇:

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

本篇主要讲idl的一些语法特性。

idl的语法和C语言非常类似,但是它扩展了一些特性,这些特性用于兼容其它语言特性,或者是用来表示RPC中的行为。我们先从idl中简单地数据类型说起,然后再将它的一些属性。

一、枚举类型

和C语言类似,我们可以用enum关键字来定义一个枚举。枚举可以定义在接口内,那么其scope就是在接口内。例如idl文件:

// idl
[object,uuid(f39553d7-1cc7-4ca3-9295-5adbfe2b9148)
]
interface IFoo : IUnknown
{typedef [v1_enum] enum Apartment{STA,MTA,} Apartment;HRESULT Foo(Apartment);
};

对应生成的.h头文件:

/* interface IFoo */
/* [uuid][object] */ typedef /* [v1_enum] */
enum Apartment{STA  = 0,MTA    = ( STA + 1 ) }   Apartment;EXTERN_C const IID IID_IFoo;MIDL_INTERFACE("f39553d7-1cc7-4ca3-9295-5adbfe2b9148")IFoo : public IUnknown{public:virtual HRESULT STDMETHODCALLTYPE Foo( Apartment __MIDL__IFoo0000) = 0;};

不过,idl中支持[v1_enum]属性,意味着我们的enum在列集的时候使用32位整型来传输。没有它的话则使用16位,这在一般情况下也够用了。

二、基本类型

idl中的基本类型在下方链接中:

MIDL Predefined and Base Types - Win32 apps​docs.microsoft.com

常见基本类型

三、指向性属性

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

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不会讲其从服务端返回:

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

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

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

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

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

int main()
{CoInitialize(NULL);{CComPtr<IMessage> 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<IMessage> 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<IMessage> 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<IMessage> 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

五、数组

1. 固定长度的数组

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

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

然后测试代码:

int main()
{CoInitialize(NULL);{CComPtr<IMessage> 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<IMessage> 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

SafeArrayAccessData function (oleauto.h) - Win32 apps​docs.microsoft.com

SafeArrayUnaccessData function (oleauto.h) - Win32 apps​docs.microsoft.com

SafeArrayGetLBound function (oleauto.h) - Win32 apps​docs.microsoft.com

SafeArrayGetUBound function (oleauto.h) - Win32 apps​docs.microsoft.com

SafeArrayCreateVector function (oleauto.h) - Win32 apps​docs.microsoft.com

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 = SafeArrayGetLBound(*array, 1, &lBound); // 下界hr = SafeArrayGetUBound(*array, 1, &uBound); // 上界int* data = nullptr;hr = SafeArrayAccessData(*array, (void**)&data); // 获取数据for (int i = 0; i <= uBound - lBound; ++i){std::cout << data[i] << std::endl;}hr = SafeArrayUnaccessData(*array); // 释放数据return S_OK;
}

在测试代码中,我们创建一个SAFEARRAY并进行测试:

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

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

六、常用属性

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

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

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

常用的接口属性如下:

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

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

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

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

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

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

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

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

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

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

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

全部属性请参考:

MIDL Language Reference - Win32 apps​docs.microsoft.com

参考

  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+/ch05c.htm

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

  1. c6011取消对null指针的引用_C++| 函数的指针参数如何传递内存?

    函数的参数是一个一级指针,可以传递内存吗? 如果函数的参数是一个一级指针,不要指望用该指针去申请动态内存. 看下面的实例: #include using namespace std; void Get ...

  2. c6011取消对null指针的引用_C/C++学习笔记——C提高:指针强化

    指针是一种数据类型 指针变量 指针是一种数据类型,占用内存空间,用来保存内存地址. void test01(){ int* p1 = 0x1234; int*** p2 = 0x1111; print ...

  3. 【C语言进阶深度学习记录】二十二 指针的本质分析

    在C语言中,最难的也就是指针了.如果我们了解了指针的本质,它就会变得简单 文章目录 1 回顾:什么是变量? 1.1 *号的意义 1.2 指针使用示例 2 传值调用与传址调用 2.1 利用指针交换两个变 ...

  4. 警告 C6011 取消对NULL指针XXX的引用

    若要更正此警告,请检查指针是否有 null 值 #include <malloc.h> void f( ) {char *p = ( char * )malloc ( 10 );if ( ...

  5. c6011取消对null指针的引用_C++中的野指针及其规避方法

    今天在调试程序过程中,用到了一些指针的方法,这里记录一下野指针的概念. 1.概念 野指针,也就是指向不可用内存区域的指针.通常对这种指针进行操作的话,将会使程序发生不可预知的错误. 野指针与空指针(N ...

  6. c6011取消对null指针的引用_C++中的引用

    当变量声明为引用时,它将成为现有变量的替代名称.通过在声明中添加"&",可以将变量声明为引用. #include using namespace std; int main ...

  7. java 指针_java多线程学习二十二:::java中的指针

    在上面那个图,我们看到一个特殊的变量unsafe,它的包名是 sun.misc.Unsafe;从名字看,这个类应该是封装一些不安全的操作,为什么不安全?对c语言理解的朋友就知道了,指针是不安全的,在j ...

  8. 电子表整点报时怎么取消_上海迪士尼取消FP后,预约等候卡使用攻略!

    2020年5月11日上海迪士尼乐园恢复营业.同时,取消了以往的FP(快速通行证),推出了"预约等候卡",在一定时间内,没有预约等候卡,排队的资格都没有了. 迪士尼的预约等候卡是什么 ...

  9. python指针引用的区别_C++基础:指针和引用的区别

    C++基础:指针和引用的区别 *例 int a; int &b = a; 其中b是a的引用,b引用了a,a被b引用.b 相当于 a 的别名,对 b 的任何操作就是对a的操作.所以b既不是a的拷 ...

最新文章

  1. Linux下显示IP地址所在地信息的小工具——nali
  2. zabbix 4.0.3 use docker-compose deploy
  3. PHP学习笔记-Cookie
  4. python 链表的基础概念和基础用法
  5. 防火墙如可禁止tracert但允许ping
  6. linux文件指令 例子,Linux 命令:文件目录操作与实例
  7. Wpf 调用线程无法访问此对象,因为另一个线程拥有该对象,解决方案
  8. status c语言_STM32 嵌入式C语言教程--第四课C语言中的存储空间与位域
  9. html如何查看文档,查看文档
  10. Hadoop(七)Hive基础
  11. js html 处理json数据,JS中Json数据的处理和解析JSON数据的方法详解
  12. 荒野行动 android 鼠标,荒野行动键盘映射模拟器
  13. 台达b3伺服参数设置方法_台达伺服驱动器参数设置一览表
  14. 关于工信部要求品牌电脑强制预装“绿坝-花季护航”软件
  15. 项目实习(四)多线程端口扫描器
  16. 使用微PE工具箱制作U盘启动盘
  17. python中round(18.67、-1)_Python torch.round方法代码示例
  18. post 防篡改_如何防止http请求数据被篡改
  19. SSM整合练习:记账管理
  20. 解闷又有趣的小游戏在这就有

热门文章

  1. python索引字符串_Python:通过索引删除子字符串
  2. zend studio mysql 配置_php 在Zend Framework中配置数据库参数
  3. Observe rainy world
  4. vivo6.0系统怎么样不用root激活XPOSED框架的方法
  5. 运维人员如何最大限度避免误删除文件
  6. Unity设置播放模式下始终先执行指定的场景
  7. 输入一个数,求1到他 的和(for循环)
  8. lvs+keepalived+nginx+tomcat高可用高性能集群部署
  9. 将学校版JAVA系统迁移到Mysql数据库的工作安排
  10. 西安市2008驾照理论考试题