COM组件技术积累_第1页
COM组件技术积累_第2页
COM组件技术积累_第3页
COM组件技术积累_第4页
COM组件技术积累_第5页
已阅读5页,还剩30页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

COM根本概念什么是COM所谓COM〔ComponetObjectModel,组件对象模型〕,是一种说明如何建立可动态互变组件的标准,此标准提供了为保证能够互操作,客户和组件应遵循的一些二进制和网络标准。通过这种标准将可以在任意两个组件之间进行通信而不用考虑其所处的操作环境是否相同、使用的开发者语言是否一致以及是否运行于同一台计算机。COM标准所描述的即是如何编写组件,遵循COM标准的任何一个组件都是可以被用来组合成应用程序的。至于对组件采取的是何种编程语言那么是无关紧要的,可以自由选取。作为一个真正意义上的组件,应具备如下特征:实现对开发语言的封装。以二进制形式发布。能够在不阻碍已有用户的情况下被升级。在网络上的位置必须能够被透明的重新分配。在Windows操作系统平台上,有一些用COM形式提供的组件模块极大地丰富了Windows的功能,而且也使Windows功能扩展更加灵活,例如:DiretX多媒体软件包。它以COM接口的形式为Windows平台提供了强大的多媒体功能,现广泛用于游戏娱乐软件及其他多媒体软件的开发。RDO〔remotedataobjet,远程数据对象〕和DAO〔dataaccessobject,数据访问对象〕数据库访问对象库。它以COM自动化对象的形式为数据库应用提供了便捷的操作方法,特别适合于在BASIC语言或其他一些高级语言中使用。而数据访问一致接口OLEDB/ADO〔activedataobject,活动数据对象〕更淋漓尽致地发挥了COM接口的作用。InternetClientSDK。它提供了一组COM库,为应用系统增加Internet特性提供了底层透明的一致操作。其它还有一些组件如MAPI〔messagingAPI,消息应用编程接口〕、ADSI〔activedirectoryserviceinterface,活动目录效劳接口〕等,它们都提供了一致、高效的效劳。从整个Windows操作系统来看,COM成了系统的根本软件模型,它带来的是灵活性和高效率,以及应用开发的一致性。COM对象与C++对象比拟COM对象建立在二进制一级的根底上,而C++对象建立在源代码一级的根底上,但从特性上,可以作一比拟。封装性:COM对象的数据成员的封装以组件模型为最终边界,对于用户是完全透明的、不可见的;而C++对象的封装特性只是语义上的封装,对于对象用户是可见的。可重用性COM对象的可重用性表现在COM对象的包容和聚合,一个对象可以完全使用另一个另一个对象的所有功能;而C++对象的可重用表现在C++类的继承性,派生类可以调用其父类的非私有成员函数。多态性C++对象的多态性表达了C++语言用类描述事物的高度抽象的特征;COM对象也具有多态性,但这种多态性需要通过COM对象所具有的接口才能表达出来,就像C++对象的多态性需要通过其虚函数才能表达一样。COM对象和接口COM提供的是面向对象的组件模型,COM组件提供应客户的是以对象形式封装起来的实体。客户程序和COM组件程序进行交互的实体是COM对象。COM对象包括属性〔也称为状态〕和方法〔也称为操作〕,对象的状态反映了对象的存在,也是区别于其他对象的要素;而对象所提供的方法就是对象提供应外界的接口,客户必须通过接口才能获得对象的效劳。对于COM对象来说,接口是它与外界进行交互的唯一途径,因此,封装特性是COM对象的根本特征。COM对象标识——CLSIDCOM组件的位置对于客户来说是透明的,因为客户并不直接去访问COM组件,客户程序通过一个全局标识符进行对象的创立和初始化工作,这个全局标识符就是CLSID。CLSID结构定义上与GUID一致。COM标准采用了128位全局唯一标示符GUID。这是一个随机数。手工创立128位GUID或者编写程序来产生GUID是件很麻烦的事。为此,MicrosoftVisualC++提供了两个工具实现这样的目的:UUIDGen.exe和GUIDGen.exe,前者是一个命令行程序,后者是一个基于对话框的应用程序。另外,COM库为我们提供了以下API函数可以产生GUID:HRESULTCoCreateGuid〔GUID*pguid〕HRESULTCoCreateGuid〔GUID*pguid〕下面为例如工程中.rgs文件中CLSID的定义:Math.Obj.1=s'MyMathClass'Math.Obj.1=s'MyMathClass' { CLSID=s'{3B28F0D6-D029-484B-80D7-A946EB20E9BD}' }将例如工程的COM组件成功注册后,我们可以根据组件的CLSID在系统的注册表编辑器中找到组件的注册信息,如图1。图1系统注册表COM对象的数据类型HRESULT:一个双字节的值,其最高位〔bit〕如果是0表示成功,1表示错误。常见的HRESULT值:HRESULT值含义S_OK0x00000000成功S_FALSE0x00000001函数成功执行完成,但返回时出现错误E_INVALIDARG0x80070057参数有错误E_OUTOFMEMORY0x8007000E内存申请错误E_UNEXPECTED0x8000FFFF未知的异常E_NOTIMPL0x80004001未实现功能E_FAIL0x80004005没有详细说明的错误E_POINTER0x80004003无效的指针E_HANDLE0x80070006无效的句柄E_ABORT0x80004004终止操作E_ACCESSDENIED0x80070005访问被拒绝E_NOINTERFACE0x80004002不支持接口UNICODE:IDL字符串的标准形式,使用2个字节表示一个字符〔unsignedshortint、WCHAR、_wchar_t、OLECHAR〕,不会出现乱码,UNICODE的范围是0x0000-0xFFFF共6万多个字符。BSTR:一个OLECHAR*类型的Unicode字符串。由于操作系统提供相应的API函数〔如SysAllocString〕来管理它以及一些默认的调度代码,因此BSTR实际上就是一个COM字符串,自带字符串长度信息。API函数函数作用SysAllocString()申请一个BSTR指针,并初始化为一个字符串SysFreeString()释放BSTR内存SysAllocStringLen()申请一个指定字符长度的BSTR指针,并初始化为一个字符串SysAllocStringByteLen()申请一个指定字节长度的BSTR指针,并初始化为一个字符串SysReAllocStringLen()重新申请BSTR指针CString函数函数作用AllocSysString()从CString得到BSTRSetSysString()重新申请BSTR指针,并复制到CString中CComBSTR函数ATL的BSTR包装类,在atlbase.h中定义,具体查看MSDN_bstr_tC++对BSTR的封装,它的构造和析构函数分别调用SysAllocString和SysFreeString函数,其他操作是借用BSTRAPI函数。VARIANT:具有跨语言的特性,它可以表示〔存储〕任意类型的数据,既包括了数据本身,也包含了数据的类型,定义在oaidl.h中。structtagVARIANT{structtagVARIANT{VARTYPEvt;Union{shortiVal;//VT_I2longlVal;//VT_I4floatfltVal;//VT_R4doubledblVal;//VT_R8DATEdate;//VT_DATEBSTRbstrVal;//VT_BSTR…..short*piVal;//VT_BYREF|VT_I2long*plVal;//VT_BYREF|VT_I4float*pfltVal;//VT_BYREF|VT_R4double*pdbVal;//VT_BYREF|VT_R8DATE*pdate;//VT_BYREF|VT_DATEBSTR*pbstrVal;//VT_BYREF|VT_BSTR};};typedeftagVARIANTVARIANT;VARIANT使用例如:VARIANTva;VARIANTva;::VariantInit(&va);//初始化Inta=2023;Va.vt=VT_I4;//指明long数据类型Va.IVal=a;//赋值::VariantClear();Windows定义的VARIANT相关函数:VariantIntVariantInt——将变量初始化为VT_EMPTY;VariantClear——消除并初始化VARIANT;VariantChangeType——改变VARIANT的类型;VariantCopy——释放与目标VARIANT相连的内存并赋值源VARIANTCOleVariant:对VARIANT结构的封装COleVariantv1(COleVariantv1(“Thisisatest〞);//直接构造COleVariantv2=“Thisisatest〞;//结果时VT_BSTR类型,值为〞Thisisatest〞COleVariantv3((long)2023);COleVariantv4=(long)2023;//结果时VT_I4类型,值为2023_variant_t:用于COM的VARIANT类描述性语言IDL和MIDL编译器COM标准在采用OSF的DCE标准描述远程调用接口IDL〔interfacedescriptionlanguage,接口描述语言〕的根底上,进行扩展形成了COM接口的描述语言。接口描述语言提供了一种不依赖于任何语言的接口描述方法,因此,它可以成为组件程序和客户端程序之间的共同语言。COM标准使用的IDL接口描述语言不仅可用于定义COM接口,同时还定义了一些常用的数据类型,也可以描述自定义的数据结构,对于接口成员函数,我们可以指定每个参数的类型、输入输出特性,甚至支持可变长度的数组的描述。IDL中所有数据、方法、接口、类和库的特性都由属性信息来描述。属性信息中由括号括起来,作为它们描述的对象的前缀。in:输入型参数,从调用者传递到被调用者,被调用者对输入型参数的更改不传回调用者。out:输出型参数,从被调用者返回调用者,而被调用者不关心参数的初始值。In,out:输入输出型参数在调用的时候传到被调用者,同时,被调用者可以对参数进行修改,这个修改在调用返回的时候会被复制回调用者。〔PS:非指针类型一定是输入型参数。输出型参数和输入输出型参数一定是指针类型〕retval:返回一个与方法的物理HRESULT不相关的逻辑结果,与out一起使用,且只能有一个,放在参数的最后。string:参数所指向的是一个字符串类型参数,以Null终止。size_is:指针数组中元素个数由另一个参数说明。length_is:用来设置在序列化时需要复制的元素数量。下面IDL例如为工程Math的IDL文件。IDL中接口定义例如:[[ object, uuid(CED2CE33-5419-49E0-AE72-CB1E4D2B0C8F), oleautomation, nonextensible, pointer_default(unique)]interfaceIMyMath:IUnknown{ [helpstring("方法Add")]HRESULTAdd([in]Element*pElement,[out]DWORD*pValue); [helpstring("方法Operate")]HRESULTOperate([in]Element*pElement,functionfun,[out]DWORD*pValue);[helpstring("方法Sum")]HRESULTSum([in]DWORDColCount, [in]DWORDRowCount, [in,size_is(ColCount*RowCount)]DWORD*pNums, [out,retval]DWORD*pResult);};IDL中enum定义例如:typedeftypedef [ uuid(9CE9F449-3894-44AD-9F15-1DE67E915329), version(1.0), helpstring("Enumoffunction") ] enumfunction{ fAdd=0, fSub, fMul}function;IDL中struct定义例如:typedeftypedef [ uuid(9CE9F449-3894-44AD-9F15-1DE67E915329), version(1.0), helpstring("Enumoffunction") ] enumfunction{ fAdd=0, fSub, fMul}function;IDL中union定义例如:typedeftypedef

[

uuid(994A75FF-6FC8-4802-AA42-4E04776BD521),

version(1.0),

helpstring(“NUMBER")

]

unionNUMBER{

[case(1)]longi;

[case(2)]]floatf;

}NUMBER;IDL类定义例如:[[ uuid(12881436-9C8F-457E-851F-25CCD3F25D30), version(1.0),]libraryMathLib{ importlib("stdole2.tlb"); [ uuid(3B28F0D6-D029-484B-80D7-A946EB20E9BD) ] coclassMyMath { [default]interfaceIMyMath;interfaceIMyMath2; };};MicrosoftVisualC++提供了MIDL工具,可以把IDL接口描述文件编译成C/C++兼容的接口描述文件〔.h〕和C文件〔.c〕,可以被组件程序和客户程序所使用。XX_i.h:一个同C和C++兼容的,包含IDL中所描述的所有接口声明的头文件;XX_i.c:一个定义有IDL文件中所用的所有GUID的C文件IUnknown接口COM定义的每一个接口都必须从IUnknow继承过来,其原因在于IUnknow接口提供了非常重要的特性:生存期控制和接口查询。客户程序只能通过接口和COM对象进行通信,虽然客户程序可以不管对象内部的实现细节,但它要控制对象的存在与否。QueryInterface接口方法介绍按照COM标准,一个COM对象可以实现多个接口,客户端程序可以在运行时刻对COM对象的接口进行询问,如果对象实现了该接口,那么对象可以提供这样的接口效劳。QueryInterface函数的说明:HRESULTQueryInterface([in]REFIIDiid,[ou]void**ppV);函数的输入参数iid为接口标识符IID,输出参数ppv为查询得到的结果接口指针,如果没有实现iid所标识的接口,那么输出参数ppv指向空〔NULL〕。函数的返回值为一个32位的整数,反映了查询的结果,其含义有三种情况:S_OK,查到了指定的接口,接口指针存放在ppv输出参数中;E_NOINTERFACE,对象不支持所指定的接口,*ppv为NULL;E_UNEXPECTED,发生了意外错误,*ppv为NULL。对于调用QueryInterface函数,COM标准给出了以下一些规那么:对于同一个对象的不同接口指针,查询得到的IUnknown接口必须完全相同。也就是说每个COM对象的IUnknown接口指针是唯一的。接口自反性。对于一个接口查询其自身总应该成功接口对称性。如果从一个接口指针查询到另一个接口指针,那么从第二个接口指针再回到第一个接口指针必定成功,如:IMyMath*pIMyMath=NULL;IMyMath*pIMyMath=NULL; HRESULThr=::CoCreateInstance(CLSID_MyMath,NULL,CLSCTX_INPROC_SERVER,IID_IMyMath,(void**)&pIMyMath);IMyMath2*pIMyMath2=NULL; hr=pIMyMath->QueryInterface(IID_IMyMath2,(void**)&pIMyMath2);接口传递性。如果从第一个接口指针查询到第二个接口指针,从第二个接口指针可以查询到第三个接口指针,那么从第三个接口指针一定可以查询到第一个接口指针。接口查询时间无关性。如果某一个时刻可以查询到某一个接口指针,那么以后任何时候再查询到同样的接口指针,一定可以查询成功。引用计数IUnknown引入了“引用计数〞〔referencecounting〕方法,可以有效地控制对象的生存周期,解决内存管理的问题。COM对象通过引用计数来决定是否继续生存下去。IUnknown的接口成员函数AddRef和Release分别完成引用计数的增1和减1操作。如果一个COM对象实现了多个接口,那么可以采用同样的计数技术,只要引用计数不为0,就说明该COM对象的客户仍然在使用它〔前提是客户程序正确地操作了引用计数〕,它就继续生存下去;反之,如果引用计数减到0,那么说明客户不再使用该对象了,于是它就可以被去除。需要调用AddRef方法的情形:当把一个非空指针写到局部变量中时当被调用方把一个非空接口指针写到方法或者函数的[out]或者[in,out]参数中时当被调用方返回一个非空接口指针作为函数的实际结果时当把一个非空接口指针写到对象的一个数据成员中时注意:QueryInterface内含AddRef,不需要再调用需要调用Release方法的情形:在改写一个非空局部变量或者数据成员之前在离开非局部变量的作用域〔scope〕之前在被调用方要改写方法或者函数的[in,out]参数,并且参数的初始值为非空时。注意,对于传入的out参数,不需要释放在改写一个对象的非空数据成员之前在离开一个对象的析构函数之前,并且这时还有一个非空接口指针作为数据成员特殊情况当调用方把一个非空接口指针通过[in]参数传递给一个函数或者方法时,既不需要调用AddRef,也不需要调用Release,因为在调用堆栈中,临时变量的生命周期只是“用于初始化形式参数〞的表达式的生命周期的一个子集。重要概念:套间什么是套间?根据《COM技术内幕》的观点,COM没有定义自己新的线程模型,而是直接利用了Win32线程,或者说对其做了改造、包装。《COM本质论》是这样定义的:套间定义了一组对象的逻辑组合,这些对象共享一组并发性和冲入限制。每个COM对象都属于某一个套间,一个套间可以包含多个COM对象。MSDN上解释说,可以把进程中的组件对象想象为分成了很多组,每一组就是一个套间。属于这个套间的线程,可以直接调用组件,不属于这个套间的线程,要通过代理才能调用组件。最直接的说,COM库为了实现简化多线程编程的设想,提出了套间的概念。套间是一个逻辑上的概念,它把Win32里的线程、组件等,按照一定的规那么结合在一起,并且以此提供一种模式,用于多线程并发访问COM组件。可以把套间看做COM对象的管理者,它通过调度,切换COM对象的执行环境,保证COM对象的多线程调用正常运行。COM和线程不是包含关系,而是对应和关联关系。单进程套间STASingle-threadedApartments,一个套间只关联一个线程,COM库保证对象只能由这个线程访问〔通过对象的接口指针调用其他方法〕,其他线程不得直接访问这个对象〔可以间接访问,但最终还是由这个线程访问〕。COM库实现了所有调用的同步,因为只有关联线程访问COM对象。如果有N个调用同时并发,N-1个调用处于阻塞状态,如图2。对象的状态〔也就是对象的成员变量的值〕肯定是正确变化的,不会出现线程访问冲突而导致对象状态错误。图2STA实现过程//创立一个STA套间并和当前线程//创立一个STA套间并和当前线程关联::CoInitialize(NULL);//或者::CoInitializeEx(NULL,COINIT_APARTMENTTHREADED);……….……………::CoUninitialize();上述代码创立了一个STA,然后套间把当前的线程和自己关联在一起,线程被标记为套间线程,只有这个线程能直接调用COM对象。多进程套间MTAMultithreadedApartments,一个套间可以对应多个线程,COM对象可以被多个线程并发访问,如图3。所以这个对象的作者必须在自己的代码中实现线程保护、同步工作,保证可以正确改变自己的状态。图3MTA实现过程//创立一个//创立一个MTA套间并和当前线程关联::CoInitializeEx(NULL,COINIT_COINIT_MULTITHREADED);………..……………::CoUninitialize();第一次如此调用的时候,会创立一个MTA,然后套间把当前线程和自己关联在一起,线程被标记为自由线程。以后第二个线程再调用〔同一个进程中〕的时候,这个MTA会把第二个线程也关联在一起。一个MTA可以关联多个线程。所有的关联线程都可以调用套间中的组件。这就涉及到同步问题,需要组件编写者解决。这个对于作为业务逻辑组件或干后台效劳的组件非常适合。因为作为一个分布式的效劳器,同一时间可能有几个效劳请求到达,如果排队进行调用,那么将是不能想象的。NA套间COM+为了进一步简化多线程编程,引入的中立线程套间概念。NA/TNA/NAT,NeutralApartment/ThreadNeutralApartment/NeutralThreadedApartment。这种套间只和对象相关联,没有关联的线程,因此任何线程都可以直接访问里面的对象,不存在STA还是MTA。线程模型属性组件编写者可以实现:同一个组件,既可以在STA中运行,也可以在MTA中运行,还可以在两种环境中同时存在。可以说组件有一种属性说明可以在哪种环境中生存,属性名叫做“线程模型〞。这个属性为ThreadModel,可以取以下值:MainThreadApartment(Single)SingleThreadApartment(Apartment)FreeThreadApartment(Free)AnyApartment(Both)NeutralApartment(N/A)根据ATL工程向导创立COM效劳器时,当我们为COM组件添加一个COM类时就可以确定其线程模型。选择原那么是根据组件的功能选择:如果组件做I/O,首选是Free,因为可以相应其他客户端调用。如果组件和用户交互,首选Apartment,保持消息依次调用。COM+首选N/A,如果没有定义,COM库默认为是MainTreadApartment。Apartment简单,Free强大但要自己实现同步。当是进程内组件时,根据注册表项<CLSID>\InprocServer32\ThreadingModel和线程的不同,列于下表:创立线程关联的套间种类ThreadingModel键值组件对象最后所在套间STAApartment创立线程的套间STAFree进程内的MTA套间STABoth创立线程的套间STA“〞或Single进程内的主STA套间STANeutral进程内的NA套间MTAApartment新建的一个STA套间MTAFree进程内的MTA套间MTABoth进程内的MTA套间MTA“〞或Single进程内的主STA套间MTANeutral进程内的NA套间进程内的主STA套间是进程中第一个调用CoInitialize的线程所有关联的套间〔即进程中的第一个STA套间〕。当时进程外组件时,由主函数调用CoIntializeEx或CoIntialize指定组件所在套间,与上表相同。COM效劳器COM效劳器类型介绍进程内COM组件在Windows中一个正在被执行的程序被称作是一个进程。每一个应用程序〔EXE〕都将以一个单独的进程运行,每一个进程都有一个4GB的地址空间。一个进程中的一个地址同另一个进程中的某个地址是不同的。由于进程内组件驻留在客户端程序的地址空间中,因此它们共享同一进程,它们也可以共享同一地址空间。当客户端程序得到组件的一个接口指针时,连接客户和组件的唯一中介是接口的二进制结构。当客户查询组件的某个接口时,它所请求的实际上是具体特定格式的一块内存。当组件返回一个接口指针时,他告诉客户的实际上是此块内存的地址。由于接口是在客户和组件都能够访问的内存中,因此这种情况实际上与当客户和组件在同一EXE文件中时是相同的。进程外COM效劳器不同EXE中的组件和客户将在不同的进程中运行,客户和组件之间的交互就会跨越进程边界。在某些情况下,进程外COM效劳器也可以分为“本地效劳器〞和“远程效劳器〞。远程效劳器指的是运行于另外一个不用的机器上的进程外效劳器。对于进程间的通信,有几种不同的方式,如动态数据交换〔DDE〕、命名管道以及共享内存等。远程COM效劳器所用的方法是远程过程调用〔RPC〕,本地效劳器那么是通过本地过程调用〔LPC〕。LPC是同一机器上不同进程间通信的一种方法,它是基于远程过程调用〔RPC〕的用于单机上进程间通信的专利技术。将函数调用的参数从一个进程的地址空间传到另外一个进程的地址空间中需要一种方法——“调整〞。假设两个进程都在同一个机器上,那么调整过程是直接的:只需将参数数据从一个进程的地址空间复制到另外一个进程的地址空间中。假设参与参数传递的两个进程在不同的地址空间中,那么考虑到不同机器在数据表示方面的不同,如整数的字节顺序可能会不一样,必须将参数数据转换成标准的格式。代理和存根当调用Win32函数时,系统实现上将调用一个DLL中的函数,而此函数将通过LPC调用Windows中的实际代码。这种结构可以将用户进程同Windows代码隔离开。由于不同的进程具有不用的地址空间,因此用户不可能对操作系统造成破坏。COM使用的结构与此类似。客户将同一个模仿组件的DLL进行通信。这个DLL可以为客户完成参数的调整及LPC调用。在COM中,此DLL〔也是一个组件〕被称作是一个代理。用COM术语来说,一个代理就是同另一个组件行为相同的组件。代理必须是DLL形式的,因为它们需要访问客户进程的地址空间以便对传给接口函数的数据进行调整客户程序和进程外COM效劳器之间的调用关系如图4所示。客户程序只与同一进程中的LPC返回结果LPC调用组件存根存根DLL代理DLL中代理对象代理〔proxy〕对象打交道,组件程序只与同一进程中的存根DLL打交道,LPC调用只在代理对象和存根DLL之间进行。组件还需要一个被称作是残根的DLL,以对从客户传来的数据进行反调整,残根DLL也将对传回给客户的数据进行调整。LPC返回结果LPC调用组件存根存根DLL代理DLL中代理对象组件对象组件程序(组件进程)客户程序(客户进程)代理DLL中代理对象LPC返回结果LPC调用组件存根组件对象组件程序(组件进程)客户程序(客户进程)代理DLL中代理对象LPC返回结果LPC调用组件存根存根DLL存根DLL图4客户端与组件调用关系远程COM效劳器时,即组件程序运行在不同的机器上,那么代理DLL和存根DLL就通过RPC方式进行网络上的过程调用,从而实现分布式组件对象模型。因此RPC比LPC更为复杂,LPC相当于一个优化了的RPC实现。列集与散集代理DLL和存根DLL除了完成LPC调用之外,它还需要对参数和返回值进行翻译和传递,客户程序调用的参数,首先经过代理DLL的处理,它把参数以及其他的一些调用信息组装成一个数据包传递给组件进程,这个过程称为参数列集〔marshaling〕;组件进程接收到数据包之后,要进行解包操作,把参数信息提取出来,这个过程被称为散集〔unmarshaling〕。函数的返回值和输出参数在返回的过程中也要进行列集和散集操作,只是在存根DLL一端进行列集,在代理DLL一端进行散集,最后把散集后的结果返回给客户程序。列集过程可通过两种方式实现:第一种列集方法为自定义列集法〔custommarshaling〕,也称为根本列集法〔basicmarshalingarchitecture〕,其列集过程完全由对象自身控制,对象指定其代理对象的CLSID,代理对象控制了其所有接口的列集过程,包括接口参数的列集和散集,以及代理对象和存根代码之间的跨进程通信过程。第二种列集方法为标准列集法〔standardmarshaling〕,这是由COM提供缺省的代理对象和存根代码,因为列集过程涉及到操作系统的一些复杂特性的编程,如共享内存操作或其他跨进程数据传输机制,甚至通过网络协议传输数据,所以COM提供了缺省的代理和存根代码以及一套标准的列集方法,可以处理常用数据类型的列集和散集,包括指针类型和接口指针类型。标准列集法的原理以及其列集过程与自定义列集法完全一致,事实上标准列集法就是自定义列集法的一个特例,但两者有一个根本的不同:自定义列集法其列集过程完全由对象自身控制,所以自定义列集法以整个对象为列集单位,即对象指定的代理对象和存根代码必须处理对象支持的所有接口;而标准列集法使用COM提供的标准代理对象和存根代码,实际上该代理对象和存根代码只是接口列集过程的管理器,因此,标准列集法是以接口为列集单位,COM提供的很多标准接口,其列集过程已经由COM库提供了,程序员只需要提供自定义接口的列集代码即可。标准列集的优点:组件无需实现IMarshal接口及代理组件,但组件需要为自己生成一个代理/存根组件〔Proxy/Stub〕,由于可通过MIDL由IDL文件自动生成,效率高,代码正确性有保证。除非有一些特殊目的,否那么一般采用标准列集法。下面着重介绍标准列集法。用标准的列集散集函数列集函数:HRESULTCoMarshalInterface(HRESULTCoMarshalInterface( [in]IStream*pStm, //写列集状态的位置 [in]REFIIDriid, //列集指针类型¨ [in,iid_is(riid)]IUnknown*pItf, //列集指针 [in]DWORDdwDestCtx, //目标的MSHCTX [in]void*pvDestCtx, //保存必须为0 [in]DWORDdwMshlFlags //标志常规还是表格列集);散集函数:HRESULTCoUnmarshalInterface(HRESULTCoUnmarshalInterface( [in]IStream*pStm, //读取列集状态 [in]REFIIDriid, //散集指针类型ª [out,iid_is(riid)]void**ppv, //存放散集指针的位置 );用列集辅助函数和散集辅助函数列集辅助函数:在原有根底上进行了简单封装,同样适用于同一进程内的线程间传递接口指针HRESULTCoMarshalInterThreadInterfaceInStream(HRESULTCoMarshalInterThreadInterfaceInStream( [in]REFIIDriid, [in,iid_is(riid)]IUnknown*pItf, [out]IStream**ppStm );散集辅助函数:在原有根底上进行了简单封装,同样适用于同一进程内的线程间传递接口指针HRESULTCoGetInterfaceAndReleaseStream(HRESULTCoGetInterfaceAndReleaseStream( [in]IStream*pStm, //读取列集状态 [in]REFIIDriid, //散集指针类型¨ª [out,iid_is(riid)]void**ppv, //存放散集指针的位置 );全局接口表〔GIT,GlobalInterfaceTable〕:允许接口指针被进程内所有套件访问。一个进程拥有一个GIT,其所包含的经过列集的接口指针,在同一进程中可以被有效的散集屡次IGoballnterfaceTable接口定义:MIDL_INTERFACE(MIDL_INTERFACE("00000146-0000-0000-C000-000000000046")IGlobalInterfaceTable:publicIUnknown{public:virtualHRESULTSTDMETHODCALLTYPERegisterInterfaceInGlobal(/*[annotation][in]*/__inIUnknown*pUnk,/*[annotation][in]*/__inREFIIDriid,/*[annotation][out]*/__outDWORD*pdwCookie)=0;virtualHRESULTSTDMETHODCALLTYPERevokeInterfaceFromGlobal(/*[annotation][in]*/__inDWORDdwCookie)=0;virtualHRESULTSTDMETHODCALLTYPEGetInterfaceFromGlobal(/*[annotation][in]*/__inDWORDdwCookie,/*[annotation][in]*/__inREFIIDriid,/*[annotation][iid_is][out]*/__deref_outvoid**ppv)=0;};下面结合实例介绍列集散集函数方法:实例工程PrintServer中已实现一个本地COM效劳器,其中暴露的IMyFun接口中包含了两个方法:PringOne和PrintTwo。这两个方法只是简单返回了两个不同的字符串,方便在客户端调用时区分。//PrintServer.idl:PrintServer//PrintServer.idl:PrintServer的IDL文件[ object, uuid(7EC1667C-1037-48D4-ACF0-F1159414793B), pointer_default(unique)]interfaceIMyFun:IUnknown{ [id(1),helpstring("方法PrintOne")]HRESULTPrintOne([out,string]BSTR*pbstr); [id(2),helpstring("方法PrintTwo")]HRESULTPrintTwo([out,string]BSTR*pbstr);};[ uuid(C606F70F-EF78-47DD-A051-3C93CC093E1D), version(1.0),]libraryPrintServerLib{ importlib("stdole2.tlb"); [ uuid(FCD6C50B-EFEB-4E9D-9714-09D5A5341D43) ] coclassMyFun { [default]interfaceIMyFun; };};下面介绍客户端中实现跨套间调用接口方法的实例。首先获取接口COM对象实例。//生成MTA套间并关联//生成MTA套间并关联 HRESULThr=CoInitializeEx(NULL,COINIT_MULTITHREADED); if(FAILED(hr)) { printf("CoinitializeExfailed!"); CloseHandle(g_heventWritten); return0; } //创立实例对象IMyFun*pTest=NULL; hr=::CoCreateInstance(CLSID_MyFun, 0, CLSCTX_LOCAL_SERVER, IID_IMyFun, (void**)&pTest); if(FAILED(hr)) { printf("CoCreateInstaancefailed!"); CloseHandle(g_heventWritten); ::CoUninitialize(); return0; }列集实现函数:////列集实现函数HRESULTWritePtrToGlobalVariable(IMyFun*pRacer,HGLOBAL&rhglobal){ //存放列集指针IStream*pStmPtr=NULL; //分配并封装内存块 HRESULThr=CreateStreamOnHGlobal(0,FALSE,&pStmPtr); if(SUCCEEDED(hr)) { //将列集对象写入内存块 hr=CoMarshalInterface(pStmPtr,IID_IMyFun,pRacer,MSHCTX_INPROC,0,MSHLFLAGS_TABLESTRONG); //抽出底层内存的句柄 if(SUCCEEDED(hr)) { hr=GetHGlobalFromStream(pStmPtr,&g_rhglobal); pStmPtr->Release(); } } SetEvent(g_heventWritten); returnhr;}散集实现函数:////散集函数HRESULTReadPtrFromGlobalVariable(HGLOBALrhglobal,IMyFun*&rpRacer){ IStream*pStmPtr=NULL; //现有内存封装块从输入传入 HRESULThr=CreateStreamOnHGlobal(g_rhglobal,FALSE,&pStmPtr);if(SUCCEEDED(hr)) { //获取对象有效指针 hr=CoUnmarshalInterface(pStmPtr,IID_IMyFun,(void**)&rpRacer); pStmPtr->Release(); } returnhr;}在实现函数中,先调用列集函数,然后在创立的线程执行函数中,就可以调用散集函数,从而实现对接口方法的调用。〔注意:组件要自己实现同步,保证线程平安〕COM库的内存管理COM应用程序中但凡涉及客户、COM库和组件三者之间内存交互〔分配和释放不在同一个模块中〕的操作,必须使用一致的内存管理器。COM库不仅提供了内存管理器,还提供了内存管理器的标准,应用程序可以按照COM标准指定的标准建立自定义内存管理器,以取代COM库的缺省内存管理器。下面是COM提供的内存管理器标准,实际上是一个COM接口IMalloc:MIDL_INTERFACE(MIDL_INTERFACE("00000002-0000-0000-C000-000000000046")IMalloc:publicIUnknown{public:virtualvoid*STDMETHODCALLTYPEAlloc(/*[annotation][in]*/__inSIZE_Tcb)=0;virtualvoid*STDMETHODCALLTYPERealloc(/*[annotation][in]*/__in_optvoid*pv,/*[annotation][in]*/__inSIZE_Tcb)=0;virtualvoidSTDMETHODCALLTYPEFree(/*[annotation][in]*/__in_optvoid*pv)=0;virtualSIZE_TSTDMETHODCALLTYPEGetSize(/*[annotation][in]*/__in_optvoid*pv)=0;virtualintSTDMETHODCALLTYPEDidAlloc(/*[annotation][in]*/__in_optvoid*pv)=0;virtualvoidSTDMETHODCALLTYPEHeapMinimize(void)=0;};IMalloc接口的三个成员函数Alloc、Realloc和Free的含义与C语言库中的malloc、realloc和free函数类似,分别用于内存分配、重新分配和释放内存的操作,而且成员函数的参数也与这些函数的参数根本一致。成员函数GetSize用于返回被申请内存的大小,以字节为单位计数,参数pv为通过Malloc或Realloc成员函数返回得到的内存指针。成员函数DidAlloc用于确定内存指针pv是否由该内存管理器所分配,假设返回1那么说明确是它所分配的,返回0表示不是它分配的内存。成员函数HeapMinimize使堆内存尽可能减小,以便把没有用到的内存交还给操作系统,供其他进程使用,可在系统性能优化时调用。在COM库初始化成功之后,不管是使用缺省内存管理器还是使用自定义的内存管理器,应用程序都可以使用COM进行内存分配或释放,为此COM库提供了两种操作方法:直接使用IMalloc接口指针。DWORDlength=MAX_LENGTH;DWORDlength=MAX_LENGTH;IMalloc*pIMalloc;HRESULThr;hr=CoGetMalloc(MEMCTX_TASK,&pIMalloc);if(hr!=S_OK) returnfailure;psz=pIMalloc->Alloc(length);pIMalloc->Release();COM库封装了三个API函数,可用于内存分配和释放,函数如下Void*CoTaskMemAlloc();VoidCoTaskMemFree();VoidCoTaskMemRealloc();我们可以直接使用这些函数,上面的代码可简化为:psz=CoTaskMemAlloc(length);if(NULL==psz) returnfailure;COM库内存管理原那么:输入型参数,调用方负责创立和销毁;输出型参数,由被调用方创立,调用方销毁;输入输出型参数,最初由调用者分配内存,然后由被调用者释放和再分配内存〔假设需要〕,调用者负责最后返回值的释放。COM效劳器创立过程进程内组件创立进程内组件创立一个ATL工程选择COM效劳器类型,进程内组件我们选择的应该是Dynamic-linklibrary(DLL)选中工程添加一个COM类,选择类型为ATLSimpleObject按照ATL简单对象向导对话框,设置COM类的根本属性线程模型已在1.6中介绍,此处选择Apartment。接口类型有两种:双重接口〔Dual〕和自定义接口〔Custom〕,此处选择Custom。完成上述步骤后,一个COM效劳器的框架文件根本已生成,之后就是添加自己的方法实现。编辑IDL文件,添加方法、结构或者增加接口。VS已生成一个IDL文件,在文件中已定义了COM类以及一个没有方法的接口。importimport"oaidl.idl";import"ocidl.idl";//此处添加自定义结构[ object, uuid(40BD768A-3A7F-49FE-B880-7F553D242E7D), pointer_default(unique)]interfaceIMyMath:IUnknown{//此处添加该接口提供的方法};[ uuid(6146099F-5B20-4079-BA8D-EBD22DD92B1C), version(1.0),]//此处添加接口libraryMathLib{ importlib("stdole2.tlb"); [ uuid(B755ED9E-03D4-45D6-B2A3-05FC64153283) ] coclassMyMath { [default]interfaceIMyMath;//添加接口 };};在IMyMath中添加方法〔注意:方法中涉及的结构体,必须在IDL中定义,定义方法在1.4节中已介绍。[[ object, uuid(40BD768A-3A7F-49FE-B880-7F553D242E7D), pointer_default(unique)]interfaceIMyMath:IUnknown{//此处添加该接口提供的方法[helpstring("方法Add")]HRESULTAdd([in]Element*pElement,[out]DWORD*pValue); [helpstring("方法Operate")]HRESULTOperate([in]Element*pElement,functionfun,[out]DWORD*pValue); [helpstring("方法Sum")]HRESULTSum([in]DWORDColCount, [in]DWORDRowCount, [in,size_is(ColCount*RowCount)]DWORD*pNums, [out,retval]DWORD*pResult);};编辑IDL文件后,还需在COM类中增加方法的定义及实现。//MyMath.h:DeclarationoftheCMyMath//MyMath.h:DeclarationoftheCMyMath//CMyMathclassATL_NO_VTABLECMyMath: publicCComObjectRootEx<CComSingleThreadModel>, publicCComCoClass<CMyMath,&CLSID_MyMath>, publicIMyMath,{/*...............................................................................................................................*/public: STDMETHOD(Add)(Element*pElement,DWORD*pValue); STDMETHOD(Operate)(Element*pElement,functionfun,DWORD*pValue); STDMETHOD(Sum)(DWORDColCount,DWORDRowCount,DWORD*pNums,DWORD*pResult);};//MyMath.cpp:ImplementationofCMyMath//MyMath.cpp:ImplementationofCMyMath#include"stdafx.h"#include"MyMath.h"STDMETHODIMPCMyMath::Add(Element*pElement,DWORD*pValue){//方法实现returnS_OK;}STDMETHODIMPCMyMath::Operate(Element*pElement,functionfun,DWORD*pValue){returnS_OK;}STDMETHODIMPCMyMath::Sum(DWORDColCount,DWORDRowCount,DWORD*pNums,DWORD*pResult){returnS_OK;}增加接口方法,首先编辑IDL文件[[ object, uuid(D4C79A78-7471-4B9B-8466-8B92F9962C35), oleautomation, nonextensible, pointer_default(unique)]interfaceIMyMath2:IUnknown{ [helpstring("方法Sub")]HRESULTSub([in]Element*pElement,[out]DWORD*pValue);};////在类定义中添加一行coclassMyMath { [default]interfaceIMyMath; interfaceIMyMath2;//添加该行} };为了在工程中实现新的接口,把新接口添加到COM类的继承列表,把接口添加到COM_MAP中。//MyMath.h:DeclarationoftheCMyMath//MyMath.h:DeclarationoftheCMyMathclassATL_NO_VTABLECMyMath: publicCComObjectRootEx<CComSingleThreadModel>, publicCComCoClass<CMyMath,&CLSID_MyMath>, publicIMyMath, publicIMyMath2//添加该行BEGIN_COM_MAP(CMyMath) COM_INTERFACE_ENTRY(IMyMath) COM_INTERFACE_ENTRY(IMyMath2)//添加该行END_COM_MAP()添加完接口后。即可继续在新接口中增加方法。进程外组件创立本地效劳同样使用ATL模板向导创立COM本地效劳,其步骤与创立进程内组件一致。但在工程选项时有所不同。在选择效劳器类型时,我们应选择第二项可执行程序〔EXE〕。在设置COM类的根本属性时,应注意选择线程模型类型。该组件为本地效劳,推荐选择Free类型。在1.7中已介绍过组件线程模型,Free类型十分强大但需要自己实现同步。设置好选项后,简单对象向导生本钱地COM效劳器的框架文件。之后,便可添加自己的实现。创立Windows系统效劳创立Windows系统效劳,在选择效劳器类型时,我们应选择第三项效劳Service〔EXE〕。使用ATL模板向导创立Windows效劳的根本框架后,此时我们应考虑COM效劳另一个特性。在多个客户端调用组件时,我们需要控制其被调用的组件都是同一个还是每个客户端都是各自一个COM组件。控制COM组件实例个数,我们可以使用DECLARE_CLASSFACTORY_SINGLETON宏来控制。添加了该宏后,就是实现单实例,多个客户端调用COM组件时,都是同一个组件。此时那么应该注意在COM组件中实现同步。该宏使用法如下:classclassATL_NO_VTABLECMyFun: publicCComObjectRootEx<CComMultiThreadModel>, publicCComCoClass<CMyFun,&CLSID_MyFun>, publicIMyFun{public: CMyFun() { }DECLARE_REGISTRY_RESOURCEID(IDR_MYFUN)//此处添加宏,控制实例个数DECLARE_CLASSFACTORY_SINGLETON(CMyFun)BEGIN_COM_MAP(CMyFun) COM_INTERFACE_ENTRY(IMyFun)END_COM_MAP()……………….………………..}COM组件的调用进程内组件调用因为进程内组件与客户程序运行在同一进程内,它们拥有相同的地址空间,所以客户程序和组件之间的调用非常简便。以下是实现MyMath组件客户端工程Math_Client的局部关键代码。首先必须包含MyMath工程所生成的Math_i.h和Math_i.c文件。//COM初始化,创立一个STA套间并和当前线程关联//COM初始化,创立一个STA套间并和当前线程关联 ::CoInitialize(NULL); //获取实例指针 IMyMath*pIMyMath=NULL;HRESULThr=::CoCreateInstance(CLSID_MyMath,NULL,CLSCTX_INPROC_SERVER,IID_IMyMath,(void**)&pIMyMath);成功获取实例指针后,便可直接用该指针调用接口方法。////构造参数结构,调用接口方法 Element*pElement=newElement; pElement->element1=99; pElement->element2=100; DWORDdwValue=0; pIMyMath->Add(pElement,&dwValue); functionf=fAdd; dwValue=0; pIMyMath->Operate(pElement,f,&dwValue); cout<<"Add,Value="<<dwValue<<endl; f=fSub; dwValue=0; pIMyMath->Operate(pElement,f,&dwValue); cout<<"Sub,Value="<<dwValue<<endl; f=fMul; dwValue=0; pIMyMath->Operate(pElement,f,&dwValue); cout<<"Mul,Value="<<dwValue<<endl; DWORDCol=4; DWORDRaw=5; DWORD*pNum=newDWORD[Col*Raw]; for(DWORDi=0;i<Col*Raw;i++) { pNum[i]=i; } dwValue=0; pIMyMath->Sum(Col,Raw,pNum,&dwValue); cout<<"Sum,Value="<<dwValue<<endl;同时,假设有多个接口,可以通过pIMyMath查询获取其他的接口指针。////查询获取其他接口指针IMyMath2*pIMyMath2=NULL; hr=pIMyMath->QueryInterface(IID_IMyMath2,(void**)&pIMyMath2); dwValue=0;//直接通过接口指针调用方法 pIMyMath2->Sub(pElement,&dwValue); cout<<"Sub,Value="<<dwValue<<endl;组件调用结束后,应调用Release()////调用接口指针减1操作pIMyMath->Release(); pIMyMath2->Release();//释放COM库资源 ::CoUninitialize();进程外组件调用本地效劳和Windows系统效劳都属于进程外COM组件,因此在客户端调用实现上根本没有区别。小节2.3中实例代码就是对的本地效劳COM组件的客户端调用实现,其中运用了列集散集的方法。在此,就介绍另一种客户端调用COM组件的方法实现——全局接口表。实现将全局接口表绑定到一个全进程范围内有效的接口指针的方法////定义全局变量IGlobalInterfaceTable*g_pGIT=0;HRESULTInitOnce(void){return::CoCreateInstance(CLSID_StdGlobalInterfaceTable, NULL, CLSCTX_INPROC_SERVER, IID_IGlobalInterfaceTable, (void**)&g_pGIT);}实现将接口指针注册到全局接口表方法。HRESULTWritePtrToGlobalVariable(IMyFun*pRacer)HRESULTWritePtrToGlobalVariable(IMyFun*pRacer){ //存放累计指针 externDWORDg_dwCookie; //读写线程同步事件 externHANDLEg_heventWritten; //将列集对象引用写入全局变量 HRESULThr=g_pGIT->RegisterInterfaceInGlobal(pRacer,IID_IMyFun,&g_dwCookie); //停止其他线程,指针可用 SetEvent(g_heventWritten); returnhr;}实现散集从全局接口列表获取接口指针方法。HRESULTReadPtrFromGlobalVariable(IMyFun*&rpRacer,HRESULTReadPtrFromGlobalVariable(IMyFun*&rpRacer,boolbLastUnmarshal){ externDWORDg_dwCookie; externHANDLEg_heventWritten; //等待其他线程通知指针可用 ::WaitForSingleObject(g_heventWritten,INFINITE); //从全局变量读取列集对象引用 HRESULThr=g_pGIT->GetInterfaceFromGlobal(g_dwCookie,IID_IMyFun,(void**)&rpRacer); //如果散集到最后,销毁指针 if(bLastUnmarshal) { g_pGIT->RevokeInterfaceFromGlobal(g_dwCookie); } returnhr;}通过合理调用上述方法,即可获得接口指针,就可以调用接口方法。具体是详见工程实例PrintTesst。跨平台调用COM组件在Windows系统中,一个64位进程不能加载一个32位DLL,同理一个32位进程也不能加载一个64位DLL。但是,64位Windows支持64位和32位进程〔包括本机或跨机〕间进程通信〔RPC〕。在64位windows中,一个进程外32位COM效劳能够与64位客户端进行通信,同样一个进程外64位COM效劳器也能与32位客户端进行通信。因此,当创立一个适合跨平台的COM组件时,应选择进程外COM组件。跨平台组件调用时最重要的是要注意接口方法中的数组参数传递,数组必须要用平安数组SAFEARRAY。SAFEARRAY的主要目的是用于automation中的数组型参数的传递。因为在网络环境中,数组是不能直接传递的,而必须将其包装成SafeArray。实质上SafeArray就是将通常的数组增加一个描述符,说明其维数、长度、边界、元素类型等信息。SafeArray也并不单独使用,而是将其再包装到VARIANT类型的变量中,然后才作为参数传送出去。在VARIANT的vt成员的值如果包含VT_ARRAY,那么它所封装的就是一个SafeArray,它的parray成员即是指向SafeArray的指针。SafeArray中元素的类型可以是VARIANT能封装的任何类型,包括VARIANT类型本身。下面介绍SAFEARRAY的操作方法:方法一:使用SafeArrayAllocDescriptor在栈上创立一维数组,并用SafeArrayAccessData访问数组。//创立SAFEARRAY数组//创立SAFEARRAY数组,每个元素为long型,该数组是一维数组longnData[10]={1,2,3,4,5,6,7,8,9,10};SAFEARRAY*pArray=NULL;HRESULThr=SafeArrayAllocDescriptor(1,&pArray);//创立SAFEARRAY结构的对象pArray->cbElements=sizeof(nData[0]);//长度应用字节长度pArray->rgsabound[0].cElements=10;pArray->rgsabound[0].lLbound=0;pArray->pvData=nData;pArray->fFeatures=FADF_AUTO|FADF_FIXEDSIZE;//FADF_AUTO指定在栈上分配数据,并且大小不可以改变〔固定为10〕//访问SAFEARRAY数组long*pValue=NULL;SafeArrayAccessData(pArray,(void**)&pValue);longLow(0),High(0);hr=SafeArrayGetLBound(pArray,1,&Low);//维数索引从1开始hr=SafeArrayG

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论