我要投搞

标签云

收藏小站

爱尚经典语录、名言、句子、散文、日志、唯美图片

当前位置:王中王 > 定点数 >

在COM中使用数组参数 学步园

归档日期:08-14       文本归类:定点数      文章编辑:爱尚语录

  的基本原理和开发方法。为了能够更好的理解本文中的内容,读者需要具备基本的

  在COM中,如果对接口中方法的调用是跨套间的,就必须对所进行的调用进行序列化。

  在一个进程中可以包含多个套间,每个套间可以包含一个或多个线程(最新的COM标准中,某些线程可能会跨越套间,这里不讨论这种情况。)。包含单个线程的套间叫做单线程套间;包含多个线程的套间叫做多线程套间。在一个进程中最多可以包含一个多线程套间,但可以拥有0或多个单线程套间。每个使用COM的线程,无论是客户程序还是COM程序都要通过调用CoInitialize或CoInitializeEx函数进入套间,同时确定所在套间的类型。在进入套间之前不能使用COM功能,否则会导致错误结果。

  在COM架构中调用者和被调用者如果在不同的套间,就不可以直接调用,而必须通过代理(Proxy)和占位(Stub)程序调用。代理程序和调用者在同一个套间中,而占位程序和被调用者在同一个套间中。代理程序模仿COM组件的行为,接受调用,而占位程序模仿调用者的行为发出调用。这样就可以保证调用是在同一个套间中进行了。代理和占位程序之间通过特定的网络通讯协议传递被调用的方法、参数和返回值。把调用转换成网络协议的过程叫做序列化(Marshal)。由于涉及到指针或数组类型的传递地址型的参数,序列化过程非常复杂,幸好我们有简单的方法可以生成效率还算不错的代理和占位程序。

  对于指针类型,具体的指针地址是不重要的,关键是指针指向的内存中的数据。所以在传递指针类型的参数时,必须传递所指向的数据。对于数组类型数据也有类似的情况。如果存在双重或多重指针,情况就会变得更加复杂。

  例如long*类型的参数。在序列化的时候代理程序把参数所指向的长整型值传递给占位程序,占位程序要为参数申请内存,然后把长整型的值存放到申请到的内存中,使用这个新的内存地址作为参数调用目标函数。在函数返回的时候,内存中的数据变化被占位程序传递回代理程序。代理程序把数据复制回调用者的内存,然后返回。如果指针所指向的数据不是单个的值,而是一块不定大小的内存,序列化时就要确定所要传递数据的长度。另外,在多重指针的情况下,要传递的就不是一块数据,而可能是多块数据段了。

  COM通过代理和存根实现序列化功能。每个可能跨套间调用的接口都必须有相应的代理和存根程序。代理和存根程序是在同一个动态连接库中的。每个COM接口的设计者负责实现自己的代理和存根程序。一般情况下,代理和存根程序的代码可以通过一个叫做MIDL的工具自动生成的,我们要做的只是把它编译出来。

  代理的作用是在客户套间中“伪装”成COM对象,供客户程序调用。而存根的作用则是在COM对象所在的套间中“伪装”成客户程序,发出调用请求。代理和存根之间通过网络协议(实际上基于在远程过程调用协议(RPC)的一种协议。)交换调用请求和返回结果。

  在COM中使用数组可以使用三种方法:数组指针、SafeArray和ICollection。数组指针和我们熟悉的C/C++程序中传递数组的方法是相同的、SafeArray是VB中标准的存放数组的方法,也是Automation中的标准方法、ICollection方法是通过一个独立的COM对象传递数据。这三种方法各有优缺点,应该按照具体的需求决定使用哪种方法。

  数组指针是标准的C/C++中的数组参数传递方式。数组指针实际上就是数组元素序列化存放时的首地址。数组指针的操作非常简单,所以也是效率最高的传递方式。但是,这种方式不能够在VB中使用。数组指针可以传递一维数组,也可以传递多维数组。如果COM的客户端是VC++程序的话,这是最好的传递方式。

  数组指针作为跨套间的调用参数时,需要进行marshal。所以,应该编译和注册proxy/stub。

  SafeArray是标准的VB数组存放方式。和数组指针类似,SafeArray可以传递一维数组,也可以传递多维数组。由于SafeArray具有比数组指针更复杂的结构,所以,编程比使用数组指针复杂(指用VC++编程,如果用VB实现COM,这是唯一的传递方式。),程序运行效率也相对较低。

  使用SafeArray方式传递的数组,可以从VB程序中调用,也可以从VC++程序中调用。而且,由于SafeArray是Automation的标准数据,所以可以通过缺省的基于TLB的proxy/stub进行跨套间的调用,而不必编译和注册自己的proxy/stub。

  ICollection方式是最复杂,也是使用最广泛的。ICollection并不是一个接口的名称,而是指实现了枚举器和索引属性的IDispatch接口。这种数组传递方式的特点是自己实现数组对象,所以有最大的灵活性,可以实现按需生成数组元素等高级功能。

  ICollection所传递的数组对象不再是普通的指针或特定的结构,而是一个独立的COM对象。由于传递的是接口,所以参数具有面向对象的多态性特征,就是说数组元素可以是自己实现的,也可以是其他人实现的,只要是实现了有特定属性的IDispatch接口就可以作为参数。另一方面,由于用作数组的COM对象可以单独设计,所以,可以使用更加合理的实现方式,例如使用列表、hash表或平衡树等方式实现。

  有的数组实现,只需要访问少数的几个元素,或者元素个数理论上是无穷的,或者每个数组元素的生成需要耗费大量的资源。这时,应该使用ICollection方式实现数组传递。

  数组指针使用标准的C/C++数组表示方式。数组中的每个元素按照顺序在内存中依次排放。数组的下标从0开始计算。数组的第一个元素(下标为0的元素)的地址就是数组的指针,数组中每个元素所占的内存空间大小必须是固定的,只和数组类型有关。计算数组中某个元素的指针时,使用元素所占的字节数乘上元素下标就可以得出这个元素和数组指针之间的偏移量(准确地说,数组元素所占内存的字节数和编译时的对齐参数有关)。根据这个偏移量就可以计算出这个元素的地址指针了。

  使用数组指针最重要的是确定数组长度,使序列化程序可以正确地复制内存。为了生成代理和存根程序,我们要使用接口定义语言(IDL)描述接口和COM对象类型。MIDL读取IDL文件中的描述,生成TLB和代理存根的代码。

  为了尽量提高序列化的效率,在IDL中可以确定参数的传递方向。参数的传递方向有三种:输入型(in)、输出型(out)、和输入输出型(in, out)。输入型参数从调用者传递到被调用者,被调用者对输入型参数的更改不传回调用者。输出型参数正相反,从被调用者分回调用者,而被调用者不关心参数的初始值。输入输出型参数在调用的时候传到被调用者,同时,被调用者可以对参数进行修改,这个修改在调用返回的时候会复制回调用者。

  在IDL声明中,应该正确的设置数组长度和复制长度。数组长度用size_is或min_is、max_is属性定义,复制长度则使用length_is或first_is、last_is属性定义。在目前的IDL实现中,min_is并没有实现,所以,min_is只能是0。

  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不可以同时定义。复制元素的范围不能够超出数组本身的范围。如果没有指定复制范围,缺省的复制范围是整个数组。

  复制范围一般不在单纯的输入参数或输出参数上使用,而只在输入输出型参数中使用(复制范围还大量使用在自定义类型中的数组成员,这已经超出了本文的范围,请大家参考相关资料)。在IDL中指定复制范围会影响到哪部份的内存数据会从客户端复制到COM端;当COM端的方法返回时,也是根据复制范围,把数据复制回客户程序。数组从客户端复制到COM端和从COM端复制回客户端的范围可以是不同的。

  数组用作输出参数时,有两种方案:调用方建立数组和被调用方建立数组。当输出数组的长度是可预知的时候,应该使用调用方建立数组的方式。数组用作输出时,需要把属性中的in改成out。参见例2和例3。

  /* 例1:计算数组中数字的和,采用辅助变量确定需要传递的数据的长度*/

  /* 例2:计算数组中前Count个数的平方,保存到数组的后Count个元素中,注意数组长度和复制长度是不同的。*/

  /* 例3:取得前n个质数。由调用方申请内存。这里仅列出指针风格的定义。*/

  在IDL中,可以使用双重或多重指针类型的参数。这里只介绍用于数组输出用的双重指针,关于多重指针的使用,请参考相关文档。

  双重指针的size_is属性中包含用逗号分隔的两个长度值,分别是顶级指针数组的长度和次级指针的长度。在长度值空缺的情况下,使用缺省值1。例如:size_is(m,n)的含义是顶级指针长度是m,次级指针长度是n。size_is(,n)相当于size_is(1,n)。

  在使用数组指针传递数组时,常用的是顶级指针长度是1的双重指针。也就是指向数组指针的指针。

  如果输出的数组长度无法预知,就需要被调用的函数动态申请内存,建立数组。这个时候,由于调用方不知道数组的长度,不可能事先申请内存。所以,需要被调用方申请内存,并且把所申请的内存的地址传给调用方。也就是说,调用方要传递存放数组指针地址的地址给被调用方,也就是通过数组指针的指针传递参数。

  /*例1:取得所有员工编号,员工编号用长整形表示,员工的数量和每个员工编号都是输出参数。*/

  /*例2:返回传入数组中质数元素的指针。输出是一个指针数组,所以所传递的参数是一个三重指针,但本质上却是一个二重指针,所以size_is参数仍然使用二重指针的型式。*/

  在IDL中,没有定义多维数组,如果要使用多维数组,必须转换成一维数组来处理。一维数组的长度是多维数组中每个维度长度的乘积。比如说3*5的数组可以用一个长度为15的数组表示。

  // 在例3的输出中,取得数组元素时比较复杂,可以使用如下方法取得第i行第j列的元素:

  字符串是一种特殊的数组形式,这种数组不定义长度,而是以特殊标志——数值0结尾。由于在C语言中字符串都是以0结尾的方式存储,所以这种类型常被用于传递字符串参数。在IDL中规定字符串的元素类型只能是单字节或wchar_t类型,而且不能是多维的。

  定义字符串的属性是string。字符串数组只能是一维的。如果字符串是输出参数,应该使用双重指针,或者使用size_is属性指定buffer的长度。

  在上面提到的数组指针方法中,size_is中的参数也可以是常量,但是效率比较低。如果数组长度是一个常量,可以使用固定长度数组的定义。固定长度的数组可以是多维的,可以是输入、输出或输入输出类型。

  根据COM规范,输入型参数由调用方申请和释放内存。输出型参数由被调用方申请内存,由调用方释放内存。输入和输出型参数,由调用方申请内存,被调用方可以释放并重新申请内存,最终由调用方释放内存。

  由于涉及到代理和存根,跨套间的调用时代理和存根也参与内存管理,所以,COM和客户端必须使用相同的内存管理方式。在COM中,系统提供了一套内存管理函数,凡是涉及到COM接口参数的内存块都必须通过这几些函数进行管理,这里列出这些函数原型,具体说明请参考相关文档。

  COM提供了一套API用于处理SafeArray。为了保证程序和SafeArray结构无关[1],程序中建立、读取、更改和释放SafeArray都应该通过这些API进行,而不应该直接读写SafeArray结构。

  创建SafeArray可以使用COM提供的四个创建函数之一。所有的创建函数都返回一个SafeArray指针。通过这个指针可以读写SafeArray中的数据。SafeArray使用完后必须释放。

  这个函数用来创建简单类型的一维数组。这个函数有三个参数:vt是数组类型、lLbound是数组下界值(最小下标)和数组长度。vt的取值如下表:

  lLbound是数组的最小下标,可以是取负数。cElements是数组的长度。数组的最大下标的值是最小下标加上数组长度减一。

  当需要创建COM数组时,可以使用IUnknown指针,也可以用IDispatch指针。如果需要使用其它指针类型,应该使用QueryInterface方法取得,而不能直接在数组中保存。因为SafeArray数组的序列化程序只能处理IUnknown和IDispatch两种指针类型,如果在数组中放其它接口类型的指针,可能在跨套间使用中会出现问题。

  读写SafeArray数组时。应该使用COM提供的标准API。COM提供了大量函数用于SafeArray数组的操作,本文中仅使用其中的两个函数,SafeArrayAccessData和SafeArrayUnaccessData,和一些辅助用的函数。实际上是用这两个函数就可以进行所有的数组操作了。其它的函数用于对单个元素的操作,由于使用不多,而且效率也不高,所以本文中不进行说明。

  这个函数用于获取SafeArray的数据指针,并锁定SafeArray数组的数据。在取得了数据指针之后,就可以直接访问SafeArray数组中的数据了。

  如果数组类型是Type,那么所取得的数据指针实际上就是Type类型的数组的地址。在多维数组的情况下,必须把多个维度的下标转换成一维下标进行访问。

  这个函数的作用是对SafeArray数据解锁,解锁后,就不应该继续对数据指针进行读写访问。如果要访问,必须重新获取并锁定数据。

  在访问数组之前,必须知道数组中数据的类型,、维数以及每个维度的下界和长度。COM提供了取得这些数组参数的函数。

  // 取得每个维度的属性,返回指定维数(nDim)的上界和下界(nDim从1开始):

  // 取得自定义类型接口,对于自定义结构数组,返回自定义结构类型数据的指针:

  从SafeArrayAccessData返回的指针实际上就是C语言中的一维数组地址。在VC++中可以像访问普通数组一样读写这个数组。

  需要注意的是,在C语言中,所有的数组下标都是从0开始的。而在SafeArray中,数组下标可以从任何数字开始。所以在访问前必须进行转换。转换方法就是从SafeArray的下标中减去数组的下界,就可以得到C语言中数组的下标了。

  访问多维数组和访问一维数组类似,只是要把多维下标转换成一维下标。把多维下标转换成一维下标的方法和在数组指针中介绍的是相似的。

  设:有n个维度,每个维度的长度(上界减去下界加一)分别是L1、L2、…、Ln。要转换的下标是X1、X2、…、Xn。可以根据下述公式转换成一维数组的下标。

  访问自定义结构数组的时候,可以使用#iimport自动生成或者IDL编译产生的类型定义。如果没有办法取得自定义结构的声明,可以使用IRecordInfo接口中的方法间接访问自定义结构。

  首先需要取得自定义结构的长度,这可以通过IRecordInfo::GetSize方法取得。

  ICollection接口提供了最大的面向对象的设计灵活性和可重用性。在数组指针和SafeArray方法中,数组的每个元素必须事先计算出来,并且保存在特定的数据结构中。使用ICollection接口,可以设计出动态生成的数组,就是说数组的元素在需要的时候才进行计算,以便减少内存使用并加快处理速度。

  ICollection接口用于定义数组对象,而IEnumVARIANT接口用于定义枚举对象。枚举对象的作用是按顺序读取数组元素,有时,通过枚举对象可以获得更高的效率。

  有的时候,COM对象不但要实现数组功能,而且还要实现其它功能。所以,大多数时候,COM对象实现的接口是从ICollection继承来的。

  通过ICollection操纵数组大体上有两种方法。一种是通过Item属性用数组下标取得元素。这种方式,每次只能取得一个元素,而且要传递下标对象,所以效率比较低下。另一种方法是通过枚举器。数组对象的枚举器通过_NewEnum属性取得。通过枚举器只能按顺序获取元素,但每次可以取得任意多的元素,所以效率较高。ICollection对象可以只实现其中的一种访问方法,也可以两种都实现。ICollection中还有一个重要属性:Count。Count属性返回数组的长度,对于无法确定长度的数组,也可以不实现Count属性。

  IEnumVARIANT接口用于定义枚举器。枚举器用于顺序读取数组元素。通过Next方法,可以一次读取任意多的元素。由于枚举器只可以按顺序访问数组元素,所以Next方法不需要传递下标。Skip方法用于跳过若干元素,而不读取。Reset把当前元素设置到数组头,这样就可以重新开始枚举。Clone用于获得一个新的枚举器。两个枚举器可以互不干扰的工作。

  要注意的事,可能有某些数组对象的实现方法使用不同的属性名称。实际上ICollection中的属性名称是不重要的,重要的是Dispatch ID。只要通过Dispatch ID就可以取得正确的属性。

  数组对象是实现了ICollection接口的COM对象。数组对象的使用者通过ICollection接口取得数组中的数据,而完全不需要知道数组的具体实现方式。这种设计的好处是使用数组的代码可以完全不理会数组的实现方法,而当数组的实现发生变化时,使用数组的代码可以在二进制代码上保持兼容,也就是说目标代码不用编译就可以使用。

  最简单制作数组对象的方法是使用ATL的模板。CComEnumOnSTL模板用于生成实现IEnumVARIANT接口的枚举对象。当然,如果要实现数组对象的所有优点,最好自己编写数组对象的代码。

  在IDL声明中。数组对象应该声明成IDispatch *。如果是输出或输入输出参数,则应该使用双重指针。

  目前,我们看到的ICollection数组都是只读的。实际上ICollection完全可以设计成可读写的数组对象,只要把ICollection的Item属性设置成可读写的就可以了。关于可读写的ICollection对象请参考相关资料。

本文链接:http://brazil-run.com/dingdianshu/1006.html