分享

[笔记]COM组件初识

 king9413 2013-06-21

COM是一种组件开发技术,实际是一种在二进制层上兼容的软件开发方法的规范,COM技术是与具体的编程语言无关的技术,只要是支持COM开发的开发工具都可以用来进行COM应用的开发,而它们在二进制上兼容的要求由各个开发工具来实现,绝大部分是由编译器实现的。

一、组件对象模型:

1.组件

COM是开发软件组件的一种方法,组件实际上是一些小的二进制可执行程序,它们可以给应用程序、操作系统以及其它组件提供服务。开发自定义的COM组件就如同开发动态的、面向对象的API。多个COM对象可以连接起来形成应用程序或组件系统。且组件可以在运行时刻,在不被重新链接或编译应用程序的情况下被卸下或替换掉。Microsoft的许多技术,如ActiveXDirectXOLE等都是基于COM建立起来的。

COM框架下,可以开发出各种各样的功能专一的组件,然后将它们按照需要组合起来,构成复杂的应用系统。

COM所含的概念并不只是在microsoft windows 操作系统下有小,COM并非一个大的API,它实际上像结构化编程及面向对象编程方法那样是一种编程方法。在任何一种操作系统中,开发人员均可以遵循“COM方法”。

将单个应用程序分隔成单独多个独立的部分,即组件,这样的好处是可以用新的组件取代已有的组件,且利用已有的组件,用户还可以快速的建立全新的应用。

传统的做法是将应用程序分割成文件、模块或类,然后将它们编译并链接成一个单模应用程序。它与组件建立应用程序的过程(称为组件构架)有很大不同。一个组件同一个微型应用程序类似,都是已经编译链接好并可以使用的二进制代码,应用程序就是由多个这样的组件打包而得到的。单模应用程序只有一个二进制代码模块。自定义组件可以在运行时刻同其他的足迹连接起来构成某个应用程序。

COM即组件对象模型,是关于如何建立组件以及如何通过组件建立应用程序的一个规范,说明了如何可动态交替更新组件。

所有的组件必须满足两个条件:组件必须动态链接;必须隐藏或封装其内部实现细节。动态链接是组件的一个至关重要的要求,消息隐藏是动态链接的一个必要条件。

2.接口

对于COM来讲,接口是一个包含一个函数指针数组的内存结构。每一个数组元素包含一个由组件所实现的函数地址。对于COM而言,接口就是此内存结构,其他东西均是COM不关心的实现细节。

C++中,可以用抽象类来实现COM接口,由于一个COM组件可以实现支持任意数目的接口,因此对于这样的组件,可以用抽象基类的多重继承来实现。用类来实现组件将比其他方法更为容易。

对于客户而言,一个组件就是一个接口集,客户只能通过接口才能和COM组件打交道,该接口就是IUnknownIUnknown接口定义包含在win32 SDK中的UNKNOWN.H头文件中:

interface IUnknown

{

virtual HRESULT-__stdcall QueryInterface(const IID& iid,void **ppv)=0;

virtual ULONG__stdcall AddRef()=0;

virtual ULONG__release()=0;

};

所有COM组件都要继承IUnknown,可以用IUnknown的接口指针来查询该组件的其他接口,并且每个接口的vtbl中的前3个函数都是QueryInterfaceAddRefRelease。这就使得所有的COM接口都可以被当作成IUnknown接口来处理。由于所有的接口都支持QueryInterface,故组件的任一个接口都可以被客户用来获取它所支持的其他接口。

在用QueryInterface将组件抽象成由多个相互独立的接口构成的集合后,还需要管理组件的生命期。这一点是通过对接口的引用计数实现的。客户并不能直接控制组件的生命期。当使用完一个借口而要用组件的另一个接口时,是不能将该组件释放的。对组件的释放可以由组件在客户使用完所有组件之后自己完成。IUnknown的另两个成员函数AddRefRelease的作用就是给客户提供一种让它指示何时处理完一个接口的手段。

AddRefRelease实现的是一种名为引用计数的内存管理技术。当客户从组件获得一个接口时,此引用数值将增加1;当使用完某个接口时,组件的引用计数值将减1,当引用计数值为0时,组件可以将自己从内存中删除。AddRefRelease可以增加和减少这一计数值。

3.创建组件

将组件分成多个接口只是将单模应用分隔成多个部分的第一步,组件需要被仿佛动态链接库(DLL)中。DLL是一个组件服务程序,或者说是发行组件的一种方式。组件可看作在DLL中实现的接口集。在客户获取某个组件接口指针之前,它必须先将相应的DLL装载到其进程空间中,并创建此组件。

由于客户组件需要的所有函数都可以通过某个接口指针而访问到,因此可以在DLL中引出CreateInstance函数就可以使用户调用它。之后,可以装载DLL并调用其中的函数,此功能可由COM库函数CoCreateInstance来实现。CoCreateInstance创建组件的过程是:传给它一个CLSID,然后它创建相应的组件,并返回指向所请求的接口的指针。但CoCreateInstance没有给客户提供一种能控制组件创建过程的方法,缺乏一定的灵活性。常用类厂来创建组件。类厂就是一个带有能够创建其他组件的接口的组件。客户先创建类厂本身,然后再用一个接口IClassFactory来创建所需的组件,然后还要用DllRegisterSeverwindows中注册该组件。

4.复用

COM组件可以被复用,它支持“接口继承”,这种继承指的是一个类继承其基类的类型或接口。抽象基类是一种最纯粹的接口继承,并且正好也被用来实现COM接口。在COM中,我们可以包容和聚合来对组件进行改造。

包容是在接口级完成的,外部组件包含指向内部接口的指针,此时外部组件只是内部组件的一个客户而已,它将使用内部组件的接口来实现它自己的接口,外部组件也可以通过将调用转发给内部组件的方法来重新实现内部组件所支持的某个接口。且外部组件还可以在内部组件代码的前后加上一些代码以对接口进行改造。

聚合时包容的一种变化形式。当外部组件聚合了某个内部组件的一个接口时,它并没有像包容那样重新实现此接口并显式地将调用请求转发给内部组件。外部组件直接把内部组件的接口指针返回给客户。使用这种方法,外部组件无需重新实现并转发接口中的所有函数。

包容和聚合为实现组件的复用提供了一种极具健壮性的机制,在组建架构下,客户与组件的实现完全隔离。

二、COM编程

1.使用COM接口

COM下,对对象的直接访问是不允许的,与对象的通信时通过定义接口而进行的。对访问对象进行限制使COM成为一个与环境和语言无关的模型。在COM的约定中没有限制用户一定要用C++,而其他编程语言CDELPHIVB也都可以编写COM应用程序。

接口是包含了一组函数的数据结构,通过这组数据结构,客户代码可以调用组件对象的功能,客户程序使用一个接口数据结构的指针来调用接口成员函数。接口指针实际上又指向另一个指针,第二个指针指向一组函数,称为接口函数表。接口函数表中每一项为4个字节的函数指针,每个函数指针与对象的具体实现联系在一起。

标准规定:COM中所有接口都是以“I”开头的,I即代表interface。每个对象都有IUnknown接口。

2.标识COM接口和对象

COM是面对对象的组件模型。COM提供给客户的是以对象形式封装起来的实体。COM组件的位置对客户来说是透明的,因为客户并不直接取访问COM组件,客户程序通过一个全局标志符进行对象的创建和初始化工作。如何在没有中心机构管理的情况下保证唯一性是解决标志符的要点。

COM指定接口和对象用128位数字来标识。这个128位数字叫做全局唯一标识符GUIDGUID用以标志两种类型的项目:用于标识接口的GUID叫做接口标识符interface identifier IID;用于标识某种类型的对象的GUID称为类标识符ClassID CLSID。随机性有两方面的特性保证:空间和时间。

可以使用GUIDGEN.EXT,获得一个属于自己的独一无二的GUID,这是一个图形化的UUIDGEN,可以把GUID拷贝到剪贴板,然后再把它们粘贴到用户的源代码中。

查看更多精彩图片

3.处理GUID

由于GUID的长度比较长,处理GUID比处理32位句柄要困难一些,下面是GUID的结构

 

typedef struct _GUID

{

 unsigned long Data1;

 unsigned short Data2;

 unsigned short Data3;

 unsigned char Data4[8];

}GUID;

Windows 注册表中,GUID的两边各加上一个花括号{}

Win32 SDK提供了以下几个用于处理GUID的函数:

CLSIDFromString:把字符串转化为CLSIDCoCreateGuid:产生新的GUIDIIDFromString:把字符串转化为IIDIsEqualCLSID:比较2CLSIDIsEqualGUID:比较两个GUIDIsEqualIID:比较2IIDStringFromCLSID:CLSID转化为字符串;StringFromCLSID:格式化GUID,并存入所提供的缓冲区;StringFromIID:IID转化为字符串。

4.使用IUnknown接口:

COM定义的每一个接口都必须从IUnknown继承过来,原因在于IUnknown接口提供的2个特性:生命期控制和接口查询。客户程序只能通过接口与COM对象进行通信,虽然客户程序可以不管对象内部的实现细节,但它要控制对象的存在与否。如果客户还要继续对对象进行操作,就必须保证对象能一直存在于内存中;如果客户对对象的操作已经完成,以后再也不需要对象了,则它必须及时地把对象释放掉,以提高资源的利用率。IUnknown引入了“引用记数”方法可以有效地控制对象的生存周期。

如果一个COM对象实现了多个接口,在初始时刻,客户程序不太可能得到该对象所有的接口指针,它只会拥有一个接口指针。如果客户程序需要其他的指针,那么它如何通过接口指针呢?IUknown使用了“接口查询”方法完成接口之间的跳转。

IUnknown定义(IDL):

Interface IUnknown

{

HRESULT QueryInterface([in]REFIID iid,[out] void **ppv);

ULONG ADDRef(void);

ULONG Release(void);

}

IUnknown包含了3种成员函数:QueryInterfaceAddRefRelease函数。QueryInterface用于查询COM对象的其他接口指针,函数AddRefRelease用于对引用计数进行操作。

关于接口的几点说明:

引用计数

COM对象的生存周期是由该对象所保存的内部引用计数值严格控制的。该计数值代表客户所创建的指向接口的指针的数目。

关于引用计数存在如下一些通用规则:在创建对象时,它的构造函数把引用计数设置为0;在指向接口的指针被提供给对象的客户时,创建该指针的函数将引用计数增加1AddRef函数实现);当不再使用指针时,内部引用计数值通过调用Release函数减少,这就使得每一个对象都可以知道有多少外部客户程序与自己连接在一起。当内部计数器的值为0时,没有别的程序使用该对象,这时它通常会销毁自己。

如果对象不是在堆上创建的,以上步骤可以进行优化。对静态创建的对象而言,引用计数的操作一般都是空操作。但是客户程序在使用该组件对象时仍然必须遵守引用计数的规则。

查询另一个接口

通过调用QueryInterface,可以获得指向对象所支持的任意接口的指针。与AddRefRelease一样,QueryInterface也是IUnknown接口的一部分,所以可以通过任一接口指针调用QueryInterface

QueryInterface有两个参数,该函数返回结果代码的句柄,例如:

IFTeChart *pTeChart

HRESULT hr;

hr=pIUnknown->QueryInterface(IID_IFTeChart,&*pTeChart);

if(FAILED(hr)){

//返回错误代码

}else{

//使用该接口

pTeChart->Release();

}

根据QueryInterface的实现,对象的行为必须遵守下列规则:

在成功调用了QueryInterface之后,在该函数把新的接口指针返回给调用程序之前,接口的引用计数值必须增加1;对于对象的某个实例,某个给定的接口总是返回相同的指针值,如果用户查询某对象的接口并得到一定的指针值,那么之后同一对象的接口上对该对象进行的任何QueryInterface调用都将返回相同的值;对象不允许扩充新的接口,如果某次QueryInterface调用成功,以后它总是返回正确的值。反之,如果某次调用它出错,则以后对它的调用将总是出错;接口层次必须是有序的,如果QueryInterface调用返回指向新接口的指针,则可以通过调用QueryInterface返回到前一个接口。

处理返回值

与大多数的COM接口和函数一样,QueryInterface返回的是HRESULT值。这是一个结构化的32位值,与通常的返回代码相比,这个值所包含的内容要丰富的多。函数可能会有不同的方式来表示判断一个函数是否成功,用户必须使用SUCCEEDEDFAILED宏来检测是否成功。

hr=pIUnknown->QueryInterface(IID_IFTeChart,&*pTeChart);

if(FAILED(hr)){

}

Hr=pIUnknown->GetData(&Data);

if(SUCCEEDED (hr)){}

5.创建COM对象

客户程序通过调用CoCreateInstance来创建COM对象的一个实例,并向想要创建的对象传送一个CLSID,比如:

IGraphBuilder *pGraph=NULL;

HRESULT hr=CoCreateInstance(CLSID_FilterGraph,NULL,CLSCTX_INPROC,IID_IGraphBuilder,(void**)&pGraph);

if(FAILED(hr))

{

//ERROR

}

上述程序中利用CoCreateInstance创建了一个IGraphBuidler对象,如果创建成功将返回指向IGraphBuilder对象的指针。

一般情况下,创建COM对象需要经历几个步骤。所有的COM对象都是由下面3种服务程序之一创建的:进程内服务程序,它在DLL中实现,并运行于客户程序的地址空间中;本地服务程序,它们是EXE文件,运行在客户程序的同一台计算机上,并且它们在自己的地址空间运行;远程服务程序,它们也是EXE文件,运行在网络上的某台计算机上。

使用类对象

每一个COM对象都与特定的对象相关联,这种特定的对象称为类对象class object。类对象也称为类工厂,它们负责创建COM对象的实例,每个类对象负责创建一个CLSID。如果用户正在创建几个CLSID服务程序,则必须为每个CLSID准备一个类对象。

所有的COM对象都是通过该COM对象的CLSID关联的类对象实现的,这就使得客户程序可以用一种标准方法来创建COM对象,客户程序可以只处理单个接口就行了。

IClassFactory接口需要类对象来实现;IClassFactory接口在标准的IUnKnown接口的基础是增加2个函数;CreateInstance放回一个接口指针,指向CLSID所代表的COM对象的新实例;LockServer要求类对象驻留在内存中,不要销毁自己。

代码如下:

STDMETODIMP CClassFactory::CreateInstance(LPUNKNOWN punk,PEFIID rlld,LPVOID *ppv)

{

*ppv=NULL;

if(punk!=NULL)

return ResultFromScode(CLASS_E_NOAGGREGATION);

CCTest *pTest=new CTest;

if(pTest==NULL)

return ResultFromScode(E_OUTOFMEMORY);

return pTest->QueryInterface(rlid,ppv);

}

在该段程序中,特定的类工厂CreateInstance函数创建CTest对象,在把接口指针放回调用程序之前,该程序对象执行第一次的QueryInterface。在每次使用windows 2000CoCreateInstance函数时,该函数都会为CLSID所指向的类对象在内部创建一个实例。用户可以执行相同的步骤:首先调用CoGetClassObjectAPI函数来获取指向类对象的指针,在用户获得指向对象上相关接口的指针后,就可以通过调用CreateInstance函数得到一个或多个COM对象实例。

HRESULT MyCoCreateInstance(REFCLSID retsid,LPUNKNOWN pUnkOuter,DWORD dwClsContent,REFIID riid,LPVOID ppv)

{

ICLassFactory *pcl=NULL;

HRESULT hr=CoGetClassObject(rcsid,dwClsContent,NULL,IID_IClassFactory,(void**)&pcf);

if(FALIED(hr))

return hr;

hr=pcf->CreateInstance(pUnkOuter,riid,ppvObj);

pcf->Release();

return hr;

}

如果用户要创建COM对象的多个实例,那么与每个实例调用一次CoCreateInstance比较,使用类对象来创建多个实例效率要高得多。

在注册表中查找类对象

包含COM组件的类对象的模块可以在系统注册表中找到,如果该CLSID是进程内服务程序,则该服务程序的DLL的路径存放在InProcServer32关键字中,例如

[HKEY_CLASSES_ROOT\CLSID\{32GUID}\InprocServer32]

缺省值为”D:\\Program Files\Common Files\\Microsoft shared\\DAO\\DAO350.DLL”.

客户程序通过注册表加载该动态链接库,如果用户把动态链接库放到系统的目录下将会加快加载该对象的速度。

.进程内服务程序的需求

除了类对象所需的函数以外,进程内服务程序必须实现3个函数:

DLLMain:它是模块的入口点,该函数所完成的唯一工作就是保存实例句柄以备程序使用;DLLGetClassObject,必须从模块中导出该函数,在创建新的COM类对象时,windows系统将调用该函数。

DLLCanUnloadNowwindows系统调用这个函数来测试DLL是否可以被加载。在创建过程中,模块所创建的每一个对象都会使用全局引用计数器值增加1,而销毁时则减1

 

三、创建COM程序

目前编写的COM程序可以通过MFC或者ATL来进行,MFC是创建COM程序的一种简单、一致的方法,但是ATL提供了一种框架来实现创建COM客户机和服务器所必须的样板文件代码。使用这两种方法创建COM程序各自有各各自的优缺点。

1.使用MFC创建COM程序

使用MFC使得开发windows应用程序比使用SDKAPI容易得多。MicrosoftMFC的基础上,增加了对即存框架的COM支持。由于MFC的开发者在增加越来越多的函数时必须保持框架的完整,同时,visual C++编译器那时还不支持模板,因此,它们不得不借助非模板的其他手段来将COM功能掺入它们的类中。Microsoft 通过加入一些虚函数到CCmdTarget类中和一些宏中,解决了此问题,使得MFC中实现了COM接口有了可能。

MFC内部的COM支持从CCmdTarget开始,CCmdTarget类实现了IUnknown接口,还包括了一个用于引用计数的成员变量(m_dwRef)和用于实现IUnknown6个函数:InternalAddRefInternalReleaseInternalQueryInterfaceExternalAddRefExternalReleaseExternalQueryInterfaceQueryInterface的两个版本,AddRefRelease支持COM聚合。InternalAddRefInternalReleaseInternalQueryInterface完成引用计数和QueryInterface操作,而ExternalAddRefExternalReleaseExternalQueryInterface代理控制聚合的对象。

MFC使用嵌套的类负荷策略实现COM接口。在MFC中,想实现COM接口的泪是从CCmdTarget类中派生的。每个由CCmdTarget派生出的类实现的接口得到它自己的嵌套类。MFC使用宏BEGIN_INTERFACE_PARTEND_INTERFACE_PART来产生嵌套类。

MFC还实现了表驱动的QueryInterfaceMFC接口映射的工作机理同它的消息映射基本相同:MFC的消息映射把一个windows消息和一个C++类中的函数相联系;MFC的接口映射把一个接口的GUID和一个表示此接口的特定的vptr的地址相联系。每个基于CCmdTarget类实现COM接口通过更多的宏:DECLARE_INTERFACE_MAPBEGIN_INTERFACE_MAPINTERFACE_PARTEND_INTERFACE_MAP来增加一个接口映射。

除了实现了IUnKnown接口,MFC还包括IClassFactory的一个标准实现。MFC通过若干宏提供此支持。MFC2个宏来提供类对象:DECLARE_OLECREATE_EXIMPLEMENT_OLECREATE_EX。在一个基于CCmdTarget的类中使用这些宏增加一个COleObjectFactory类型的静态成员到该类中。如果你看一下AFXDISP.HCOleObjectFactory的定义,将会看到用在COleObjectFactory中的MFC的嵌套类宏为实现IClassFactory2定义了一个嵌套类。IClassFactory::CreateInstanceMFC版本使用MFC的动态创建机制(DECLARE_DYNCREATEIMPLEMENT_DYNCREATE宏打开此功能)来实例化COM类。

最后MFC中实现一个分发接口,可以通过ClassWizard来实现该功能。ClassWizard中有一个按钮用于添加属性,另一个用于添加方法。在MFC中,IDispatch支持来自CCmdTarget类。IDispatchMFC实际实现在一个叫做COleDispatchImpl的类中,COleDispatchImpl派生自IDispatch,实现了所有4IDispatch函数:GetTypeInfoCountGetTypeInfoGetIDsNamesInvoke。由CCmdTarget派生的类通过调用EnableAutomationIDispatch vptr加入到它们的接口映射中,当客户在基于MFCCOM组建上调用IDispatchQueryInterface时,CCmdTarget交出链接在COleDispatchImpl上的vptr

每次使用ClassWizard将一个自动属性或者方法加入到一个类中时,同时也在该类的分发映射表中加入了一项,一个分发映射表是一个将DISPIDs(用来调用分发成员的符号)和它们攻人读的名字以及和实际完成这个工作的某些C++代码联系起来的简单表格。COleDispatchImpl的调用以及GetIDsOfNames函数通过在类的分发映射表中查找分发成员并分发DISPID相对应的函数来工作。MFC能为某些基于COM的高级技术如OLE文档、OLE拖放和自动操作提供非常好的支持。

2.使用ATL编写COM程序

ATLActiveX template library的缩写,是一套C++模板库。使用ATL能够快速地开发出高效、简洁的代码,同时对COM组件的开发提供最大限度的代码自动生成以及可视化支持。为了方便的使用,microsoftATL集成到visual C++开发环境中。

ATL的目标是使开发者不必重写IUnknownIDispatchIClassFactory和其他的分支将常规的DLLEXE变成基于COMDLLEXE。从这个角度讲,ATL是一个比MFC精简得多的框架,它设计和生成时就考虑了COM的支持,它使用基于模板的方法,通过继承ATL提供的模板,开发者可以加入各种COM功能片断。

ATL的原始COM支持是从对IUnknown的支持开始的。ATLIUnknown的实现分成两个部分:CComObjectRootEx是一个机遇模板的类,将线性模型作为其唯一参数。ATL2个处理引用计数的泪,用于处理不同的线性模型:CComSingleThreadModelCComMultiThreadModel。这些类每个都有一个递增和一个递减的函数。其区别在于CComSingleThreadModel用标准C++操作符(++--)实现递增和递减;而CComMultiThreadModel使用线程安全的InterlockedIncrementInterlockedDecrement函数来实现这两个功能。根据用来实例化CComObjectRootEx的模板参数,它能正确的运行给定的组件类型。像MFCATL使用基于表的查找机制实现QueryInterfaceCComObjectRootBase通过一个接口映射处理类的QueryInterface函数。BEGIN_COM_MAPEND_COM_MAP宏定义了一个借口映射的开始和结束。与MFC不同的是,ATL提供了17种途径来组成一个接口映射,例如使用从ATL的基于模板的接口实现类如IOleObjectImpl来的vptrs。这包含了那些从tear-off的类或者由聚合提供的类来的vptrs

ATL里,C++类通过继承CComObjectRootEx,指定它们想要的组件模型(记住MFCIUnknown支持是内建在CCmdTarget中的)变成COM类。

ATL的类对象(以及IClassFactory)支持也来自模板,而MFC的类对象支持通过COleObjectFactory和一些宏而有效。ATL的类对象支持来自CComCoClass/CComClassFactory类家族和CComCreator类家族。CComCoClass包含了类的GUID,定义了COM类的错误处理设施。CComCreator类提供了CreateInstance的实现,供CComClassFactory使用。对于MFC,可以通过若干宏,使所有这种支持有效。ATL包括DECLARE_CLASS_FACTORY,DECLARE_CLASS_FACTORY2,DECLARE_CLASS_FACTORY_AUTO_THREAD以及DECLARE_CLASS_FACTORY_SINGLETON等宏用来使各种具体的类工厂支持有效。

最后ATLIDispatch的支持还来自模板类其名字是IDispatchImpl。与MFCIDispatch来说,ATLIDispatch的支持更加容易和标准。MFC试用了一种hand-rolledIDispatch来实现,而ATL使用更加标准的方法来加载一个接口的类型信息并代表标准的类型库编译器。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多