COM组件技术积累DOC.docx
《COM组件技术积累DOC.docx》由会员分享,可在线阅读,更多相关《COM组件技术积累DOC.docx(50页珍藏版)》请在冰豆网上搜索。
![COM组件技术积累DOC.docx](https://file1.bdocx.com/fileroot1/2023-1/1/c4de4b54-7428-4947-8399-7ea556262a96/c4de4b54-7428-4947-8399-7ea556262a961.gif)
COM组件技术积累DOC
第1章COM基本概念
§1.1什么是COM
所谓COM(ComponetObjectModel,组件对象模型),是一种说明如何建立可动态互变
组件的规范,此规范提供了为保证能够互操作,客户和组件应遵循的一些二进制和网络标准。
通过这种标准将可以在任意两个组件之间进行通信而不用考虑其所处的操作环境是否相同、使用的开发者语言是否一致以及是否运行于同一台计算机。
COM规范所描述的即是如何编写组件,遵循COM标准的任何一个组件都是可以被用来组合成应用程序的。
至于对组件采取的是何种编程语言则是无关紧要的,可以自由选取。
作为一个真正意义上的组件,应具备如下特征:
1)实现对开发语言的封装。
2)以二进制形式发布。
3)能够在不妨碍已有用户的情况下被升级。
4)在网络上的位置必须能够被透明的重新分配。
在Windows操作系统平台上,有一些用COM形式提供的组件模块极大地丰富了
Windows的功能,而且也使Windows功能扩展更加灵活,例如:
1)DiretX多媒体软件包。
它以COM接口的形式为Windows平台提供了强大的多媒体功能,现广泛用于游戏娱乐软件及其他多媒体软件的开发。
2)RDO(remotedataobjet,远程数据对象)和DAO(dataaccessobject,数据访问对象)数据库访问对象库。
它以COM自动化对象的形式为数据库应用提供了便捷的操作方法,特别适合于在BASIC语言或其他一些高级语言中使用。
而数据访问一致接口OLEDB/ADO(activedataobject,活动数据对象)更淋漓尽致地发挥了COM接口的作用。
3)InternetClientSDK。
它提供了一组COM库,为应用系统增加Internet特性提供了底层透明的一致操作。
其它还有一些组件如MAPI(messagingAPI,消息应用编程接口)、ADSI(activedirectory
serviceinterface,活动目录服务接口)等,它们都提供了一致、高效的服务。
从整个Windows操作系统来看,COM成了系统的基本软件模型,它带来的是灵活性和高效率,以及应用开发的一致性。
§1.2COM对象与C++对象比较
COM对象建立在二进制一级的基础上,而C++对象建立在源代码一级的基础上,但从特性上,可以作一比较。
1)封装性:
COM对象的数据成员的封装以组件模型为最终边界,对于用户是完全透明的、不可见
的;而C++对象的封装特性只是语义上的封装,对于对象用户是可见的。
2)可重用性
COM对象的可重用性表现在COM对象的包容和聚合,一个对象可以完全使用另一个另
一个对象的所有功能;而C++对象的可重用表现在C++类的继承性,派生类可以调用其父类的非私有成员函数。
3)多态性
C++对象的多态性体现了C++语言用类描述事物的高度抽象的特征;COM对象也具有
多态性,但这种多态性需要通过COM对象所具有的接口才能体现出来,就像C++对象的多态性需要通过其虚函数才能体现一样。
§1.3COM对象和接口
COM提供的是面向对象的组件模型,COM组件提供给客户的是以对象形式封装起来的
实体。
客户程序和COM组件程序进行交互的实体是COM对象。
COM对象包括属性(也称为状态)和方法(也称为操作),对象的状态反映了对象的存在,也是区别于其他对象的要素;而对象所提供的方法就是对象提供给外界的接口,客户必须通过接口才能获得对象的服务。
对于COM对象来说,接口是它与外界进行交互的唯一途径,因此,封装特性是COM对象的基本特征。
§1.3.1COM对象标识——CLSID
COM组件的位置对于客户来说是透明的,因为客户并不直接去访问COM组件,客户程序通过一个全局标识符进行对象的创建和初始化工作,这个全局标识符就是CLSID。
CLSID结构定义上与GUID一致。
COM规范采用了128位全局唯一标示符GUID。
这是一个随机数。
手工创建128位GUID或者编写程序来产生GUID是件很麻烦的事。
为此,MicrosoftVisualC++提供了两个工具实现这样的目的:
UUIDGen.exe和GUIDGen.exe,前者是一个命令行程序,后者是一个基于对话框的应用程序。
另外,COM库为我们提供了以下API函数可以产生GUID:
HRESULTCoCreateGuid(GUID*pguid)
下面为示例工程中.rgs文件中CLSID的定义:
Math.Obj.1=s'MyMathClass'
{
CLSID=s'{3B28F0D6-D029-484B-80D7-A946EB20E9BD}'
}
将示例工程的COM组件成功注册后,我们可以根据组件的CLSID在系统的注册表编辑器中找到组件的注册信息,如图1。
图1系统注册表
§1.3.2COM对象的数据类型
⏹HRESULT:
一个双字节的值,其最高位(bit)如果是0表示成功,1表示错误。
常见的HRESULT值:
HRESULT
值
含义
S_OK
0x00000000
成功
S_FALSE
0x00000001
函数成功执行完成,但返回时出现错误
E_INVALIDARG
0x80070057
参数有错误
E_OUTOFMEMORY
0x8007000E
内存申请错误
E_UNEXPECTED
0x8000FFFF
未知的异常
E_NOTIMPL
0x80004001
未实现功能
E_FAIL
0x80004005
没有详细说明的错误
E_POINTER
0x80004003
无效的指针
E_HANDLE
0x80070006
无效的句柄
E_ABORT
0x80004004
终止操作
E_ACCESSDENIED
0x80070005
访问被拒绝
E_NOINTERFACE
0x80004002
不支持接口
⏹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得到BSTR
SetSysString()
重新申请BSTR指针,并复制到CString中
CComBSTR函数
ATL的BSTR包装类,在atlbase.h中定义,具体查看MSDN
_bstr_t
C++对BSTR的封装,它的构造和析构函数分别调用SysAllocString和SysFreeString函数,其他操作是借用BSTRAPI函数。
⏹VARIANT:
具有跨语言的特性,它可以表示(存储)任意类型的数据,既包括了数据本身,也包含了数据的类型,定义在oaidl.h中。
structtagVARIANT{
VARTYPEvt;
Union{
shortiVal;//VT_I2
longlVal;//VT_I4
floatfltVal;//VT_R4
doubledblVal;//VT_R8
DATEdate;//VT_DATE
BSTRbstrVal;//VT_BSTR
…..
short*piVal;//VT_BYREF|VT_I2
long*plVal;//VT_BYREF|VT_I4
float*pfltVal;//VT_BYREF|VT_R4
double*pdbVal;//VT_BYREF|VT_R8
DATE*pdate;//VT_BYREF|VT_DATE
BSTR*pbstrVal;//VT_BYREF|VT_BSTR
};
};
typedeftagVARIANTVARIANT;
VARIANT使用示例:
VARIANTva;
:
:
VariantInit(&va);//初始化
Inta=2012;
Va.vt=VT_I4;//指明long数据类型
Va.IVal=a;//赋值
:
:
VariantClear();
Windows定义的VARIANT相关函数:
VariantInt——将变量初始化为VT_EMPTY;
VariantClear——消除并初始化VARIANT;
VariantChangeType——改变VARIANT的类型;
VariantCopy——释放与目标VARIANT相连的内存并赋值源VARIANT
COleVariant:
对VARIANT结构的封装
COleVariantv1(“Thisisatest”);//直接构造
COleVariantv2=“Thisisatest”;//结果时VT_BSTR类型,值为”Thisisatest”
COleVariantv3((long)2012);
COleVariantv4=(long)2012;//结果时VT_I4类型,值为2012
_variant_t:
用于COM的VARIANT类
§1.4描述性语言IDL和MIDL编译器
COM规范在采用OSF的DCE规范描述远程调用接口IDL(interfacedescriptionlanguage,
接口描述语言)的基础上,进行扩展形成了COM接口的描述语言。
接口描述语言提供了一种不依赖于任何语言的接口描述方法,因此,它可以成为组件程序和客户端程序之间的共同语言。
COM规范使用的IDL接口描述语言不仅可用于定义COM接口,同时还定义了一些常用的数据类型,也可以描述自定义的数据结构,对于接口成员函数,我们可以指定每个参数的类型、输入输出特性,甚至支持可变长度的数组的描述。
IDL中所有数据、方法、接口、类和库的特性都由属性信息来描述。
属性信息中由括号括起来,作为它们描述的对象的前缀。
1)in:
输入型参数,从调用者传递到被调用者,被调用者对输入型参数的更改不传回调用者。
2)out:
输出型参数,从被调用者返回调用者,而被调用者不关心参数的初始值。
3)In,out:
输入输出型参数在调用的时候传到被调用者,同时,被调用者可以对参数进行修改,这个修改在调用返回的时候会被复制回调用者。
(PS:
非指针类型一定是输入型参数。
输出型参数和输入输出型参数一定是指针类型)
4)retval:
返回一个与方法的物理HRESULT不相关的逻辑结果,与out一起使用,且只能有一个,放在参数的最后。
5)string:
参数所指向的是一个字符串类型参数,以Null终止。
6)size_is:
指针数组中元素个数由另一个参数说明。
7)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定义示例:
typedef
[
uuid(9CE9F449-3894-44AD-9F15-1DE67E915329),
version(1.0),
helpstring("Enumoffunction")
]
enumfunction
{
fAdd=0,
fSub,
fMul
}function;
IDL中struct定义示例:
typedef
[
uuid(9CE9F449-3894-44AD-9F15-1DE67E915329),
version(1.0),
helpstring("Enumoffunction")
]
enumfunction
{
fAdd=0,
fSub,
fMul
}function;
IDL中union定义示例:
typedef
[
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文件
§1.5IUnknown接口
COM定义的每一个接口都必须从IUnknow继承过来,其原因在于IUnknow接口提供了
非常重要的特性:
生存期控制和接口查询。
客户程序只能通过接口和COM对象进行通信,虽然客户程序可以不管对象内部的实现细节,但它要控制对象的存在与否。
§1.5.1QueryInterface接口方法介绍
按照COM规范,一个COM对象可以实现多个接口,客户端程序可以在运行时刻对COM对象的接口进行询问,如果对象实现了该接口,则对象可以提供这样的接口服务。
QueryInterface函数的说明:
HRESULTQueryInterface([in]REFIIDiid,[ou]void**ppV);
函数的输入参数iid为接口标识符IID,输出参数ppv为查询得到的结果接口指针,如果没有实现iid所标识的接口,则输出参数ppv指向空(NULL)。
函数的返回值为一个32位的整数,反映了查询的结果,其含义有三种情况:
1)S_OK,查到了指定的接口,接口指针存放在ppv输出参数中;
2)E_NOINTERFACE,对象不支持所指定的接口,*ppv为NULL;
3)E_UNEXPECTED,发生了意外错误,*ppv为NULL。
对于调用QueryInterface函数,COM规范给出了以下一些规则:
1)对于同一个对象的不同接口指针,查询得到的IUnknown接口必须完全相同。
也就是说每个COM对象的IUnknown接口指针是唯一的。
2)接口自反性。
对于一个接口查询其自身总应该成功
3)接口对称性。
如果从一个接口指针查询到另一个接口指针,则从第二个接口指针再回到第一个接口指针必定成功,如:
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);
4)接口传递性。
如果从第一个接口指针查询到第二个接口指针,从第二个接口指针可以查询到第三个接口指针,则从第三个接口指针一定可以查询到第一个接口指针。
5)接口查询时间无关性。
如果某一个时刻可以查询到某一个接口指针,则以后任何时候再查询到同样的接口指针,一定可以查询成功。
§1.5.2引用计数
IUnknown引入了“引用计数”(referencecounting)方法,可以有效地控制对象的生存周期,解决内存管理的问题。
COM对象通过引用计数来决定是否继续生存下去。
IUnknown的接口成员函数AddRef和Release分别完成引用计数的增1和减1操作。
如果一个COM对象实现了多个接口,则可以采用同样的计数技术,只要引用计数不为0,就表明该COM对象的客户仍然在使用它(前提是客户程序正确地操作了引用计数),它就继续生存下去;反之,如果引用计数减到0,则表明客户不再使用该对象了,于是它就可以被清除。
⏹需要调用AddRef方法的情形:
1)当把一个非空指针写到局部变量中时
2)当被调用方把一个非空接口指针写到方法或者函数的[out]或者[in,out]参数中时
3)当被调用方返回一个非空接口指针作为函数的实际结果时
4)当把一个非空接口指针写到对象的一个数据成员中时
5)注意:
QueryInterface内含AddRef,不需要再调用
⏹需要调用Release方法的情形:
1)在改写一个非空局部变量或者数据成员之前
2)在离开非局部变量的作用域(scope)之前
3)在被调用方要改写方法或者函数的[in,out]参数,并且参数的初始值为非空时。
注意,对于传入的out参数,不需要释放
4)在改写一个对象的非空数据成员之前
5)在离开一个对象的析构函数之前,并且这时还有一个非空接口指针作为数据成员
⏹特殊情况
1)当调用方把一个非空接口指针通过[in]参数传递给一个函数或者方法时,既不需要调用AddRef,也不需要调用Release,因为在调用堆栈中,临时变量的生命周期只是“用于初始化形式参数”的表达式的生命周期的一个子集。
§1.6重要概念:
套间
什么是套间?
根据《COM技术内幕》的观点,COM没有定义自己新的线程模型,而是
直接利用了Win32线程,或者说对其做了改造、包装。
《COM本质论》是这样定义的:
套间定义了一组对象的逻辑组合,这些对象共享一组并发性和冲入限制。
每个COM对象都属于某一个套间,一个套间可以包含多个COM对象。
MSDN上解释说,可以把进程中的组件对象想象为分成了很多组,每一组就是一个套间。
属于这个套间的线程,可以直接调用组件,不属于这个套间的线程,要通过代理才能调用组件。
最直接的说,COM库为了实现简化多线程编程的构想,提出了套间的概念。
套间是一个逻辑上的概念,它把Win32里的线程、组件等,按照一定的规则结合在一起,并且以此提供一种模式,用于多线程并发访问COM组件。
可以把套间看做COM对象的管理者,它通过调度,切换COM对象的执行环境,保证COM对象的多线程调用正常运行。
COM和线程不是包含关系,而是对应和关联关系。
§1.6.1单进程套间STA
Single-threadedApartments,一个套间只关联一个线程,COM库保证对象只能由这个线程访问(通过对象的接口指针调用其他方法),其他线程不得直接访问这个对象(可以间接访问,但最终还是由这个线程访问)。
COM库实现了所有调用的同步,因为只有关联线程访问COM对象。
如果有N个调用同时并发,N-1个调用处于阻塞状态,如图2。
对象的状态(也就是对象的成员变量的值)肯定是正确变化的,不会出现线程访问冲突而导致对象状态错误。
图2STA实现过程
//创建一个STA套间并和当前线程关联
:
:
CoInitialize(NULL);
//或者
:
:
CoInitializeEx(NULL,COINIT_APARTMENTTHREADED);
……….
………
……
:
:
CoUninitialize();
上述代码创建了一个STA,然后套间把当前的线程和自己关联在一起,线程被标记为套间线程,只有这个线程能直接调用COM对象。
§1.6.2多进程套间MTA
MultithreadedApartments,一个套间可以对应多个线程,COM对象可以被多个线程并发访问,如图3。
所以这个对象的作者必须在自己的代码中实现线程保护、同步工作,保证可以正确改变自己的状态。
图3MTA实现过程
//创建一个MTA套间并和当前线程关联
:
:
CoInitializeEx(NULL,COINIT_COINIT_MULTITHREADED);
………..
………
……
:
:
CoUninitialize();
第一次如此调用的时候,会创建一个MTA,然后套间把当前线程和自己关联在一起,线程被标记为自由线程。
以后第二个线程再调用(同一个进程中)的时候,这个MTA会把第二个线程也关联在一起。
一个MTA可以关联多个线程。
所有的关联线程都可以调用套间中的组件。
这就涉及到同步问题,需要组件编写者解决。
这个对于作为业务逻辑组件或干后台服务的组件非常适合。
因为作为一个分布式的服务器,同一时间可能有几个服务请求到达,如果排队进行调用,那么将是不能想象的。
§1.6.3NA套间
COM+为了进一步简化多线程编程,引入的中立线程套间概念。
NA/TNA/NAT,NeutralApartment/ThreadNeutralApartment/NeutralThreadedApartment。
这种套