第一章 概述
在Windows系统平台上,一个COM组件或者是一个DLL,或者是一个EXE文件.一组件可以
包含多个COM对象,并且每个COM对象可以由多个界面.?另外的组件或普通程序(即组件
的客户程序)?用?件的功能?,它首先?建一?COM?象或者通?其它途??得COM?象,
然后通???象所??的COM界面?用它所提供的服?.
COM用?种机制???象的重用.我?假定有??COM?象,?象1希望能重用?象2的
功能,我?把?象1??外部?象,?象2???部?象.
1).包容方式.?象1包含了?象2,??象1用到了?象2的功能?,它??的把??交??象2
?完成,?然?象1和?象2支持同?的界面,但?象1在??界面???上?用了?象2的??.
2).聚合方式.?象1只??的把?象2的界面?交?客?即可,?象1并?有???象2的界面,
但它把?象2的界面也暴露?客?程序,而客?程序并不知道?部?象2的存在.
第二章 COM?象和界面
COM定?的每一?界面都必??IUnknown?承??,其原因在于IUnknown接口提供了
??基本?非常重要的特性:生存期控制和接口查?.
第三章 COM的实现
如果用动态连接库的方式实现组件程序,则客户程序调用组件程序的服务时,会把组件程序装入到自己的进程中,所有客户程序和组件程序运行在同一个进程空间中,我们把这种组件程序称为进程内组件。实现组件程序的另一种形式是EXE程序,这种组件程序在被调用时有其自己的进程空间,所以客户程序和组件程序运行在不同的进程空间中,我们把这种组件程序称为进程外组件。
进程外组件程序和客户程序位于不同的进程空间之间,它们使用不同的地址空间,所以组件和客户之间的通信必须跨越进程边界,这就涉及到下列一些问题:
1)。一个进程如何调用另一个进程中的函数。
2)。参数如何从一个进程被传递到另一个进程中。
COM采用本地过程调用(local procedure call,LPC)和远过程调用的方法进行进程之
间的通信。
客户程序
(客户进程)
|
代理DLL
中代理对象
|
组件程序
(组件进程)
|
存根DLL
|
组件对象
|
图3。2 进程外组件与客户程序协作的结构图
代理DLL和存根DLL除了完成LPC调用之外,它还需要对参数和返回值进行翻译和传递,客户程序调用的参数,首先经过代理DLL的处理,它把参数以及其他的一些调用信息组装成一个数据包传递给组件进程,这个过程称为参数列集(marshaling);组件进程接收到数据包之后,要进行解包操作,把参数信息提取出来,这个过程被称为散集(unmarshaling);
然后再进行实际的接口功能调用。
进程内组件实现自注册的工作就是提供两个引出函数:DllRegisterServer,DllUnregisterServer。COM规范规定,支持自注册的进程外组件必须支持两个命令行参数:
/RegServer和/UnregServer,以便完成注册或注销工作。
在COM库中,有三个API函数可用于对象的创建,它们分别是CoGetClassObject,
CoCreateInstance和CoCreateInstanceEx.以上三个函数,我们按下面的原则进行选择:
1)。如果创建远程对象或者希望一次获取对象的多个接口指针,则选用CoCreateInstanceEx。
2)。如果我们希望获取类厂对象或者要调用类厂的某些成员函数,则选用CoGetClassObject,以便获得类厂对象,并对类厂对象进行操作。
3)。在其他情况下,使用CoCreateInstance函数创建对象,这是我们最常用的方法。
COM库函数的初始化:HRESULT CoInitialize(IMalloc *pMalloc);
COM库的终止函数:void CoUniintialize(void);
组件程序的装载是在客户创建第一个组件对象时进行的,组件程序的卸载是在最后一个组件对象被释放之后进行的,这两个动作并不由客户程序直接完成,而是在COM中完成的。
1. 进程内组件的装载
客户程序通过调用COM库的CoCreateInstance或者CoGetClassObject函数创建COM对象,在CoGetClassObject函数中,COM库根据系统注册表中的信息,找到类标识符CLSID对应的组件程序(DLL文件)的全路径,然后调用LoadLibrary函数(实际上是CoLoadLibarary),并调用组件程序中的DllGetClassObject引出函数,DllGetClassObject函数创建相应的类厂对象,并返回类厂对象的IClassFactory接口,至此CoGetClassObject函数的任务完成。然后客户程序或者CoCreateInstance函数继续调用类厂对象的CreateInstance成员函数,由它负责COM对象的创建工作。
2. 进程外组件的装载
在COM库的CoGetClassObject函数中,当它发现是EXE文件(由注册表组件对象信息中的LocalServer或LocalSever32值指定)时,COM库启动一个进程启动组件程序,并带上“/Embedding”命令行参数,然后等待组件程序;而组件程序在启动后,当它检测到“/Embedding”参数后,就会创建类厂对象,然后调用CoRegisterClassObject函数把类厂对象返回,由于类厂和客户程序运行在不同的进程中,所以客户程序得到的时类厂对象的代理对象。
从这个过程可以看出,进程内对象和进程外对象的不同创建过程仅仅影响了CoGetClassObject函数的实现过程,对于客户程序来说时透明的,所以在客户程序中,可以把所有的COM对象都按照进程内对象来出来和理解,这使得COM应用的编程更为简单,而且,在应用系统中由于COM的引入可使得进程之间的通信更为便利。
3. 进程内组件的卸载
4. 进程外组件的卸载。
表 2。1 COM库中一些常用函数
类别
|
函数
|
功能
|
初始化函数
|
CoBuildVersion
CoInitalize
CoUninitialize
CoFreeUnusedLibraries
|
获取COM库的版本号
COM库的初始化
COM库功能服务终止
释放进程中所有不再使用的组件程序
|
GUID相关函数
|
IsEqualGUID
IsEqualIID
IsEqualCLSID
CLSIDFromProgID
StringFromCLSID
IIDFromString
StringFromIID
StringFromGUID2
|
判断两个GUID是否相等
判断两个lID是否相等
判断两个CLSID是否相等
把字符串形式的对象标识转换为CLSID结构形式
把CLSID结构形式转换为字符串形式
把字符串形式的IID转换为IID结构形式
把IID结构形式转换为字符串形式
把GUID结构形式转换为字符串形式
|
对象创建函数
|
CoGetClassObject
CoCreateInstance
CoCreateInstanceEx
CoRegisterClassObject
CoRevokeClassObjce
CoDisconnectObject
|
获取对象的类厂
创建COM对象
创建COM对象,可以指定多个接口或远程对象
登记一个对象,以便其他应用程序可以连接到该对象
取消该对象的登记操作
断开其他应用与对象的连接
|
内存管理函数
|
CoTaskmemAlloc
CoTaskMemRealloc
CoTaskMemFree
CoGetMalloc
|
内存分配函数
内存重新分配函数
内存释放函数
获取COM库的内存管理器接口
|
第四章 COM特性
可重用性:包容和聚合
根据包容和聚合的不同结构,在选择重用模型时,可以依据这样的原则:在一个组件对象在行为上更类似于另一个组件对象的客户,并且它要调用第二个对象的某些对象接口的情况下,比较适合用包容模型,第一个对象包容第二个对象;如果一个现成的组件对象实现的接口与将要实现的对象的接口的行为完全一致,则采用聚合模型更为合适。
包容和聚合是COM中比较重要的概念,不过它们的原理还是比较简单的。
一、包容
如图所示:
图1
从上图可看出组件A自己实现了组件B的接口IB,只不过在它实现接口IB的时侯,其内部
可能重用了组件B的一些代码。组件B的内部实现对client是完全隐藏的,client看见的只是IB。对client来说,接口IB就是组件A提供的接口。可见包容的概念是相当简单自然的,它类似于设计模式中的“委托”模式。
二、聚合 NondelegationQueryInterface()。如图所示:
图2
从上图可看出组件A自己并没有实现组件B的接口IB。当client向组件A请求接口IB的指针时,组件A返回它内部聚合(已经用 CoCreateInstance创建好了)的组件B的接口IB。与包容相同的,对client来说,接口IB就是组件A提供的接口。但它们不同之处在于,聚合中组件A自己没有实现IB,client得到的是原封不同的组件B对IB的实现。而包容中client得到的是组件A自己对IB 的实现,它可能与组件B对IB的实现大相径庭。
聚合中令人感兴趣的一个问题是:IUnknown是怎么实现的。组件A并没有实现IB,那么如何才能使IB就象是组件A自身具有的呢?具体地说就是如何根据pIA查询出pIB,又如何根据pIB查询出pIA呢?而且别忘了组件B并不只是一个被聚合对象,它可以独立存在也可以去聚合别的组件。
其实解决方法也很简单。COM给出的解决方法是这样的:组件B实现两个IUnknown接口(这只是逻辑上的),分别叫委托IUnknown和非委托IUnknown,分别实现QueryInterface、AddRef、Release和NondelegationQueryInterface、NondelegationAddRef、 NondelegationRelease。它的查询过程如下图所示:
图3
注解如下:
(1)client通过pIB->QueryInterface()查询接口。
(2)若查询的是组件A实现的接口,则查询转到组件A实现的IUnknown接口,即调用组件A的QueryInterface()。
(3)若查询的是组件B自己的接口,则查询转到组件B实现的非代理IUnknown接口,即调用组件B的NondelegationQueryInterface()。
(4)client通过pIA->QueryInterface()查询接口。
(5)若查询的是组件B的接口,则查询直接转到组件B的非代理IUnknown接口,即调用组件B的NondelegationQueryInterface()。
就这么简单,还有一点要补充的是。组件A也可能被其他组件聚合,所以它也会有一个代理的IUnknown和一个非代理的IUnknown。另外组件A除了聚合组件B之外,也可能聚合组件C,所以在图3中组件A的IUnknown显然也应该是代理的IUnknown。
列集(marshaling),是指客户进程可以透明的调用另一进程中的对象成员函数的一种参数处理机制。代理对象用列集手段处理成员函数的参数,通过列集处理后得到一个数据包(数据流),然后通过一种跨进程的数据传输方法,比如共享内存方法,甚至是网络协议等,当数据包传输到对象进程后,存根代码用散集(unmarshaling)的方法把数据包参数解译出来,再用这些参数去调用组件对象;当组件对象成员函数返回后,存根代码又把返回值和输出参数列集成新的数据包,并把数据包传到客户进程,代理对象接收到数据包后,把数据包解译出来返回给客户函数,从而完成一次调用。
对指针的列集处理过程是:列集时,把指针所指的数据拷贝到数据包中;散集时,在进程中分配一块内存,把数据包中的数据拷贝到内存中,所得的内存地址即为散集的结果。
第5章 用Visual C++开发应用程序
与COM接口有关的一些宏的说明
宏
|
说明
|
DECLARE_INTERFACE(iface)
|
声明接口iface,它不从其他的接口派生
|
DECLARE_INTERFACE_(iface,baseiface)
|
声明接口iface,它从baseiface派生
|
STDMETHOD(method)
|
声明接口成员函数method,函数的返回类型为HRESULT
|
STDMETHOD_(type,method)
|
声明接口成员函数method,函数的返回类型为type
|
接口表映射
首先看一下CCmdTarget类中给出的接口映射表的宏定义。在CCmdTarget类的定义中,使用了DECLARE_INTERFACE_MAP()宏,其定义如下:
#define DECLARE_INTERFACE_MAP() /
private:/
staic const AFX_INTERFACEMAP_ENTRY _interfaceEntries[];/
proteced:
static AFX_DATA const AFX_INTERFACEMAP interfaceMap;/
static const AFX_INTERFACEMAP* PASCAL _GetBaseInterfaceMap();/
virtual const AFX_INTERFACEMAP* GetInterfaceMap() const;/
从宏定义可以看出,CCmdTarget定义了静态成员_interfaceEntries和静态的interfaceMap成员,以及静态成员函数_GetBaseInterfaceMap()和成员函数GetInterfaceMap.
下面给出宏定义中用到的数据结构:
struct AFX_INTERFACEMAP_ENTRY
{
const void* piid;
//the interface id(IID)(NULL for aggregate)
size_t nOffset;
//offset of the interface vtable from m_unknown
};
struct AFX_INTERFACEMAP
{
#ifdef _AFXDLL
const AFX_INTERFACEMAP *(PASCAL* pfnGetBaseMap)();//NULL is root class
#else
const AFX_INTERFACEMAP* pBaseMap;
#endif
const AFX_INTERFACEMAP_ENTRY *pEntry;
};
COM客户如何调用进程内组件
Client
CLSID clsid;
IClassFactory *pClf;
IUnknown* pUnk;
CoInitialize(NULL);
CLSIDFromProgID(“componentname”,&clsid);
COM
COM使用注册表,根据“组件名称”查找类ID
Client
CoGetClassObject(clsid,CLSTX_INPROC_SERVER,NULL,
IID_IClassFactory,(void**)&pClf);
COM
COM使用类ID在内存中寻找组件
If(组件DLL还没有加载)
{
COM从注册表获取DLL文件名
COM向进程内存中加载组件DLL
}
DLL Component
If(组件刚好被加载进来)
{
创建全局厂对象
调用DLL的InitInstance(只适用于MFC)
}
COM
COM带着传给CoGetClassObject的CLSID值,调用DLL的全局导出函数DllGetClassObject
DLL Component
DllGetClassObject返回IClassFactory*
COM
COM吧IClassFactory* 返回给客户
Client
pClf->CreateInstance(NULL,IID_IUnknown,(void**)&pUnk) ;
MFC接口宏
在嵌套类中实现接口,MFC中有一组宏能够完成这些工作。对于从MFC CCmdTarget类派生出的CSpaceship类来说,你可以在声明中使用下面的宏。
BEGIN_INTERFACE_PART(Motion,IMotion)
STDMETHOD_(void,Fly)();
STDMETHOD_(int&,GetPosition)();
END_INTERFACE_PART(Motion)
BEGIN_INTERFACE_PART(Visual,IVisual)
STDMETHOD_(void,Display)();
END_INTERFACE_PART(Visual)
DECLARE_INTERFACE_MAP()
在实现文件中,使用下列宏:
BEGIN_INTERFACE_MAP(CSpaceship,CCmdTarget)
INTERFACE_PART(CSpaceship,IID_IMotion,Motion)
INTERFACE_PART(CSpaceship,IID_IVisual,Visual)
END_INTERFACE_MAP
一个典型的接口成员函数看起来想下面这样:
STDMETHODIMP_(void) CSpaceship::XMotiion::Fly()
{
METHOD_PROLOGUE(CSpaceship,Motion)
pThis->m_nPosition += 10;
return;
}
不要忘记,你必须为每一个接口实现它的所有函数,包括QueryInterface,AddRef和Release。
MFC COleObjectFactory类
有一个类命名为ColeObjectFactory,它能够在运行期间创建任何指定类的对象,你所需做的只是在类声明中使用像下面这样的宏:
DECLARE_DYNCREATE(CSpaceship)
DECLARE_OLECREATE(CSpaceship)
并在实现文件中使用像下面这样的宏
IMPLEMENT_DYNCREATE(CSpaceship,CCmdTarget)
IMPLEMENT_OLECREATE(CSpaceship,”Spaceship”,0x692d03a3,
0xc689,0x11cc,0xb3,0x37,0x99,0xea,0x36,0xde,0x9e,0x4e)
包容和聚合实际上是使一个组件使用另一个组件的技术。对于这两个组件,分别将其称为外部组件和内部组件。在包容的情况下,外部组件将包含内部组件;而在聚合的情况下,
则称外部组件聚合内部组件。
IDispatch具有一个函数:Invoke,无论是C++还是VBA程序建立和使用的COM对象,都可以使用IDispatch::Invoke函数。Invoke并非IDispatch的唯一的成员函数,控制程序可能会调用另一个成员函数是GetIDsOfNames。
大多数自动化组件具有一个后缀名为TLB的二进制类型库文件。ClassWizard可以访问这个类型库文件,从而生成一个COleDispatchDriver的派生类。生成的这个控制程序类包含了针对组件的所有方法和属性的成员函数,这些属性和方法的分发ID都是预先设定的。
在生成了驱动程序类以后,可以在下面这样在视图类(或者是别的什么类)里插入一个前述的驱动程序类的对象:
CClockDriver m_clock;
然后,用下面的语句请求COM建立一个时钟组件对象:
m_clock.CreateDispatch(“Ex25c.Document”);
现在,可以调用分发驱动程序函数了:
m_clock.SetTime(ColeDateTime::GetCurrentTime());
m_clock.RefreshWin();
当m_clock超出范围时,其析构函数删除IDispatch指针。
使用编译程序#import指令的自动化客户程序
现在有一种编写自动化客户程序的全新方法,过去使用ClassWizard生成COleDispatchDriver的派生类,现在,你可以使用编译程序,从组件的类型库直接生成头文件和实现文件。对于时钟组件来说,客户程序包括一下语句:
#import ”../ex25c/debug/ex25c.tlb” rename_namespace(“ClockDriv”) using namespace ClockDriv;
编译程序生成(并处理)两个文件,ex25c.tlh和ex25c.tli.TLH文件包含IEx25c时钟驱动程序类的定义和下面这个智能指针的定义:
_COM_SMARTPTR_TYPEDEF(IEx25c,__uuidof(IDispatch));
TLI文件包含有成员函数的内联实现,下面显示一些函数代码:
Inline HRESULT IEx25c::RefreshWin()
{
return _com_dispatch_method(this,0x4,DISPATCH_METHOD,VT_EMPTY,NULL,NULL);
}
在自动化客户程序中,要像下面这样定义一个嵌入的只能指针成员:
在自动化客户程序中,要像下面这样定义一个嵌入的只能指针成员:
IEx25cPtr m_clock;
然后用这条语句建立一个时钟组件对象:
m_colck.CreateInstance(__uuidof(Document));
现在,你可以使用IEx25cPtr类重载的-》运算符来调用载TLI文件中定义的成员函数了
m_clock->PutTime(COleDateTime::GetCurrentTime());
m_clolc->RefreshWin();
当m_clock智能指针对象运行到范围之外时,它的析构函数会调用COM Release函数。
BSTR变量是这样的指针,它指向在前面带有字符数的零结束字符数组。Windows提供SysAllocString和SysFreeString函数来分配和删除BSTR对象。
Windows为VARIANT提供了一些有用的函数,所有的VARIANT函数都是全局函数,并都带有VARIANT*参数
函数
|
说明
|
VariantInit
VariantClear
VariantCopy
VariantCopyInd
VariantChangeType
|
初始化VARIANT
清除VARIANT
释放与目标VARIANT关联的内存,并复制源VARIANT
释放与目标VARIANT,并尽可能间接复制源VARIANT
改变VARIANT的类型
|
如果遇到包含有DATE的variant,可以按下面所示的方法使用COleVariant::ChangeType函数,以便将日期类型转换为字符串:
COleVariant vaTimeDate = date;
COleVariant vaTemp;
vaTemp.ChangeType(VT_BSTR,&vaTimeDate);
CString str = vaTemp.bstrVal;
TRACE(“date = %s/n”,str);