序言:
这几天看了看ATL的接口映射宏,不知不觉看得比较深入了,突然就萌发了把它写出来的想法。ATL中定义了很多接口映射宏,有几个还是比较重要的,虽然好象没有必要把它所有的细节都弄得很清楚,但深入学习的过程中也可以顺带学一学其他的ATL类,对它的机制也可以更清楚一些,应该还是会有些好处的吧。我按照我学习的过程把它写出来,也 不知道大家能不能看懂。想模仿一下侯老师的手笔力争把其内部细节解释清楚,但也不敢大言不惭的美其名曰“深入浅出”,呵呵,只希望能对大家有所帮助了。
以后将分别介绍ATL中各个形式为COM_INTERFACE_ENTRY_XX的接口映射宏并将按照从易到难的顺序讲解,每一部分都将建立在前一部分的基础上。每一部分都将通过分析实际的调用函数堆栈来进行分析,堆栈的写法是从下向上。文中所涉及的代码都为略写,只列出相关部分。
一、COM_INTERFACE_ENTRY(x)
首先我们从一个最典型的应用开始:
定义一个最简单的ATL DLL:
编写一段最简单的查询接口代码:
执行客户代码,首先我们看看组件对象是如何被创建的。
函数调用堆栈一:
解释如下:
1:
CoCreateInstance(CLSID_MyObject, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnk);
其内部将调用OLE API函数CoGetClassObject(), 而CoGetClassObject则会通过 LoadLibrary(...)装入DLL,并调用DLL中的DllGetClassObject()函数。
2:
其中值得注意的是_Module变量,在DLL中定义了全局变量:
CComModule _Module;
ATL通过一组宏:
生成一个静态全局_ATL_OBJMAP_ENTRY型数组:ObjectMap[];
然后ATL又在
中初始化_Module //注意在有的情况下是在InitInstance()中初始化_Module
那么_Module初始化都做了些什么呢,其实他什么也没做,在CComModule::Init中,它调用AtlModuleInit(_ATL_MODULE* pM, _ATL_OBJMAP_ENTRY* p, HINSTANCE h),在其中关键的只有一句:pM->m_pObjMap = p;可见_Module仅仅是把这个全局对象映射数组 ObjectMap[]给存了起来。那么为什么可以通过_Module.GetClassObject得到类厂呢?其实关键在于我们的组件CMyObject继承的又一个基类CComCoClass! 在CComCoClass中缺省定义了一个宏DECLARE_CLASSFACTORY()而
CComCreator,CComObjectCached我们暂且不管,但一看到CComClassFactory,顾名思义,我们就知道我们要的类厂终于出现了!每个组件内部原来都有一个类厂对象。绕了一大圈,我们现在已经知道了_Module中包含了我们所要的每个组件的类厂对象,这对目前来说已经足够了,现在继续路由下去!
3:
CComModule::GetClassObject的实现非常简单,仅仅是调用ATL的API函数。
4:
现在好象已经有点看不懂了,看来我们得看看_ATL_OBJMAP_ENTRY的结构了
pclsid很清楚就代表着我们组件的CLSID;pfnGetClassObject我们也已经知道了它就是CMyObject::_ClassFactoryCreatorClass::CreateInstance(我们组件所包含的类厂对象的CreateInstance函数);pCF我们也可以猜出它是指向这个类厂的IUnknown指针,代表这个类厂对象是否被创建过,若类厂对象已经存在,就不用再创建新的类厂对象了。现在就剩下pfnCreateInstance我们还不明白怎么回事。其实答案还是在 CComCoClass中!
在CComCoClass中缺省定义了宏DECLARE_AGGREGATABLE(x),这个宏表示这个组件既可以是聚集的也可以是非聚集的,关于聚集的概念我们暂且不理,先看它的定义:
#define DECLARE_AGGREGATABLE(x) public:/
typedef CComCreator2< ccomcreator< CComObject< x > >, /
CComCreator< ccomaggobject< x > > > _CreatorClass;
我们看到了一个熟悉的字符串_CreatorClass, 原来这还有一个组件包含的对象。但还有一个问题我们没有搞清楚,就是为什么_ClassFactoryCreator和_CreatorClass后面都要跟着一个CreateInstance? 看来我们必须先来看看CComCreator是个什么东西了。
原来它里面只有一个CreateInstance函数,我们现在终于大体明白_ClassFactoryCreatorClass::CreateInstance 表示什么意思了,它就代表CComClassFactory::CreateInstance(..)吧,差不多就是这样了。那我们再来看看CComCreator2有什么不同:
这个类与CComCreator很类似,都只有一个CreateInstance成员函数,从_CreatorClass 中我们可以知道它实际上包含两个类CComObject,CComAggObject的CreateInstance函数(通过CComCreator),其中CComObject用于非聚集对象,CComAggObject用于聚集对象根据情况它建立相应的对象。(ATL中实际生成的组件对象不是CMyObject,而是 CComObject,CComAggObject或CComPolyObject对象,这个概念很重要,但现在暂且不谈) 现在我们对AtlModuleGetClassObject(...)基本已经知道是怎么回事了,它就是根据存在对象映射数组中的创建类厂的函数的地址来创建类厂。pfnGetClassObject以及 pfnCreateInstance我们基本上都已经知道是怎么回事了,但还有一个问题为什么要把pEntry->pfnCreateInstance作为pEntry->pfnGetClassObject(...)中的一个参数传递?答案在下面呢,让我们继续路由下去!
5:
注意这里的T1是CComObjectCached< ATL::CComClassFactory >,这是我们给CComCreator 的模板参数。我们又一次看到了我们熟悉的操作符'new'!直到现在我们终于创建了组件的类厂。但还没完,继续往下走,看看SetVoid(pv)里干了些什么?
大家还记得我们曾经把CMyObject::_CreatorClass::CreateInstance作为参数传给 pEntry->pfnGetClassObject(...)吧,当时我们不明白是怎么回事,现在已经豁然开朗!原来是类厂需要它来创建组件对象!虽然我们只是从字面意思猜出这一点,但实际上也正如我们所预料的那样,在 CComClassFactory::CreateInstance(...)中,我们看到了m_pfnCreateInstance(pUnkOuter, riid, ppvObj);现在一切都已经明白了, ATL为我们创建类厂而作的层层包装我们都已经打开,剩下的创建组件的过程已经是我们很熟悉的过程了!
但是现在还没有完,我们还需要为类厂对象查询一个IUnknown指针,这个指针就存在我们在前面所看到的pEntry->pCF中。
6:
现在调用的是CComObjectCached::QueryInterface,至于这个类有何特别之处,我们现在好象还不需要知道,我也很累的说,呵呵。
7:
所有的类的_InternalQueryInterface(...)都是在BEGIN_COM_MAP中定义的。 CComObjectCached没有BEGIN_COM_MAP宏,所以现在调用的是CComClassFactory的。注意把this指针和接口映射数组_GetEntries()传给了InternalQueryInterface(), 这是InternalQueryInterface(...)实现查询的依据。在BEGIN_COM_MAP(x)中定义了一个静态的接口映射数组:
_ATL_INTMAP_ENTRY _entries[];
每一个接口映射宏实际上都是向这个数组中增加了一项。一个接口映射宏包括三个部分:接口的IID号、偏移值(大部分时候下)、需要执行的函数,对一般接口来说不用执行其他函数。_GetEntries()就是返回这个数组。还有一些细节问题以后再说。
8:
现在调用的是CComObjectRootBase::InternalQueryInterface(...)
9:现在我们终于到了QueryInterface的鼻祖了。
AtlInternalQueryInterface(...)是整个查询过程的终点,它遍历接口映射表,并根据每一项做出相应的动作。ATL中的消息映射宏有很多种,相应的动作也很多,但现在我们不管那些,现在我们要做的就是查到一个IUnknown接口,这很容易,我们甚至不需要遍历接口映射表。
这里有一个规定,接口映射表的第一个接口必须是_ATL_SIMPLEENTRY型的。至于为什么有这个要求,以及pThis+pEntries->dw是什么意思,我们以后再说吧,那也是一堆问题。总之,我们现在如愿以偿轻松的获得了我们所需要的类厂对象以及IUnknown指针。
4:我差一点以为我们可以胜得返回到第一步了,但在ATL::AtlModuleGetClassObject 处却又停了下来,看看它的源码,原来还要再通过我们刚获得的IUnknown指针查询 IClassFactory指针。又是一通相同的调用,从第6步到第9步一模一样,我们将进行相同的调用。但注意在第9步中,我们这回查的不再是IUnknown指针了,所以我们需要看看我刚才还没列出的代码,但这留到下一次函数堆栈再看吧
1:终于终于我们已经完成了创建类厂对象的全部操作,现在我们要做的就是我们熟悉的调用类厂对象的CreateInstance(...)函数创建组件的过程了。正如我们所见到的,现在OLE开始调用CComClassFactory::CreateInstance()了,我们还没忘记,在类厂对象中保留了创建组件用的CreateInstance()函数, 这个过程已经很明朗了。
2.不用再重复了吧,看第4步。
3.不用再重复了吧,看第4步。
4.如果继续路由下去的话,我们的堆栈还可以很长,但这只是重复的枯躁的劳动。我就不继续走下去了,我也很累的说,唉。
函数调用堆栈二:
解释如下:
1.我们通过刚刚获得的组件对象的IUnknown接口指针来查询IMyObject指针,这才是我们真正需要的指针。
2.还记得我们说过ATL真正创建的组件并不是CMyObject,而是CComObject,CComAggObject 或CComPolyObject,这里我们创建的是CComObject.所以理所当然我们要调用 CComObject::QueryInterface(...),而确实CComObject也实现了这个函数。
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject)
{return _InternalQueryInterface(iid, ppvObject);}
它只是简单地调用_InternalQueryInterface(...),我们也说过,只有类里面申明了BEGIN_COM_MAP宏才会有_InternalQueryInterface(...),所以现在执行转到了它的父类CMyObject中去,所以将调用CMyObject::_InterfaceQueryInterface(...)
3.以后的调用我们已经很熟悉了,还用我再说一遍吗,呵呵
4.这个调用我们也很熟悉了,不用多说了吧
5.现在我们将要查询的是一个非IUnknown接口,所以我们来看看我们以前没列出的代码
函数的逻辑很清楚,只有两点可能不太理解,一个是 (IUnknown*)((int)pThis+pEntries->dw)是什么意思,另一个是pEntries->pFunc到底 要干些什么事。前一个问题将在讲述COM_INTERFACE_ENTRY2中讲述,后一个问题将在以后讲述不同类型的接口时分别解释。饭总是要一口一口吃的嘛,呵呵。 现在我们只需关心一下我们的IMyObject是怎么被查找的。看一下它的宏我们把COM_INTERFACE_ENTRY(IMyObject)解开以后形式为:
同样对于offsetofclass(IMyObject, CMyObject)我们也将留到下一次再讲。根据这个结构,我们很容易就能获得IMyObject接口指针。
0:OK,it is over.依次退栈返回。
其实这次查询发生的过程在刚才的调用序列中也发生了,当查询IClassFactory接口时就有类似的过程,但还是把它单独提了出来,只为了看看典型的情形,呵呵。
二、COM_INTERFACE_ENTRY2(x, x2)
ATL中是以多重继承的方式来实现组件的,但在继承树中如果有多个分支实现了同一个接口,当查询这个接口时就需要知道把哪个分支返回给它。这个宏就是干这个工作的通常这个宏是用于IDispatch接口。我们先来看看它的典型用法:
IDispatchImpl<...>这个类中实现了IDispatch接口,所以现在组件中有两个IDispatch 的实现。那查询IDispatch接口时,返回哪个实现呢?
我们再来看看COM_INTERFACE_ENTRY2(x, x2)的定义
现在问题就在于(DWORD)((x*)(x2*)((_ComMapClass*)8))-8是个什么意思?
我们先来考察一下下面一段代码:
这个继承图是个典型的菱形结构,在类A中保存有两个虚函数表指针,分别代表着它的两个分支。当为类A申明一个对象并实例化时,系统会为其分配内存。在这块内存的最顶端保留着它的两个虚函数表指针。分析程序运行的结果,可以看出,最后的结果4代表了指向接口A3的虚函数表指针与类A对象的内存块顶端之间的偏移量。
下面我们再看一个更为复杂点的继承关系:
类B将保留四个虚函数表指针,因为它共有四个分支。我们的目的是想获得B::B5::B2这个分支中的B2接口,最后的结果8正是我们所需要的,它表示在类B内存块的偏移量。
从上面两个例子中,我们已经明白了(DWORD)((x*)(x2*)((_ComMapClass*)8))-8的作用通过这个值我们能获得我们所需要的接口。
下面我们针对我们的实际情况COM_INTERFACE_ENTRY2(IDispatch, IOuter2)来分析一下
IDispatchImpl< class T,... >模板类从类T中派生,所以COuter要从两个它的模板类中继承, IOuter1、IOuter2都是双接口,即都是从IDispatch派生的类,所以可得COuter有两条分支,也是个菱形结构,所以按照我们的示例,这个偏移值也应该是4。为了证明我们的设想,我们再来通过函数堆栈来验证我们的结果。
函数堆栈:
解释:
1:这是我们的验证代码,pUnk是组件的IUnknown指针
2--5:这些代码我们现在都已经很熟悉了,我们只需再看看AtlInternalQueryInterface 的具体实现。
关键的一句话就是IUnknown* pUnk = (IUnknown*)((int)pThis+pEntries->dw); 通过观察变量,正如我们所料pEntries->dw=4。(int)pThis+pEntries->dw)保证了我们可以得到IOuter2分支的虚函数表,又因为IDispatch也是从IUnknown继承,在虚函数表的最顶端放的是IUnknown的虚函数指针,所以进行(IUnknown *)强制转换,可以获得这个虚函数表的顶端地址,这正是我们所需要的。或许会问为什么得到的是虚函数表的地址,而不是一个类实例的地址呢?别忘了,接口是没有数据的,它只有纯虚函数。对于客户来说,它只能通过接口定义的虚函数来访问它,而不可能访问实现接口的类的成员变量,组件的数据对客户来说是不可见的,所以只用得到虚函数表的地址就行了。
三、COM_INTERFACE_ENTRY_TEAR_OFF(iid, x)
使用这个宏的目的就是为了把一些很少用到的接口放在一个单独的组件中实现,仅当查询到这个接口时,才创建这个组件,并且当它的引用计数减为0时就会被释放掉。我们知道ATL中组件是通过多重继承实现的,每继承一个接口,在为它分配的内存块中就会多一个虚函数表指针,用这个宏就可以为每个组件的实例节省下这一个虚函数表指针来(一个指针4个字节,好象也不多啊,呵呵)
下面我们来看它的典型用法:
CTearOff1实现了Tear-off接口ITearOff1,实现方法与其他组件并无不同。唯一不同的是它从CComTearOffObjectBase继承,CComTearOffObjectBase定义如下:
我们又看到了我们熟悉的一个类CComObject,它是组件的真正生成类。从上面的定义中可知道CComTearOffObjectBase主要功能就是包含了一个指向外部对象(在这里就是我们的组件类CComObject)的指针。它的功能将在后面看到。
我们继续用我们的老办法来跟踪一下看看它的执行过程。假设pOuter是我们已经获得的组件的IOuter接口指针。
执行pOuter->QueryInterface(IID_ITearOff1, (void **)&pTear1);
函数堆栈一:
解释:
1--4:这些代码已经遇到过很多次了,我们还是集中精力看看核心代码:
当在COuter的接口映射数组中找到ITearOff1后,因为它不是一个简单接口,所以要执行pEntries->pFunc(....)。
我们先来看看COM_INTERFACE_ENTRY_TEAR_OFF的定义:
看不太明白,还是继续我们路由得了
5:原来_Creator是CComObjectRootBase的静态成员函数,它可是COuter的一个基类啊,所以才可以这样写而不会编译出错。看看它的实现吧:
源代码都列出来了,不用我多说,大家也都能看懂了。继续路由吧
6:绕了一大圈,现在我们调用的应该是
同我们所见到的大多数Creator类一样,它也只有一个静态CreateInstance函数。现在我们终于可以创建我们分割组件了,它不是CTearOff1,它也是经了一层包装的,是 CComTearOffObject! 现在我们再来看看它的构造函数干了些什么事:
还记得CTearOff1是从CComTearOffObjectBase继承的吗,这个基类包含了一个成员变量m_pOwner,现在它被赋值为指向它的外部对象的指针了。
7.现在终于把这个实现分割接口的组件创建了,剩下的在CTearOff1中查询ITearOff1的工作已经是重复劳动了,不再赘述。
执行pTear1->QueryInterface(ITearOff1, (void **)&pTear2)
一个实现分割接口的组件有可能包含多个分割接口,我们来检测一下它的查询过程。
函数堆栈二:
解释:
1:
还记得我们创建的分割组件是CComTearOffObject< CTearOff1 >吗?现在执行查询操作的是它的成员函数。它的实现很简单,事实上它什么也没做,仅仅是把它交给它的外部对象(即CComObject< COuter >)去做了。还记得m_pOwner是在构造函数里赋值的吧。现在是否感到有些不妙呢?呵呵
2、3:果然,现在已经不用再看下去了,剩下的将是重复我们在调用第一条查询操作所做的一切。这个过程很简单,但它也隐含说明了一点:若对一个实现分割接口的组件每查询一次它的接口,就会调用一个新的实例!!!在上例中,最后的结果pTear2和pTear1 是不一样的!!这显然是浪费!
在下一节中,我们将介绍一个可以解决这个问题的宏!
四.COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(iid, x, punk)
这个宏与上一节所讲的COM_INTERFACE_ENTRY_TEAR_OFF宏最主要的不同就在于,当查询分割对象中其他接口时,不会再新建新的对象。下面还是先看看它的典型用法:
CTearOff2实现了分割接口ITearOff2,它的类定义与上一节所看见的CTearOff1一模一样可见不管是哪种分割接口,实现都是一样的,不同的地方在于COuter。在COuter中增加了一个成员变量m_pUnkTearOff2作为宏的一个参数。
我们继续用老办法跟踪它的内部执行过程,假设pOuter是已经获得的组件COuter有接口IOuter指针。
执行pOuter->QueryInterface(IID_ITearOff2, (void **)&pTear1);
函数堆栈一:
解释:
1:pOuter->QueryInterface(IID_ITearOff2, (void **)&pTear1);
2-5:这段代码见到很多次了,不用再讲了,现在程序执行到
HRESULT hRes = pEntries->pFunc(pThis, iid, ppvObject, pEntries->dw); 看来我们得看看这个宏的定义才能知道pFunc是执行的什么功能了。
#define COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(iid, x, punk)/
{&iid,/
(DWORD)&_CComCacheData</
CComCreator< CComCachedTearOffObject< x > >,/
(DWORD)offsetof(_ComMapClass, punk)/
?gt;::data,/
_Cache},
与我们上一节见的宏的定义不太一样,还是先跟踪下去再说。
6:原来在BEGIN_COM_MAP中也定义了_Cache函数:
static HRESULT WINAPI _Cache(void* pv,REFIID iid,void** ppvObject,DWORD dw )/
{/
......
HRESULT hRes = CComObjectRootBase::_Cache(pv, iid, ppvObject, dw);/
......
}/
7:看看CComObjectRootBase::_Cache的源码:
现在问题的关键是dw了,dw是从pEntries->dw传过来的。我们得看一下宏定义中的
是什么意思。
template < class Creator, DWORD dwVar >
_ATL_CACHEDATA _CComCacheData< Creator, dwVar >::data = {dwVar, Creator::CreateInstance};
CComCreator我们在前面已经见过它的定义了,它只有一个成员函数
CComCachedTearOffObject是这个宏与上一节所讲宏不同的关键所在,因为它包含了一个CComContainedObject的对象。这个对象的作用在查询的时候再讲。
我们再来看看offsetof的定义:
#define offsetof(s,m) (size_t)&(((s *)0)->m)
对(DWORD)offsetof(_ComMapClass, punk)来说,就是punk在_ComMapClass类中的偏移值。
现在来看看_ATL_CACHEDDATA是什么东西。
要注意的是从_Cached()函数传进来的参数dw就是_ATL_CACHEDATA结构的变量,所以可知道(DWORD)pv + pcd->dwOffsetVar)得到的就是在类COuter中定义的m_pUnkTearOff2的偏移值,所以IUnknown** pp就是指向m_pUnkTearOff2的一个指向指针的指针。
而pdc->pFunc()则会调用CComCreator< CComCachedTearOffObject < x > >::CreateInstance
8:下面将调用CComCreator::CreateInstance,将创建一个CComCachedTearOffObject<>的对象实例。其构造函数定义如下:
这里contained就是CTearOff2,contained::_OwnerClass就是COuter,可见m_contained保存了外部对象的指针。
9:创建完对象后,将查询接口ITearOff2
注意,这里把查询工作交给了m_contained,也就是一个CComContainedObject对象。不过现在查询的是IUnknown指针,别忘了,我们在COuter中还定义了一个IUnknown指针呢,现在查询的就是它!!
8:经过一系列退栈,退到_Cache()中,现在还要继续查询ITearOff2接口。是根据我们刚刚查询到的IUnknown指针查询ITearOff2。所以再一次进入 ATL::CComCachedTearOffObject< CTearOff2 >::QueryInterface(...),不过这回将调用的是m_contained._InternalQueryInterface(...)了。
9:因为CComContainedObject m_contained的基类是CTearOff2,所以将调用CTearOff2::_InternalQueryInterface(...)
剩下的操作就没什么特别之处了,仅仅一般的查询操作。
执行pTear1->QueryInterface(ITearOff2, (void **)&pTear2);
函数堆栈二:
解释:
1:第一步就可能使我们迷惑了,为什么执行的是CComContainedObject::QueryInterface
在上一节中,执行的是ATL::CComTearOffObject< CTearOff1 >::QueryInterface(...),所以我们也自然而然的猜想,这里应该执行的是CComCachedTearOffObject的函数。但是来看看CComCachedTearOffObject的定义:
原来CComCachedTearOffObject没有从contained类(在这里就是CTearOff2)中继承,而 CComTearOffObject却是从CTearOff1继承的!所以我们刚才得到的pTear1就不可能是CComCachedTearOffObject的对象。而实际上,CComContainedObject是从CTearOff2继承的,在上面的函数堆栈中第9步查询ITearOff2接口时,把工作交给了m_contained, 这是个CComContainedObject< CTearOff2 >对象,所以实际上最后查询得到的ITearOff2 指向的是CComContainedObject< CTearOff2 >对象。所以现在执行的会是 CComContainedObject::QueryInterface(...)!!!
2:
把查询工作交给外部对象完成,也就是COuter。
第一、二步的功能与上一节中所讲的一样,都是交给外部对象去处理,不同之处在下面3-8:COuter中的查询过程与上例中无异,我们可以直接跳到第8步中
还记得我们在COuter中定义了一个IUnknown指针m_pUnkTearOff2吧,我们在第一次查询 ITearOff2接口时,创建了CTearOff2对象,并查询一个IUnknown指针给了它.现在它就发挥作用了,在_Cache中将判断如果m_pUnkTearOff2不等于空,则表明CTearOff2已经创建就不会再创建它了,而是直接用它去查询接口.
9:所以现在将调用CComCachedTearOffObject< CTearOff2>::QueryInterface(...),在上一个函数堆栈中的第9步中我们已经看到了这个QueryInterface(...)的代码,它把查询工作交给m_contained._InternalQueryInterface(.),其实因为CComContainedObject中没有定义BEGIN_COM_MAP宏,所以也没有定义_InternalQueryInterface(),所以实际上调用的是它包含的类的函数,即CTearOff2::_InternalQueryInterface(...)
10-12:以下的工作就很简单了,不再赘述。
总结:
COM_INTERFACE_ENTRY_CACHED_TEAR_OFF是个相对比较麻烦的宏,它与上一节介绍的宏相比不同之处就在于创建分割接口对象的过程只用进行一次,如果对象已经创建,则下一次查询该对象的接口时不会再创建一个新的分割对象。为了达到这个目的,它在外部对象中包含了一个IUnknown指针,并在第一次创建分割对象时查询这个IUnknown指针,这样就可以通过判断这个指针是否为空来知道这个分割对象是否已经创建,从而决定是否创建新的分割对象,并通过它去查询分割对象内其它接口。这里特别需要注意的是,实际上有两个对象被创建,一个是CComCachedTearOffObject< CTearOff2 >,另一个是 CComContainedObject< CTearOff2 >。并且第一个对象内部实现了第二个对象,真正的查询工作也是交给第二个对象去做。COuter::m_pUnkTearOff2是前面一个对象的IUnknown指针,当用它去查询ITearOff2时,实际上是交给了其内部对象m_contained去做了,这在第8、9步可以看得很清楚。
终于把这个宏讲完了,我感觉这个宏可能是ATL接口映射宏中实现最复杂的了,其实它并没有利用到CComContainedObject的真正功能。感觉实现这个宏也许不应这么麻烦的。
五.COM_INTERFACE_ENTRY_AGGREGATE(iid, punk)
这一节中将介绍ATL中用于聚集对象的宏。聚集对象的概念请参阅其它参考书。
现在先看一看这个宏的典型用法:
CAgg是一个聚集类,它的实现与一般的ATL组件没有区别,只是注意在它的类定义中不要加入DECLARE_NO_AGGREGATABLE.
COuter包含了聚合组件CAgg,它包含了几个不同之处:
(1)加入了COM_INTERFACE_ENTRY_AGGREGATE(IID_IAgg, m_pUnkAgg.p)宏。
offsetof我们在上一节中已经见过,可以猜到它求的就是punk在类中的位置。也就是m_pUnkAgg在COuter中的位置。
(2)加入了宏DECLARE_GET_CONTROLLING_UNKNOWN(),其定义为:
我们也没必要继续深究下去,仅从字面意思就可以看出这个函数将返回组件的IUnknown 指针。
(3)在COuter中加入一个成员变量:CComPtr< IUnknown > m_pUnkAgg; m_pUnkAgg将用于获得被聚集组件的IUnknown指针。
(4)重载了FinalConstruct,FinalRelease
当创建组件COuter后将会调用FinalConstruct,所以会在这里创建聚集组件。原则上聚集组件可以仅在需要的时候才创建,但也可以随着包含它的组件一起创建。聚集组件的创建没什么特别之处,只是要注意它将查询IUnknown指针,并返回给m_pUnkAgg.外部组件将通过m_pUnkAgg操作聚集组件。另外注意到使用pUnkOuter作为CoCreateInstance的参数,这将导致创建CComAggObject< COuter >对象,内部包含一个CComContainedObject的包含对象。与上一节中的CComCachedTearOff<>类似,CComAggObject< COuter >也不是从COuter派生的,所以真正的组件对象不是CComAggObject< COuter >对象,而是它内部包含的CComContainedObject< COuter >对象。同样pUnkOuter得到的将是CComAggObject<>的IUnknown指针,也同样调用它的QueryInterface会转而调用CComContainedObject的_InternalQueryInterface函数(呵呵,现在可都还是我猜的,看我猜的对不对吧)
运行pOuter->QueryInterface(IID_IAgg, (void **)&pAgg1)
函数堆栈一:
解释:
1-5:这几步函数调用我们已经见了很多次了,因为在这个宏定义使用了_Delegate,所以将调用CComObjectRootBase::_Delegate(...).
第二句话的含义我们在上一节中已经见过了,最后的结果p=COuter::m_pUnkAgg.
6:正如我们刚才所料,现在调用的是CComAggObject< CAgg >::QueryInterface()
也正如我们所料,将交给它的包含对象去做.(这段代码在上一节好象也见过是吧,呵呵)
7-9:同上一节一样,将交给CAgg::_InternalQueryInterface(...),剩下的工作将由CAgg完成了。最后返回的指针实际上将是CComContainedObject< CAgg >组件的接口指针。
运行pAgg1->QueryInterface(IID_IAgg, (void **)&pAgg2)
函数堆栈二:
解释:
1-9:浏览整个堆栈,与我们上一节所见的堆栈二太相近了,这是因为都是使用了包含对象。包含对象起了个代理的作用,他先把查询交给外部对象(COuter)去做(第1,2步), 当外部对象发现要查询的是聚集组件的接口时(IAgg),就会再把查询交还给它保留的聚集组件的指针(m_pUnkAgg,第7步中,注意这不是真正的聚集组件),m_pUnkAgg再把查询交给包含对象(第8步中),包含对象再把查询交给真正实现接口的类CAgg(第9步). 若外部对象发现要查询的是外部组件的接口时,那就很简单了,直接查询就行了。这样就防止了外部组件与聚集组件查询操作的不一致性。唉,真个过程真麻烦,不过还好,与上一节的宏很类似。相关的源码可参看上一节。
六、COM_INTERFACE_ENTRY_AGGREGATE_BLIND 参ATL例程COMMAP
上一节我们讲了COM_INTERFACE_ENTRY_AGGREGATE,这节要介绍的宏与它很类似。
从定义上就可以看出,它与上一节介绍宏的唯一区别就在于,它没有指明接口ID!!
所以在它的定义中第一项也是NULL。
这个宏的用法与我们COM_INTERFACE_ENTRY_AGGREGATE一模一样。大家可以参考上一节内容以及ATL的例程COMMAP。
我们来看看AtlInternalQueryInterface()中的相关代码。
若没指定接口ID,也继续执行后面的操作,可见即使并非我们所需要的IID,也会执行_Delegate.
从上可见,这个宏适用于一个聚集组件有多个接口的情况,这样只要是查询这个聚集组件的接口,就会进入_Delegate函数。但要特别注意的是这个宏的位置!! 比如若是这样的顺序:
当查询IOuter接口时就会出错!!!
七、COM_INTERFACE_ENTRY_AUTOAGGREGATE(iid, punk, clsid)
先看看这个宏的定义:
先看看它的典型用法:
与一般的组件并无二样。
与宏COM_INTERFACE_ENTRY_AGGREGRATE(_)不同,COuter不用在FinalConstruct中创建聚集组件。外部组件会自动创建聚集组件!!!
1。
2。
3。
因为_Cache,_CComCacheData,CComAggregateCreator这几个类和函数我们已经在前面见过或者见过类似的,所以就不再多讲了。总之我们可以看到,若m_pUnkAutoAgg.p不为空则直接查询,否则创建聚集组件。
与宏COM_INTERFACE_ENTRY_AGGREGATE相比,这个宏似乎更好一些,仅当需要时才会创建,使用更简单。
八、COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND( punk, clsid )
看看它的定义:
呵呵,这个宏综合了 COM_INTERFACE_ENTRY_AUTOAGGREGATE() 和 COM_INTERFACE_ENTRY_AGGREGATE_BLIND() 的特点,既可以自动创建也可以很方便地查询聚集组件中的多个接口。不再赘述!!
九、COM_INTERFACE_ENTRY_CHAIN(classname) 参ATL例程COMMAP
先看看它的定义:
典型用法:
它与一般的组件无异。
我们对查询的过程已经很熟悉了,可以直接来看看_Chain的功能。_Chain()是CComObjectRootBase的成员函数:
我们再看看宏定义中的dw部分:
基本上我们已经看懂是怎么回事了,void *p将得到基类的指针,InteralQueryInterface 我们已经很熟悉了,_Chain把基类的指针以及基类的接口映射宏传给它,实际上是查询基类的接口!!!
一般情况下把这个宏放在BEGIN_COM_MAP和END_COM_MAP之间的最后面,这表示只有在当前类中查不到接口时才去查父类的接口。不过也经常把它放在第一位,这时就是先去查父类接口,只有父类没有实现这种接口时才查自己。在ATL中组件是以多重继承的方式实现的,ATL定义了很多类实现了一些常用的接口,这些类经常被做为组件的基类,所以这个宏被大量使用。
所有重要的宏我们都已经讲过了,剩下的都是些很简单的宏了.呵呵,还是把它们都罗列一下,善始善终嘛.
十、COM_INTERFACE_ENTRY_IID(iid, x)
十一、COM_INTERFACE_ENTRY2_IID(iid, x, x2)
从定义上看这两个宏与COM_INTERFACE_ENTRY()和COM_INTERFACE_ENTRY2()相比,都只是多了一项"iid"。没有别的好处,只不过由用户明确指出接口IID,而不用系统根据接口名字去转换了。
十二、COM_INTERFACE_ENTRY_FUNC( iid, dw, func )
还记得AtlInternalQueryInterface()中的代码吗?如果在接口映射表中找到了我们要找的接口,并且这个接口不是_ATL_SIMPLEENTRY型的,则执行宏定义中的指定的函数。
这个宏就给我们提供了自己编写处理函数的功能。这个函数必须是如下定义:HRESULT WINAPI func(void* pv, REFIID riid, LPVOID* ppv, DWORD dw);
当AtlInternalQueryInterface调用func时,会传进相关的信息。pv是类对象的指针,riid是要查询的接口,ppv是要返回查询得到的接口指针,dw是在宏定义中指定的参数。另外如果函数中不打算返回接口指针,则应把ppv赋为NULL,并返回S_FALSE或 E_NOINTERFACE。返回S_FALSE刚会继续查找下去,若返回E_NOINTERFACE则会终止查询。若返回接口指针,则应返回S_OK.
十三、COM_INTERFACE_ENTRY_FUNC_BLIND(dw, func)
至于_BLIND类型的特点可以看前面几节。
十四、COM_INTERFACE_ENTRY_NOINTERFACE(x)
原来它只是返回E_NOINTERFACE,并且将终止查询。
哈哈,看来是不想让别人查到这个接口啊!!!
十五、COM_INTERFACE_ENTRY_BREAK(x)
如果查到这个接口将调用DebugBreak(),并返回S_FALSE,继续查询下去。DebugBreak()是什么效果大家自己试试吧,一定很熟悉的,呵呵。
至此全部十五个接口映射宏我们都已经讲完了,唉,真是不容易,特别是前面几个宏跟踪起来很麻烦。因为文本方式的限制,所以很多东西不容易表达清楚。有些叫法也是我自己这么叫的,可能与别人的习惯不同。没办法,大家将就将就了,呵呵。
这几天看了看ATL的接口映射宏,不知不觉看得比较深入了,突然就萌发了把它写出来的想法。ATL中定义了很多接口映射宏,有几个还是比较重要的,虽然好象没有必要把它所有的细节都弄得很清楚,但深入学习的过程中也可以顺带学一学其他的ATL类,对它的机制也可以更清楚一些,应该还是会有些好处的吧。我按照我学习的过程把它写出来,也 不知道大家能不能看懂。想模仿一下侯老师的手笔力争把其内部细节解释清楚,但也不敢大言不惭的美其名曰“深入浅出”,呵呵,只希望能对大家有所帮助了。
以后将分别介绍ATL中各个形式为COM_INTERFACE_ENTRY_XX的接口映射宏并将按照从易到难的顺序讲解,每一部分都将建立在前一部分的基础上。每一部分都将通过分析实际的调用函数堆栈来进行分析,堆栈的写法是从下向上。文中所涉及的代码都为略写,只列出相关部分。
一、COM_INTERFACE_ENTRY(x)
首先我们从一个最典型的应用开始:
定义一个最简单的ATL DLL:
class ATL_NO_VTABLE CMyObject : public CComObjectRootEx, public CComCoClass, public IDispatchImpl { ..... BEGIN_COM_MAP(CMyObject) COM_INTERFACE_ENTRY(IMyObject) //一个双接口 COM_INTERFACE_ENTRY(IDispatch) END_COM_MAP() ..... }; |
编写一段最简单的查询接口代码:
IUnknown *pUnk; IMyObject *pMyObject; CoCreateInstance(CLSID_MyObject, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnk); pUnk->QueryInterface(IID_IMyObject, (void **)&pMyObject); |
执行客户代码,首先我们看看组件对象是如何被创建的。
函数调用堆栈一:
4........... 3.ATL::CComCreator< ATL::CComObject< CMyObject > >::CreateInstance(...) 2.ATL::CComCreator2< ATL::CComCreator< ATL::CComObject< CMyObject > >, ATL::CComCreator< ATL::CComAggObject< CMyObject > > >::CreateInstance(...) 1.ATL::CComClassFactory::CreateInstance(...) 4.ATL::AtlModuleGetClassObject(...) 9.ATL::AtlInternalQueryInterface(...) 8.ATL::CComObjectRootBase::InternalQueryInterface(...) 7.ATL::CComClassFactory::_InternalQueryInterface(...) 6.ATL::CComObjectCached::QueryInterface(...) 5.ATL::CComCreator >:: CreateInstance(...) 4.ATL::AtlModuleGetClassObject(...) 3.ATL::CComModule::GetClassObject(...) 2.DllGetClassObject(...) 1.CoCreateInstance(...)(客户端) |
解释如下:
1:
CoCreateInstance(CLSID_MyObject, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **)&pUnk);
其内部将调用OLE API函数CoGetClassObject(), 而CoGetClassObject则会通过 LoadLibrary(...)装入DLL,并调用DLL中的DllGetClassObject()函数。
2:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) { return _Module.GetClassObject(rclsid, riid, ppv); } |
其中值得注意的是_Module变量,在DLL中定义了全局变量:
CComModule _Module;
ATL通过一组宏:
BEGIN_OBJECT_MAP(ObjectMap) OBJECT_ENTRY(CLSID_MyObject, CMyObject) END_OBJECT_MAP() #define BEGIN_OBJECT_MAP(x) static _ATL_OBJMAP_ENTRY x[] = { #define OBJECT_ENTRY(clsid, class) {&clsid, class::UpdateRegistry, / class::_ClassFactoryCreatorClass::CreateInstance, ///关键 class::_CreatorClass::CreateInstance, / NULL, 0, class::GetObjectDescription, / class::GetCategoryMap, class::ObjectMain }, #define END_OBJECT_MAP() {NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL}}; |
生成一个静态全局_ATL_OBJMAP_ENTRY型数组:ObjectMap[];
然后ATL又在
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/ { ..... _Module.Init(ObjectMap, hInstance, &LIBID_TEST2Lib); ..... } |
中初始化_Module //注意在有的情况下是在InitInstance()中初始化_Module
那么_Module初始化都做了些什么呢,其实他什么也没做,在CComModule::Init中,它调用AtlModuleInit(_ATL_MODULE* pM, _ATL_OBJMAP_ENTRY* p, HINSTANCE h),在其中关键的只有一句:pM->m_pObjMap = p;可见_Module仅仅是把这个全局对象映射数组 ObjectMap[]给存了起来。那么为什么可以通过_Module.GetClassObject得到类厂呢?其实关键在于我们的组件CMyObject继承的又一个基类CComCoClass! 在CComCoClass中缺省定义了一个宏DECLARE_CLASSFACTORY()而
#define DECLARE_CLASSFACTORY() DECLARE_CLASSFACTORY_EX(CComClassFactory) #define DECLARE_CLASSFACTORY_EX(cf) typedef CComCreator< ccomobjectcached< cf > > _ClassFactoryCreatorClass; |
CComCreator,CComObjectCached我们暂且不管,但一看到CComClassFactory,顾名思义,我们就知道我们要的类厂终于出现了!每个组件内部原来都有一个类厂对象。绕了一大圈,我们现在已经知道了_Module中包含了我们所要的每个组件的类厂对象,这对目前来说已经足够了,现在继续路由下去!
3:
HRESULT CComModule::GetClassObject(REFCLSID rclsid,REFIID riid,LPVOID* ppv) { return AtlModuleGetClassObject(this, rclsid, riid, ppv); } |
CComModule::GetClassObject的实现非常简单,仅仅是调用ATL的API函数。
4:
ATLINLINE ATLAPI AtlModuleGetClassObject(_ATL_MODULE* pM, REFCLSID rclsid, REFIID riid, LPVOID* ppv) { _ATL_OBJMAP_ENTRY* pEntry = pM->m_pObjMap;//从_Module中取出对象映射数组 while (pEntry->pclsid != NULL) { if ((pEntry->pfnGetClassObject != NULL) && InlineIsEqualGUID(rclsid, *pEntry->pclsid)) { if (pEntry->pCF == NULL) { hRes = pEntry->pfnGetClassObject(pEntry->pfnCreateInstance, IID_IUnknown, (LPVOID*)&pEntry->pCF); } if (pEntry->pCF != NULL) hRes = pEntry->pCF->QueryInterface(riid, ppv); break; } pEntry = _NextObjectMapEntry(pM, pEntry); } } |
现在好象已经有点看不懂了,看来我们得看看_ATL_OBJMAP_ENTRY的结构了
struct _ATL_OBJMAP_ENTRY { const CLSID* pclsid; HRESULT (WINAPI *pfnUpdateRegistry)(BOOL bRegister); _ATL_CREATORFUNC* pfnGetClassObject; _ATL_CREATORFUNC* pfnCreateInstance; IUnknown* pCF; DWORD dwRegister; _ATL_DESCRIPTIONFUNC* pfnGetObjectDescription; _ATL_CATMAPFUNC* pfnGetCategoryMap; } |
pclsid很清楚就代表着我们组件的CLSID;pfnGetClassObject我们也已经知道了它就是CMyObject::_ClassFactoryCreatorClass::CreateInstance(我们组件所包含的类厂对象的CreateInstance函数);pCF我们也可以猜出它是指向这个类厂的IUnknown指针,代表这个类厂对象是否被创建过,若类厂对象已经存在,就不用再创建新的类厂对象了。现在就剩下pfnCreateInstance我们还不明白怎么回事。其实答案还是在 CComCoClass中!
在CComCoClass中缺省定义了宏DECLARE_AGGREGATABLE(x),这个宏表示这个组件既可以是聚集的也可以是非聚集的,关于聚集的概念我们暂且不理,先看它的定义:
#define DECLARE_AGGREGATABLE(x) public:/
typedef CComCreator2< ccomcreator< CComObject< x > >, /
CComCreator< ccomaggobject< x > > > _CreatorClass;
我们看到了一个熟悉的字符串_CreatorClass, 原来这还有一个组件包含的对象。但还有一个问题我们没有搞清楚,就是为什么_ClassFactoryCreator和_CreatorClass后面都要跟着一个CreateInstance? 看来我们必须先来看看CComCreator是个什么东西了。
template < class T1 > class CComCreator { public: static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv) {..... } }; |
原来它里面只有一个CreateInstance函数,我们现在终于大体明白_ClassFactoryCreatorClass::CreateInstance 表示什么意思了,它就代表CComClassFactory::CreateInstance(..)吧,差不多就是这样了。那我们再来看看CComCreator2有什么不同:
template < class T1, class T2 > class CComCreator2 { public: static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv) { return (pv == NULL) ? T1::CreateInstance(NULL, riid, ppv) : T2::CreateInstance(pv, riid, ppv); } }; |
这个类与CComCreator很类似,都只有一个CreateInstance成员函数,从_CreatorClass 中我们可以知道它实际上包含两个类CComObject,CComAggObject的CreateInstance函数(通过CComCreator),其中CComObject用于非聚集对象,CComAggObject用于聚集对象根据情况它建立相应的对象。(ATL中实际生成的组件对象不是CMyObject,而是 CComObject,CComAggObject或CComPolyObject对象,这个概念很重要,但现在暂且不谈) 现在我们对AtlModuleGetClassObject(...)基本已经知道是怎么回事了,它就是根据存在对象映射数组中的创建类厂的函数的地址来创建类厂。pfnGetClassObject以及 pfnCreateInstance我们基本上都已经知道是怎么回事了,但还有一个问题为什么要把pEntry->pfnCreateInstance作为pEntry->pfnGetClassObject(...)中的一个参数传递?答案在下面呢,让我们继续路由下去!
5:
CComCreator::CreateInstance(void* pv, REFIID riid, LPVOID* ppv) { T1* p = NULL; ATLTRY(p = new T1(pv))//创建类厂对象 if (p != NULL) { p->SetVoid(pv); p->InternalFinalConstructAddRef(); hRes = p->FinalConstruct(); p->InternalFinalConstructRelease(); if (hRes == S_OK) hRes = p->QueryInterface(riid, ppv); if (hRes != S_OK) delete p; } } |
注意这里的T1是CComObjectCached< ATL::CComClassFactory >,这是我们给CComCreator 的模板参数。我们又一次看到了我们熟悉的操作符'new'!直到现在我们终于创建了组件的类厂。但还没完,继续往下走,看看SetVoid(pv)里干了些什么?
void CComClassFactory::SetVoid(void* pv) { m_pfnCreateInstance = (_ATL_CREATORFUNC*)pv; } |
大家还记得我们曾经把CMyObject::_CreatorClass::CreateInstance作为参数传给 pEntry->pfnGetClassObject(...)吧,当时我们不明白是怎么回事,现在已经豁然开朗!原来是类厂需要它来创建组件对象!虽然我们只是从字面意思猜出这一点,但实际上也正如我们所预料的那样,在 CComClassFactory::CreateInstance(...)中,我们看到了m_pfnCreateInstance(pUnkOuter, riid, ppvObj);现在一切都已经明白了, ATL为我们创建类厂而作的层层包装我们都已经打开,剩下的创建组件的过程已经是我们很熟悉的过程了!
但是现在还没有完,我们还需要为类厂对象查询一个IUnknown指针,这个指针就存在我们在前面所看到的pEntry->pCF中。
6:
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) {return _InternalQueryInterface(iid, ppvObject);} |
现在调用的是CComObjectCached::QueryInterface,至于这个类有何特别之处,我们现在好象还不需要知道,我也很累的说,呵呵。
7:
HRESULT _InternalQueryInterface(REFIID iid, void** ppvObject) / { return InternalQueryInterface(this, _GetEntries(), iid, ppvObject); } |
所有的类的_InternalQueryInterface(...)都是在BEGIN_COM_MAP中定义的。 CComObjectCached没有BEGIN_COM_MAP宏,所以现在调用的是CComClassFactory的。注意把this指针和接口映射数组_GetEntries()传给了InternalQueryInterface(), 这是InternalQueryInterface(...)实现查询的依据。在BEGIN_COM_MAP(x)中定义了一个静态的接口映射数组:
_ATL_INTMAP_ENTRY _entries[];
每一个接口映射宏实际上都是向这个数组中增加了一项。一个接口映射宏包括三个部分:接口的IID号、偏移值(大部分时候下)、需要执行的函数,对一般接口来说不用执行其他函数。_GetEntries()就是返回这个数组。还有一些细节问题以后再说。
8:
static HRESULT WINAPI InternalQueryInterface(void* pThis, const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject) { ... HRESULT hRes = AtlInternalQueryInterface(pThis, pEntries, iid, ppvObject); ... } |
现在调用的是CComObjectRootBase::InternalQueryInterface(...)
9:现在我们终于到了QueryInterface的鼻祖了。
AtlInternalQueryInterface(...)是整个查询过程的终点,它遍历接口映射表,并根据每一项做出相应的动作。ATL中的消息映射宏有很多种,相应的动作也很多,但现在我们不管那些,现在我们要做的就是查到一个IUnknown接口,这很容易,我们甚至不需要遍历接口映射表。
ATLINLINE ATLAPI AtlInternalQueryInterface(void* pThis, const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject) { ATLASSERT(pEntries->pFunc == _ATL_SIMPLEMAPENTRY); if (ppvObject == NULL) return E_POINTER; *ppvObject = NULL; if (InlineIsEqualUnknown(iid)) // use first interface { IUnknown* pUnk = (IUnknown*)((int)pThis+pEntries->dw); pUnk->AddRef(); *ppvObject = pUnk; return S_OK; } ...//还有一大堆呢,但现在用不上,就节省点空间吧 } |
这里有一个规定,接口映射表的第一个接口必须是_ATL_SIMPLEENTRY型的。至于为什么有这个要求,以及pThis+pEntries->dw是什么意思,我们以后再说吧,那也是一堆问题。总之,我们现在如愿以偿轻松的获得了我们所需要的类厂对象以及IUnknown指针。
4:我差一点以为我们可以胜得返回到第一步了,但在ATL::AtlModuleGetClassObject 处却又停了下来,看看它的源码,原来还要再通过我们刚获得的IUnknown指针查询 IClassFactory指针。又是一通相同的调用,从第6步到第9步一模一样,我们将进行相同的调用。但注意在第9步中,我们这回查的不再是IUnknown指针了,所以我们需要看看我刚才还没列出的代码,但这留到下一次函数堆栈再看吧
1:终于终于我们已经完成了创建类厂对象的全部操作,现在我们要做的就是我们熟悉的调用类厂对象的CreateInstance(...)函数创建组件的过程了。正如我们所见到的,现在OLE开始调用CComClassFactory::CreateInstance()了,我们还没忘记,在类厂对象中保留了创建组件用的CreateInstance()函数, 这个过程已经很明朗了。
2.不用再重复了吧,看第4步。
3.不用再重复了吧,看第4步。
4.如果继续路由下去的话,我们的堆栈还可以很长,但这只是重复的枯躁的劳动。我就不继续走下去了,我也很累的说,唉。
函数调用堆栈二:
0:............ 5.ATL::AtlInternalQueryInterface(...) 4.ATL::CComObjectRootBase::InternalQueryInterface(...) 3.CMyObject::_InternalQueryInterface(...) 2.ATL::CComObject< CMyObject >::QueryInterface(...) 1.pUnk->QueryInterface(IID_IMyObject, (void **)&pMyObject);(客户端) |
解释如下:
1.我们通过刚刚获得的组件对象的IUnknown接口指针来查询IMyObject指针,这才是我们真正需要的指针。
2.还记得我们说过ATL真正创建的组件并不是CMyObject,而是CComObject,CComAggObject 或CComPolyObject,这里我们创建的是CComObject.所以理所当然我们要调用 CComObject::QueryInterface(...),而确实CComObject也实现了这个函数。
{return _InternalQueryInterface(iid, ppvObject);}
它只是简单地调用_InternalQueryInterface(...),我们也说过,只有类里面申明了BEGIN_COM_MAP宏才会有_InternalQueryInterface(...),所以现在执行转到了它的父类CMyObject中去,所以将调用CMyObject::_InterfaceQueryInterface(...)
3.以后的调用我们已经很熟悉了,还用我再说一遍吗,呵呵
4.这个调用我们也很熟悉了,不用多说了吧
5.现在我们将要查询的是一个非IUnknown接口,所以我们来看看我们以前没列出的代码
ATLINLINE ATLAPI AtlInternalQueryInterface(void* pThis, const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject) { //确保接口映射的第一项是个简单接口 //若是查询IUnknown接口,执行相应的操作 //以下将遍历接口映射表,试图找到相应的接口 while (pEntries->pFunc != NULL) { BOOL bBlind = (pEntries->piid == NULL); if (bBlind || InlineIsEqualGUID(*(pEntries->piid), iid)) { //_ATL_SIMPLEMAPENTRY就表明是个简单接口 if (pEntries->pFunc == _ATL_SIMPLEMAPENTRY) //offset { ATLASSERT(!bBlind); IUnknown* pUnk = (IUnknown*)((int)pThis+pEntries->dw); pUnk->AddRef(); *ppvObject = pUnk; return S_OK; } else //如果不是一个简单接口,则需要执行相应的函数 { HRESULT hRes=pEntries->pFunc(pThis,iid,ppvObject,pEntries->dw); if (hRes == S_OK || (!bBlind && FAILED(hRes))) return hRes; } } pEntries++; } return E_NOINTERFACE; } } |
函数的逻辑很清楚,只有两点可能不太理解,一个是 (IUnknown*)((int)pThis+pEntries->dw)是什么意思,另一个是pEntries->pFunc到底 要干些什么事。前一个问题将在讲述COM_INTERFACE_ENTRY2中讲述,后一个问题将在以后讲述不同类型的接口时分别解释。饭总是要一口一口吃的嘛,呵呵。 现在我们只需关心一下我们的IMyObject是怎么被查找的。看一下它的宏我们把COM_INTERFACE_ENTRY(IMyObject)解开以后形式为:
{&_ATL_IIDOF(IMyObject), //得到IMyObject的IID值 offsetofclass(IMyObject, CMyObject), //定义偏移量 _ATL_SIMPLEMAPENTRY},//表明是个简单接口 |
同样对于offsetofclass(IMyObject, CMyObject)我们也将留到下一次再讲。根据这个结构,我们很容易就能获得IMyObject接口指针。
0:OK,it is over.依次退栈返回。
其实这次查询发生的过程在刚才的调用序列中也发生了,当查询IClassFactory接口时就有类似的过程,但还是把它单独提了出来,只为了看看典型的情形,呵呵。
二、COM_INTERFACE_ENTRY2(x, x2)
ATL中是以多重继承的方式来实现组件的,但在继承树中如果有多个分支实现了同一个接口,当查询这个接口时就需要知道把哪个分支返回给它。这个宏就是干这个工作的通常这个宏是用于IDispatch接口。我们先来看看它的典型用法:
class COuter : public IDispatchImpl< IOuter1, &IID_IOuter1, &LIBID_COMMAPLib>,//IOuter1是一个双接口 public IDispatchImpl< IOuter2, &IID_IOuter2, &LIBID_COMMAPLib>,//IOuter2也是一个双接口 public ... { public: COuter(){} ... BEGIN_COM_MAP(COuter) COM_INTERFACE_ENTRY2(IDispatch, IOuter2) ,//将暴露IOuter2所继承的路线 , COM_INTERFACE_ENTRY(IOuter1) COM_INTERFACE_ENTRY(IOuter2) ... END_COM_MAP }; |
IDispatchImpl<...>这个类中实现了IDispatch接口,所以现在组件中有两个IDispatch 的实现。那查询IDispatch接口时,返回哪个实现呢?
我们再来看看COM_INTERFACE_ENTRY2(x, x2)的定义
#define BEGIN_COM_MAP(x) public: / typedef x _ComMapClass; / .................... #define COM_INTERFACE_ENTRY2(x, x2)/ {&_ATL_IIDOF(x),/ //得到接口的IID值 (DWORD)((x*)(x2*)((_ComMapClass*)8))-8,/ _ATL_SIMPLEMAPENTRY}, //表明是一个简单接口 |
现在问题就在于(DWORD)((x*)(x2*)((_ComMapClass*)8))-8是个什么意思?
我们先来考察一下下面一段代码:
class A1 { public: virtual void Test(){} }; class A2 : public A1 { public: virtual void Test(){} }; class A3 : public A1 { public: virtual void Test(){} }; class A : public A2, public A3 { }; { DWORD dw; dw = (DWORD)((A *)8); //dw = 0x08 dw = (DWORD)((A3 *)(A *)8); //dw = 0x0c dw = (DWORD)((A1 *)(A3 *)(A *)8); //dw = 0x0c dw = (DWORD)((A1 *)(A3 *)(A *)8) - 8;//dw = 4 } |
这个继承图是个典型的菱形结构,在类A中保存有两个虚函数表指针,分别代表着它的两个分支。当为类A申明一个对象并实例化时,系统会为其分配内存。在这块内存的最顶端保留着它的两个虚函数表指针。分析程序运行的结果,可以看出,最后的结果4代表了指向接口A3的虚函数表指针与类A对象的内存块顶端之间的偏移量。
下面我们再看一个更为复杂点的继承关系:
class B1 { public: virtual void Test(){} }; class B2 { public: virtual void Test(){} }; class B3 { public: public: virtual void Test(){} }; class B4 : public B1, public B2 { public: virtual void Test(){} }; class B5 : public B2, public B3 { public: virtual void Test(){} }; class B : public B4, public B5 { }; { DWORD dw; dw = (DWORD)((B *)8); //dw = 0x08 dw = (DWORD)((B5 *)(B *)8); //dw = 0x10 dw = (DWORD)((B2 *)(B5 *)(B *)8); //dw = 0x10 dw = (DWORD)((B2 *)(B5 *)(B *)8) - 8; //dw = 8 } |
类B将保留四个虚函数表指针,因为它共有四个分支。我们的目的是想获得B::B5::B2这个分支中的B2接口,最后的结果8正是我们所需要的,它表示在类B内存块的偏移量。
从上面两个例子中,我们已经明白了(DWORD)((x*)(x2*)((_ComMapClass*)8))-8的作用通过这个值我们能获得我们所需要的接口。
下面我们针对我们的实际情况COM_INTERFACE_ENTRY2(IDispatch, IOuter2)来分析一下
IDispatchImpl< class T,... >模板类从类T中派生,所以COuter要从两个它的模板类中继承, IOuter1、IOuter2都是双接口,即都是从IDispatch派生的类,所以可得COuter有两条分支,也是个菱形结构,所以按照我们的示例,这个偏移值也应该是4。为了证明我们的设想,我们再来通过函数堆栈来验证我们的结果。
函数堆栈:
5.ATL::AtlInternalQueryInterface(...) 4.ATL::CComObjectRootBase::InternalQueryInterface(...) 3.CMyObject::_InternalQueryInterface(...) 2.ATL::CComObject< CMyObject >::QueryInterface(...) 1.pUnk->QueryInterface(IID_IDispatch, (void **)&pDispatch) |
解释:
1:这是我们的验证代码,pUnk是组件的IUnknown指针
2--5:这些代码我们现在都已经很熟悉了,我们只需再看看AtlInternalQueryInterface 的具体实现。
ATLINLINE ATLAPI AtlInternalQueryInterface(void* pThis, const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject) { ........... while (pEntries->pFunc != NULL) { BOOL bBlind = (pEntries->piid == NULL); if (bBlind || InlineIsEqualGUID(*(pEntries->piid), iid)) { if (pEntries->pFunc == _ATL_SIMPLEMAPENTRY) //offset { ATLASSERT(!bBlind); IUnknown* pUnk = (IUnknown*)((int)pThis+pEntries->dw); pUnk->AddRef(); *ppvObject = pUnk; return S_OK; } .....//如果是非简单接口的话... } pEntries++; } return E_NOINTERFACE; } |
关键的一句话就是IUnknown* pUnk = (IUnknown*)((int)pThis+pEntries->dw); 通过观察变量,正如我们所料pEntries->dw=4。(int)pThis+pEntries->dw)保证了我们可以得到IOuter2分支的虚函数表,又因为IDispatch也是从IUnknown继承,在虚函数表的最顶端放的是IUnknown的虚函数指针,所以进行(IUnknown *)强制转换,可以获得这个虚函数表的顶端地址,这正是我们所需要的。或许会问为什么得到的是虚函数表的地址,而不是一个类实例的地址呢?别忘了,接口是没有数据的,它只有纯虚函数。对于客户来说,它只能通过接口定义的虚函数来访问它,而不可能访问实现接口的类的成员变量,组件的数据对客户来说是不可见的,所以只用得到虚函数表的地址就行了。
三、COM_INTERFACE_ENTRY_TEAR_OFF(iid, x)
使用这个宏的目的就是为了把一些很少用到的接口放在一个单独的组件中实现,仅当查询到这个接口时,才创建这个组件,并且当它的引用计数减为0时就会被释放掉。我们知道ATL中组件是通过多重继承实现的,每继承一个接口,在为它分配的内存块中就会多一个虚函数表指针,用这个宏就可以为每个组件的实例节省下这一个虚函数表指针来(一个指针4个字节,好象也不多啊,呵呵)
下面我们来看它的典型用法:
class CTearOff1: //该类是专门用来实现分割接口ITearOff1的 public IDispatchImpl< ITearOff1, &IID_ITearOff1, &LIBID_COMMAPLib >, public CComTearOffObjectBase //外部对象 { public: CTearOff1(){} ~CTearOff1(){} BEGIN_COM_MAP(CTearOff1) COM_INTERFACE_ENTRY(ITearOff1) END_COM_MAP() HRESULT STDMETHODCALLTYPE get_Name(BSTR* pbstrName) { *pbstrName = ::SysAllocString(L"ITearOff1"); return S_OK; } }; class COuter : public ..... //我们真正要实现的组件 { public: ........... BEGIN_COM_MAP(COuter) ........... COM_INTERFACE_ENTRY_TEAR_OFF(IID_ITearOff1, CTearOff1) END_COM_MAP() ........... }; |
CTearOff1实现了Tear-off接口ITearOff1,实现方法与其他组件并无不同。唯一不同的是它从CComTearOffObjectBase继承,CComTearOffObjectBase定义如下:
template < class Owner, class ThreadModel = CComObjectThreadModel > class CComTearOffObjectBase : public CComObjectRootEx { public: typedef Owner _OwnerClass; CComObject* m_pOwner; CComTearOffObjectBase() {m_pOwner = NULL;} }; |
我们又看到了我们熟悉的一个类CComObject,它是组件的真正生成类。从上面的定义中可知道CComTearOffObjectBase主要功能就是包含了一个指向外部对象(在这里就是我们的组件类CComObject)的指针。它的功能将在后面看到。
我们继续用我们的老办法来跟踪一下看看它的执行过程。假设pOuter是我们已经获得的组件的IOuter接口指针。
执行pOuter->QueryInterface(IID_ITearOff1, (void **)&pTear1);
函数堆栈一:
7.CTearOff1::_InternalQueryInterface(...) 6.ATL::CComInternalCreator< ATL::CComTearOffObject< CTearOff1 > >::CreateInstance(...) 5.ATL::CComObjectRootBase::_Creator(...) 4.ATL::AtlInternalQueryInterface(...) 3.ATL::CComObjectRootBase::InternalQueryInterface(...) 2.COuter::_InternalQueryInterface(...) 1.ATL::CComObject< COuter >::QueryInterface(...) |
解释:
1--4:这些代码已经遇到过很多次了,我们还是集中精力看看核心代码:
ATLINLINE ATLAPI AtlInternalQueryInterface(void* pThis, const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject) { //.......... while (pEntries->pFunc != NULL) { BOOL bBlind = (pEntries->piid == NULL); if (bBlind || InlineIsEqualGUID(*(pEntries->piid), iid)) { if (pEntries->pFunc == _ATL_SIMPLEMAPENTRY) //offset { //若是简单接口,.... } else //actual function call { HRESULT hRes = pEntries->pFunc(pThis, iid, ppvObject, pEntries->dw); if (hRes == S_OK || (!bBlind && FAILED(hRes))) return hRes; } } pEntries++; } return E_NOINTERFACE; } |
当在COuter的接口映射数组中找到ITearOff1后,因为它不是一个简单接口,所以要执行pEntries->pFunc(....)。
我们先来看看COM_INTERFACE_ENTRY_TEAR_OFF的定义:
#define COM_INTERFACE_ENTRY_TEAR_OFF(iid, x)/ {&iid,/ (DWORD)&_CComCreatorData</ CComInternalCreator< CComTearOffObject< x > >/ >::data,/ _Creator}, |
看不太明白,还是继续我们路由得了
5:原来_Creator是CComObjectRootBase的静态成员函数,它可是COuter的一个基类啊,所以才可以这样写而不会编译出错。看看它的实现吧:
static HRESULT WINAPI _Creator(void* pv, REFIID iid, void** ppvObject,DWORD) { _ATL_CREATORDATA* pcd = (_ATL_CREATORDATA*)dw; return pcd->pFunc(pv, iid, ppvObject); } struct _ATL_CREATORDATA { _ATL_CREATORFUNC* pFunc; }; typedef HRESULT (WINAPI _ATL_CREATORFUNC)(void* pv, REFIID riid, LPVOID* ppv); template < class Creator > _ATL_CREATORDATA _CComCreatorData::data = {Creator::CreateInstance}; |
源代码都列出来了,不用我多说,大家也都能看懂了。继续路由吧
6:绕了一大圈,现在我们调用的应该是
CComInternalCreator<...>::CreateInstance template < class T1 > class CComInternalCreator { public: static HRESULT WINAPI CreateInstance(void* pv, REFIID riid, LPVOID* ppv) { ATLASSERT(*ppv == NULL); HRESULT hRes = E_OUTOFMEMORY; T1* p = NULL; ATLTRY(p = new T1(pv)) if (p != NULL) { p->SetVoid(pv); p->InternalFinalConstructAddRef(); hRes = p->FinalConstruct(); p->InternalFinalConstructRelease(); if (hRes == S_OK) hRes = p->_InternalQueryInterface(riid, ppv); if (hRes != S_OK) delete p; } return hRes; } }; |
同我们所见到的大多数Creator类一样,它也只有一个静态CreateInstance函数。现在我们终于可以创建我们分割组件了,它不是CTearOff1,它也是经了一层包装的,是 CComTearOffObject! 现在我们再来看看它的构造函数干了些什么事:
CComTearOffObject(void* pv) { ATLASSERT(m_pOwner == NULL); m_pOwner = reinterpret_cast< CComObject< Base::_OwnerClass >* >(pv); m_pOwner->AddRef(); } |
还记得CTearOff1是从CComTearOffObjectBase继承的吗,这个基类包含了一个成员变量m_pOwner,现在它被赋值为指向它的外部对象的指针了。
7.现在终于把这个实现分割接口的组件创建了,剩下的在CTearOff1中查询ITearOff1的工作已经是重复劳动了,不再赘述。
执行pTear1->QueryInterface(ITearOff1, (void **)&pTear2)
一个实现分割接口的组件有可能包含多个分割接口,我们来检测一下它的查询过程。
函数堆栈二:
4.............. 3.COuter::_InternalQueryInterface(...) 2.ATL::CComObject< COuter >::QueryInterface(...) 1.ATL::CComTearOffObject< CTearOff1 >::QueryInterface(...) |
解释:
1:
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) { return m_pOwner->QueryInterface(iid, ppvObject); } |
还记得我们创建的分割组件是CComTearOffObject< CTearOff1 >吗?现在执行查询操作的是它的成员函数。它的实现很简单,事实上它什么也没做,仅仅是把它交给它的外部对象(即CComObject< COuter >)去做了。还记得m_pOwner是在构造函数里赋值的吧。现在是否感到有些不妙呢?呵呵
2、3:果然,现在已经不用再看下去了,剩下的将是重复我们在调用第一条查询操作所做的一切。这个过程很简单,但它也隐含说明了一点:若对一个实现分割接口的组件每查询一次它的接口,就会调用一个新的实例!!!在上例中,最后的结果pTear2和pTear1 是不一样的!!这显然是浪费!
在下一节中,我们将介绍一个可以解决这个问题的宏!
四.COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(iid, x, punk)
这个宏与上一节所讲的COM_INTERFACE_ENTRY_TEAR_OFF宏最主要的不同就在于,当查询分割对象中其他接口时,不会再新建新的对象。下面还是先看看它的典型用法:
class CTearOff2: public IDispatchImpl< ITearOff2, &IID_ITearOff2, &LIBID_COMMAPLib >, public CComTearOffObjectBase< COuter > { public: CTearOff2(){} ~CTearOff2(){} BEGIN_COM_MAP(CTearOff2) COM_INTERFACE_ENTRY(ITearOff2) END_COM_MAP() HRESULT STDMETHODCALLTYPE get_Name(BSTR* pbstrName) { *pbstrName = ::SysAllocString(L"ITearOff2"); return S_OK; } }; class COuter : public .... { public: BEGIN_COM_MAP(COuter) COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(IID_ITearOff2, CTearOff2, m_pUnkTearOff2.p) ...... END_COM_MAP() CComPtr< IUnknown > m_pUnkTearOff2; ..... }; |
CTearOff2实现了分割接口ITearOff2,它的类定义与上一节所看见的CTearOff1一模一样可见不管是哪种分割接口,实现都是一样的,不同的地方在于COuter。在COuter中增加了一个成员变量m_pUnkTearOff2作为宏的一个参数。
我们继续用老办法跟踪它的内部执行过程,假设pOuter是已经获得的组件COuter有接口IOuter指针。
执行pOuter->QueryInterface(IID_ITearOff2, (void **)&pTear1);
函数堆栈一:
9.CTearOff2::_InternalQueryInterface(...) 8.ATL::CComCachedTearOffObject< CTearOff2 >::QueryInterface(...)(第二次调用) 9.ATL::CComCachedTearOffObject< CTearOff2 >::QueryInterface(...) 8.ATL::CComCreator< ATL::CComCachedTearOffObject< CTearOff2 > >::CreateInstance() 7.ATL::CComObjectRootBase::_Cache(...) 6.COuter::_Cache(...) 5.ATL::AtlInternalQueryInterface(...) 4.ATL::CComObjectRootBase::InternalQueryInterface(...) 3,COuter::_InternalQueryInterface(...) 2.ATL::CComObject< COuter >::QueryInterface(...) 1.CTestDlg::OnButton1() line 187 + 22 bytes |
解释:
1:pOuter->QueryInterface(IID_ITearOff2, (void **)&pTear1);
2-5:这段代码见到很多次了,不用再讲了,现在程序执行到
HRESULT hRes = pEntries->pFunc(pThis, iid, ppvObject, pEntries->dw); 看来我们得看看这个宏的定义才能知道pFunc是执行的什么功能了。
{&iid,/
(DWORD)&_CComCacheData</
CComCreator< CComCachedTearOffObject< x > >,/
(DWORD)offsetof(_ComMapClass, punk)/
?gt;::data,/
_Cache},
与我们上一节见的宏的定义不太一样,还是先跟踪下去再说。
6:原来在BEGIN_COM_MAP中也定义了_Cache函数:
{/
......
HRESULT hRes = CComObjectRootBase::_Cache(pv, iid, ppvObject, dw);/
......
}/
7:看看CComObjectRootBase::_Cache的源码:
static HRESULT WINAPI _Cache(void* pv,REFIID iid,void** ppvObject,DWORD dw) { HRESULT hRes = E_NOINTERFACE; _ATL_CACHEDATA* pcd = (_ATL_CACHEDATA*)dw; IUnknown** pp = (IUnknown**)((DWORD)pv + pcd->dwOffsetVar); if (*pp == NULL) hRes = pcd->pFunc(pv, IID_IUnknown, (void**)pp); if (*pp != NULL) hRes = (*pp)->QueryInterface(iid, ppvObject); return hRes; } |
现在问题的关键是dw了,dw是从pEntries->dw传过来的。我们得看一下宏定义中的
(DWORD)&_CComCacheData</ CComCreator< CComCachedTearOffObject< x> >,/ (DWORD)offsetof(_ComMapClass, punk)/ >::data,/ |
是什么意思。
_ATL_CACHEDATA _CComCacheData< Creator, dwVar >::data = {dwVar, Creator::CreateInstance};
CComCreator我们在前面已经见过它的定义了,它只有一个成员函数
CreateInstance. template < class contained > class CComCachedTearOffObject : public IUnknown, public CComObjectRootEx< contained::_ThreadModel::ThreadModelNoCS > { public: typedef contained _BaseClass; CComCachedTearOffObject(void* pv) : m_contained(((contained::_OwnerClass*)pv)->GetControllingUnknown()) { m_contained.m_pOwner = reinterpret_cast< CComObject< contained::_OwnerClass >* >(pv); } CComContainedObject< contained > m_contained; }; |
CComCachedTearOffObject是这个宏与上一节所讲宏不同的关键所在,因为它包含了一个CComContainedObject的对象。这个对象的作用在查询的时候再讲。
我们再来看看offsetof的定义:
#define offsetof(s,m) (size_t)&(((s *)0)->m)
对(DWORD)offsetof(_ComMapClass, punk)来说,就是punk在_ComMapClass类中的偏移值。
现在来看看_ATL_CACHEDDATA是什么东西。
struct _ATL_CACHEDATA { DWORD dwOffsetVar; _ATL_CREATORFUNC* pFunc; }; typedef HRESULT (WINAPI _ATL_CREATORFUNC)(void* pv, REFIID riid, LPVOID* ppv); |
要注意的是从_Cached()函数传进来的参数dw就是_ATL_CACHEDATA结构的变量,所以可知道(DWORD)pv + pcd->dwOffsetVar)得到的就是在类COuter中定义的m_pUnkTearOff2的偏移值,所以IUnknown** pp就是指向m_pUnkTearOff2的一个指向指针的指针。
而pdc->pFunc()则会调用CComCreator< CComCachedTearOffObject < x > >::CreateInstance
8:下面将调用CComCreator::CreateInstance,将创建一个CComCachedTearOffObject<>的对象实例。其构造函数定义如下:
CComCachedTearOffObject(void* pv) : m_contained(((contained::_OwnerClass*)pv)->GetControllingUnknown()) { ATLASSERT(m_contained.m_pOwner == NULL); m_contained.m_pOwner = reinterpret_cast< CComObject< contained::_OwnerClass >* >(pv); } |
这里contained就是CTearOff2,contained::_OwnerClass就是COuter,可见m_contained保存了外部对象的指针。
9:创建完对象后,将查询接口ITearOff2
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) { //如果是IUnknown,...,返回IUnknwon接口指针 else hRes = m_contained._InternalQueryInterface(iid, ppvObject); ..... } |
注意,这里把查询工作交给了m_contained,也就是一个CComContainedObject对象。不过现在查询的是IUnknown指针,别忘了,我们在COuter中还定义了一个IUnknown指针呢,现在查询的就是它!!
8:经过一系列退栈,退到_Cache()中,现在还要继续查询ITearOff2接口。是根据我们刚刚查询到的IUnknown指针查询ITearOff2。所以再一次进入 ATL::CComCachedTearOffObject< CTearOff2 >::QueryInterface(...),不过这回将调用的是m_contained._InternalQueryInterface(...)了。
9:因为CComContainedObject m_contained的基类是CTearOff2,所以将调用CTearOff2::_InternalQueryInterface(...)
剩下的操作就没什么特别之处了,仅仅一般的查询操作。
执行pTear1->QueryInterface(ITearOff2, (void **)&pTear2);
函数堆栈二:
12.ATL::AtlInternalQueryInterface(...) 11.ATL::CComObjectRootBase::InternalQueryInterface(...) 10.CTearOff2::_InternalQueryInterface(...) 9.ATL::CComCachedTearOffObject< CTearOff2 >::QueryInterface(...) 8.ATL::CComObjectRootBase::_Cache(...) 7.COuter::_Cache(...) 6.ATL::AtlInternalQueryInterface(...) 5.ATL::CComObjectRootBase::InternalQueryInterface(...) 4.COuter::_InternalQueryInterface(...) 3.ATL::CComObject< COuter >::QueryInterface(...) 2.ATL::CComObjectRootBase::OuterQueryInterface(...) 1.ATL::CComContainedObject< CTearOff2 >::QueryInterface(...) |
解释:
1:第一步就可能使我们迷惑了,为什么执行的是CComContainedObject::QueryInterface
在上一节中,执行的是ATL::CComTearOffObject< CTearOff1 >::QueryInterface(...),所以我们也自然而然的猜想,这里应该执行的是CComCachedTearOffObject的函数。但是来看看CComCachedTearOffObject的定义:
template < class contained > class CComCachedTearOffObject : public IUnknown, public CComObjectRootEx< contained::_ThreadModel::ThreadModelNoCS > { ... }; |
原来CComCachedTearOffObject没有从contained类(在这里就是CTearOff2)中继承,而 CComTearOffObject却是从CTearOff1继承的!所以我们刚才得到的pTear1就不可能是CComCachedTearOffObject的对象。而实际上,CComContainedObject是从CTearOff2继承的,在上面的函数堆栈中第9步查询ITearOff2接口时,把工作交给了m_contained, 这是个CComContainedObject< CTearOff2 >对象,所以实际上最后查询得到的ITearOff2 指向的是CComContainedObject< CTearOff2 >对象。所以现在执行的会是 CComContainedObject::QueryInterface(...)!!!
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) { HRESULT hr = OuterQueryInterface(iid, ppvObject); if (FAILED(hr) && _GetRawUnknown() != m_pOuterUnknown) hr = _InternalQueryInterface(iid, ppvObject); return hr; } //?m_pOuterUnknown |
2:
HRESULT OuterQueryInterface(REFIID iid, void ** ppvObject) { return m_pOuterUnknown->QueryInterface(iid, ppvObject); } |
把查询工作交给外部对象完成,也就是COuter。
第一、二步的功能与上一节中所讲的一样,都是交给外部对象去处理,不同之处在下面3-8:COuter中的查询过程与上例中无异,我们可以直接跳到第8步中
static HRESULT WINAPI _Cache(void* pv, REFIID iid, void** ppvObject,DWORD dw) { .... if (*pp == NULL) hRes = pcd->pFunc(pv, IID_IUnknown, (void**)pp); if (*pp != NULL) hRes = (*pp)->QueryInterface(iid, ppvObject); return hRes; } |
还记得我们在COuter中定义了一个IUnknown指针m_pUnkTearOff2吧,我们在第一次查询 ITearOff2接口时,创建了CTearOff2对象,并查询一个IUnknown指针给了它.现在它就发挥作用了,在_Cache中将判断如果m_pUnkTearOff2不等于空,则表明CTearOff2已经创建就不会再创建它了,而是直接用它去查询接口.
9:所以现在将调用CComCachedTearOffObject< CTearOff2>::QueryInterface(...),在上一个函数堆栈中的第9步中我们已经看到了这个QueryInterface(...)的代码,它把查询工作交给m_contained._InternalQueryInterface(.),其实因为CComContainedObject中没有定义BEGIN_COM_MAP宏,所以也没有定义_InternalQueryInterface(),所以实际上调用的是它包含的类的函数,即CTearOff2::_InternalQueryInterface(...)
10-12:以下的工作就很简单了,不再赘述。
总结:
COM_INTERFACE_ENTRY_CACHED_TEAR_OFF是个相对比较麻烦的宏,它与上一节介绍的宏相比不同之处就在于创建分割接口对象的过程只用进行一次,如果对象已经创建,则下一次查询该对象的接口时不会再创建一个新的分割对象。为了达到这个目的,它在外部对象中包含了一个IUnknown指针,并在第一次创建分割对象时查询这个IUnknown指针,这样就可以通过判断这个指针是否为空来知道这个分割对象是否已经创建,从而决定是否创建新的分割对象,并通过它去查询分割对象内其它接口。这里特别需要注意的是,实际上有两个对象被创建,一个是CComCachedTearOffObject< CTearOff2 >,另一个是 CComContainedObject< CTearOff2 >。并且第一个对象内部实现了第二个对象,真正的查询工作也是交给第二个对象去做。COuter::m_pUnkTearOff2是前面一个对象的IUnknown指针,当用它去查询ITearOff2时,实际上是交给了其内部对象m_contained去做了,这在第8、9步可以看得很清楚。
终于把这个宏讲完了,我感觉这个宏可能是ATL接口映射宏中实现最复杂的了,其实它并没有利用到CComContainedObject的真正功能。感觉实现这个宏也许不应这么麻烦的。
五.COM_INTERFACE_ENTRY_AGGREGATE(iid, punk)
这一节中将介绍ATL中用于聚集对象的宏。聚集对象的概念请参阅其它参考书。
现在先看一看这个宏的典型用法:
class CAgg : public IDispatchImpl< IAgg, &IID_IAgg, &LIBID_AGGREGLib >, public ISupportErrorInfo, public CComObjectRoot, public CComCoClass< CAgg,&CLSID_CAgg > { ..... }; |
CAgg是一个聚集类,它的实现与一般的ATL组件没有区别,只是注意在它的类定义中不要加入DECLARE_NO_AGGREGATABLE.
class COuter : public CChainBase, public IDispatchImpl< IOuter, &IID_IOuter, &LIBID_COMMAPLib >, public CComCoClass< COuter,&CLSID_COuter > { HRESULT FinalConstruct(); void FinalRelease(); BEGIN_COM_MAP(COuter) COM_INTERFACE_ENTRY_AGGREGATE(IID_IAgg, m_pUnkAgg.p) END_COM_MAP() DECLARE_GET_CONTROLLING_UNKNOWN() CComPtr< IUnknown > m_pUnkAgg; }; |
COuter包含了聚合组件CAgg,它包含了几个不同之处:
(1)加入了COM_INTERFACE_ENTRY_AGGREGATE(IID_IAgg, m_pUnkAgg.p)宏。
#define COM_INTERFACE_ENTRY_AGGREGATE(iid, punk)/ {&iid,/ (DWORD)offsetof(_ComMapClass, punk),/ _Delegate}, |
offsetof我们在上一节中已经见过,可以猜到它求的就是punk在类中的位置。也就是m_pUnkAgg在COuter中的位置。
(2)加入了宏DECLARE_GET_CONTROLLING_UNKNOWN(),其定义为:
#define DECLARE_GET_CONTROLLING_UNKNOWN() public:/ virtual IUnknown* GetControllingUnknown() {return GetUnknown();} |
我们也没必要继续深究下去,仅从字面意思就可以看出这个函数将返回组件的IUnknown 指针。
(3)在COuter中加入一个成员变量:CComPtr< IUnknown > m_pUnkAgg; m_pUnkAgg将用于获得被聚集组件的IUnknown指针。
(4)重载了FinalConstruct,FinalRelease
HRESULT COuter::FinalConstruct() { IUnknown* pUnkOuter = GetControllingUnknown(); HRESULT hRes = CoCreateInstance(CLSID_CAgg, pUnkOuter, CLSCTX_ALL, IID_IUnknown, (void**)&m_pUnkAgg); return hRes; } void COuter::FinalRelease() { m_pUnkAgg.Release(); ..... } |
当创建组件COuter后将会调用FinalConstruct,所以会在这里创建聚集组件。原则上聚集组件可以仅在需要的时候才创建,但也可以随着包含它的组件一起创建。聚集组件的创建没什么特别之处,只是要注意它将查询IUnknown指针,并返回给m_pUnkAgg.外部组件将通过m_pUnkAgg操作聚集组件。另外注意到使用pUnkOuter作为CoCreateInstance的参数,这将导致创建CComAggObject< COuter >对象,内部包含一个CComContainedObject的包含对象。与上一节中的CComCachedTearOff<>类似,CComAggObject< COuter >也不是从COuter派生的,所以真正的组件对象不是CComAggObject< COuter >对象,而是它内部包含的CComContainedObject< COuter >对象。同样pUnkOuter得到的将是CComAggObject<>的IUnknown指针,也同样调用它的QueryInterface会转而调用CComContainedObject的_InternalQueryInterface函数(呵呵,现在可都还是我猜的,看我猜的对不对吧)
运行pOuter->QueryInterface(IID_IAgg, (void **)&pAgg1)
函数堆栈一:
9.ATL::AtlInternalQueryInterface(...) 8.ATL::CComObjectRootBase::InternalQueryInterface(...) 7.CAgg::_InternalQueryInterface(...) 6.ATL::CComAggObject< CAgg >::QueryInterface(...) 5.ATL::CComObjectRootBase::_Delegate(...) 4.ATL::AtlInternalQueryInterface(...) 3.ATL::CComObjectRootBase::InternalQueryInterface(...) 2.COuter::_InternalQueryInterface(...) 1.ATL::CComObject< COuter >::QueryInterface(...) |
解释:
1-5:这几步函数调用我们已经见了很多次了,因为在这个宏定义使用了_Delegate,所以将调用CComObjectRootBase::_Delegate(...).
static HRESULT _Delegate(void* pv,REFIID iid,void** ppvObject,DWORD dw) { HRESULT hRes = E_NOINTERFACE; IUnknown* p = *(IUnknown**)((DWORD)pv + dw); if (p != NULL) hRes = p->QueryInterface(iid, ppvObject); return hRes; } |
第二句话的含义我们在上一节中已经见过了,最后的结果p=COuter::m_pUnkAgg.
6:正如我们刚才所料,现在调用的是CComAggObject< CAgg >::QueryInterface()
STDMETHOD(QueryInterface)(REFIID iid, void ** ppvObject) { //如果查询的是IUnknown,则.... else hRes = m_contained._InternalQueryInterface(iid, ppvObject); return hRes; } |
也正如我们所料,将交给它的包含对象去做.(这段代码在上一节好象也见过是吧,呵呵)
7-9:同上一节一样,将交给CAgg::_InternalQueryInterface(...),剩下的工作将由CAgg完成了。最后返回的指针实际上将是CComContainedObject< CAgg >组件的接口指针。
运行pAgg1->QueryInterface(IID_IAgg, (void **)&pAgg2)
函数堆栈二:
9.CAgg::_InternalQueryInterface(...) 8.ATL::CComAggObject< CAgg >::QueryInterface(...) 7.ATL::CComObjectRootBase::_Delegate(...) 6.ATL::AtlInternalQueryInterface(...) 5.ATL::CComObjectRootBase::InternalQueryInterface(...) 4.COuter::_InternalQueryInterface(...) 3.ATL::CComObject< COuter >::QueryInterface(...) 2.ATL::CComObjectRootBase::OuterQueryInterface(...) 1.ATL::CComContainedObject< CAgg >::QueryInterface(...) |
解释:
1-9:浏览整个堆栈,与我们上一节所见的堆栈二太相近了,这是因为都是使用了包含对象。包含对象起了个代理的作用,他先把查询交给外部对象(COuter)去做(第1,2步), 当外部对象发现要查询的是聚集组件的接口时(IAgg),就会再把查询交还给它保留的聚集组件的指针(m_pUnkAgg,第7步中,注意这不是真正的聚集组件),m_pUnkAgg再把查询交给包含对象(第8步中),包含对象再把查询交给真正实现接口的类CAgg(第9步). 若外部对象发现要查询的是外部组件的接口时,那就很简单了,直接查询就行了。这样就防止了外部组件与聚集组件查询操作的不一致性。唉,真个过程真麻烦,不过还好,与上一节的宏很类似。相关的源码可参看上一节。
六、COM_INTERFACE_ENTRY_AGGREGATE_BLIND 参ATL例程COMMAP
上一节我们讲了COM_INTERFACE_ENTRY_AGGREGATE,这节要介绍的宏与它很类似。
#define COM_INTERFACE_ENTRY_AGGREGATE_BLIND(punk)/ {NULL,/ (DWORD)offsetof(_ComMapClass, punk),/ _Delegate}, |
从定义上就可以看出,它与上一节介绍宏的唯一区别就在于,它没有指明接口ID!!
所以在它的定义中第一项也是NULL。
这个宏的用法与我们COM_INTERFACE_ENTRY_AGGREGATE一模一样。大家可以参考上一节内容以及ATL的例程COMMAP。
我们来看看AtlInternalQueryInterface()中的相关代码。
ATLINLINE ATLAPI AtlInternalQueryInterface(void* pThis, const _ATL_INTMAP_ENTRY* pEntries, REFIID iid, void** ppvObject) { //如果是IUnknown,.... while (pEntries->pFunc != NULL) { BOOL bBlind = (pEntries->piid == NULL); if (bBlind || InlineIsEqualGUID(*(pEntries->piid), iid)) { if (pEntries->pFunc == _ATL_SIMPLEMAPENTRY) //offset { ATLASSERT(!bBlind); IUnknown* pUnk = (IUnknown*)((int)pThis+pEntries->dw); pUnk->AddRef(); *ppvObject = pUnk; return S_OK; } else { HRESULT hRes = pEntries->pFunc(pThis, iid, ppvObject, pEntries->dw); if (hRes == S_OK || (!bBlind && FAILED(hRes))) return hRes; } } pEntries++; } return E_NOINTERFACE; } 注意变量bBlind: BOOL bBlind = (pEntries->piid == NULL); |
若没指定接口ID,也继续执行后面的操作,可见即使并非我们所需要的IID,也会执行_Delegate.
从上可见,这个宏适用于一个聚集组件有多个接口的情况,这样只要是查询这个聚集组件的接口,就会进入_Delegate函数。但要特别注意的是这个宏的位置!! 比如若是这样的顺序:
BEGIN_COM_MAP COM_INTERFACE_ENTRY_AGGREGATE_BLIND(m_pUnkAggBlind.p) COM_INTERFACE_ENTRY(IOuter) END_COM_MAP |
当查询IOuter接口时就会出错!!!
七、COM_INTERFACE_ENTRY_AUTOAGGREGATE(iid, punk, clsid)
先看看这个宏的定义:
#define COM_INTERFACE_ENTRY_AUTOAGGREGATE(iid, punk, clsid)/ {&iid,/ (DWORD)&_CComCacheData</ CComAggregateCreator< _ComMapClass, &clsid >,/ (DWORD)offsetof(_ComMapClass, punk)/ >::data,/ _Cache}, |
先看看它的典型用法:
class CAutoAgg : public IDispatchImpl< IAutoAgg, &IID_IAutoAgg, &LIBID_AGGREGLib >, public ISupportErrorInfo, public CComObjectRoot, public CComCoClass< CAutoAgg,&CLSID_CAutoAgg > { ...... }; |
与一般的组件并无二样。
class COuter : public CChainBase, public IDispatchImpl, public CComCoClass { BEGIN_COM_MAP(COuter) COM_INTERFACE_ENTRY_AUTOAGGREGATE(IID_IAutoAgg, m_pUnkAutoAgg.p, CLSID_CAutoAgg) END_COM_MAP() CComPtr m_pUnkAutoAgg; }; |
与宏COM_INTERFACE_ENTRY_AGGREGRATE(_)不同,COuter不用在FinalConstruct中创建聚集组件。外部组件会自动创建聚集组件!!!
1。
template < class Creator, DWORD dwVar > _ATL_CACHEDATA _CComCacheData< Creator, dwVar >::data = {dwVar, Creator::Creat eInstance}; |
2。
static HRESULT WINAPI _Cache(void* pv, REFIID iid, void** ppvObject, DWORD dw) { HRESULT hRes = E_NOINTERFACE; _ATL_CACHEDATA* pcd = (_ATL_CACHEDATA*)dw; IUnknown** pp = (IUnknown**)((DWORD)pv + pcd->dwOffsetVar); if (*pp == NULL) hRes = pcd->pFunc(pv, IID_IUnknown, (void**)pp); if (*pp != NULL) hRes = (*pp)->QueryInterface(iid, ppvObject); return hRes; } |
3。
template < class T, const CLSID* pclsid > class CComAggregateCreator { public: static HRESULT WINAPI CreateInstance(void* pv, REFIID/*riid*/, LPVOID* ppv ) { ATLASSERT(*ppv == NULL); ATLASSERT(pv != NULL); T* p = (T*) pv; return CoCreateInstance(*pclsid, p->GetControllingUnknown(), CLSCTX_ALL, IID_IUnknown, ppv); } }; |
因为_Cache,_CComCacheData,CComAggregateCreator这几个类和函数我们已经在前面见过或者见过类似的,所以就不再多讲了。总之我们可以看到,若m_pUnkAutoAgg.p不为空则直接查询,否则创建聚集组件。
与宏COM_INTERFACE_ENTRY_AGGREGATE相比,这个宏似乎更好一些,仅当需要时才会创建,使用更简单。
八、COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND( punk, clsid )
看看它的定义:
#define COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND(punk, clsid)/ {NULL,/ (DWORD)&_CComCacheData</ CComAggregateCreator< _ComMapClass, &clsid >,/ (DWORD)offsetof(_ComMapClass, punk)/ >::data,/ _Cache}, |
呵呵,这个宏综合了 COM_INTERFACE_ENTRY_AUTOAGGREGATE() 和 COM_INTERFACE_ENTRY_AGGREGATE_BLIND() 的特点,既可以自动创建也可以很方便地查询聚集组件中的多个接口。不再赘述!!
九、COM_INTERFACE_ENTRY_CHAIN(classname) 参ATL例程COMMAP
先看看它的定义:
#define COM_INTERFACE_ENTRY_CHAIN(classname)/ {NULL,/ (DWORD)&_CComChainData< classname, _ComMapClass >::data,/ _Chain}, |
典型用法:
class CChain : public IDispatchImpl< IChain, &IID_IChain, &LIBID_COMMAPLib >, public ISupportErrorInfo, public CComObjectRoot, public CComCoClass< CChain,&CLSID_CChain > { ........ }; |
它与一般的组件无异。
class COuter : public CChain, .... { BEGIN_COM_MAP(COuter) ...... COM_INTERFACE_ENTRY_CHAIN(CChain) END_COM_MAP() }; |
我们对查询的过程已经很熟悉了,可以直接来看看_Chain的功能。_Chain()是CComObjectRootBase的成员函数:
static HRESULT WINAPI _Chain(void* pv, REFIID iid, void** ppvObject,DWORD dw) { _ATL_CHAINDATA* pcd = (_ATL_CHAINDATA*)dw; void* p = (void*)((DWORD)pv + pcd->dwOffset); return InternalQueryInterface(p, pcd->pFunc(), iid, ppvObject); } struct _ATL_CHAINDATA { DWORD dwOffset; const _ATL_INTMAP_ENTRY* (WINAPI *pFunc)(); }; |
我们再看看宏定义中的dw部分:
template < class base, class derived > _ATL_CHAINDATA _CComChainData< base, derived >::data = {offsetofclass(base, derived), base::_GetEntries}; |
基本上我们已经看懂是怎么回事了,void *p将得到基类的指针,InteralQueryInterface 我们已经很熟悉了,_Chain把基类的指针以及基类的接口映射宏传给它,实际上是查询基类的接口!!!
一般情况下把这个宏放在BEGIN_COM_MAP和END_COM_MAP之间的最后面,这表示只有在当前类中查不到接口时才去查父类的接口。不过也经常把它放在第一位,这时就是先去查父类接口,只有父类没有实现这种接口时才查自己。在ATL中组件是以多重继承的方式实现的,ATL定义了很多类实现了一些常用的接口,这些类经常被做为组件的基类,所以这个宏被大量使用。
所有重要的宏我们都已经讲过了,剩下的都是些很简单的宏了.呵呵,还是把它们都罗列一下,善始善终嘛.
十、COM_INTERFACE_ENTRY_IID(iid, x)
#define COM_INTERFACE_ENTRY_IID(iid, x)/ {&iid,/ offsetofclass(x, _ComMapClass),/ _ATL_SIMPLEMAPENTRY}, |
十一、COM_INTERFACE_ENTRY2_IID(iid, x, x2)
#define COM_INTERFACE_ENTRY2_IID(iid, x, x2)/ {&iid,/ (DWORD)((x*)(x2*)((_ComMapClass*)8))-8,/ _ATL_SIMPLEMAPENTRY}, |
从定义上看这两个宏与COM_INTERFACE_ENTRY()和COM_INTERFACE_ENTRY2()相比,都只是多了一项"iid"。没有别的好处,只不过由用户明确指出接口IID,而不用系统根据接口名字去转换了。
十二、COM_INTERFACE_ENTRY_FUNC( iid, dw, func )
#define COM_INTERFACE_ENTRY_FUNC(iid, dw, func)/ {&iid, / dw, / func}, |
还记得AtlInternalQueryInterface()中的代码吗?如果在接口映射表中找到了我们要找的接口,并且这个接口不是_ATL_SIMPLEENTRY型的,则执行宏定义中的指定的函数。
这个宏就给我们提供了自己编写处理函数的功能。这个函数必须是如下定义:HRESULT WINAPI func(void* pv, REFIID riid, LPVOID* ppv, DWORD dw);
当AtlInternalQueryInterface调用func时,会传进相关的信息。pv是类对象的指针,riid是要查询的接口,ppv是要返回查询得到的接口指针,dw是在宏定义中指定的参数。另外如果函数中不打算返回接口指针,则应把ppv赋为NULL,并返回S_FALSE或 E_NOINTERFACE。返回S_FALSE刚会继续查找下去,若返回E_NOINTERFACE则会终止查询。若返回接口指针,则应返回S_OK.
十三、COM_INTERFACE_ENTRY_FUNC_BLIND(dw, func)
#define COM_INTERFACE_ENTRY_FUNC_BLIND(dw, func)/ {NULL, / dw, / func}, |
至于_BLIND类型的特点可以看前面几节。
十四、COM_INTERFACE_ENTRY_NOINTERFACE(x)
#define COM_INTERFACE_ENTRY_NOINTERFACE(x)/ {&_ATL_IIDOF(x), / NULL, / _NoInterface}, _NoInterface是CComObjectRootBase的成员函数,看看它的定义: static HRESULT WINAPI _NoInterface(...) { return E_NOINTERFACE; } |
原来它只是返回E_NOINTERFACE,并且将终止查询。
哈哈,看来是不想让别人查到这个接口啊!!!
十五、COM_INTERFACE_ENTRY_BREAK(x)
#define COM_INTERFACE_ENTRY_BREAK(x)/ {&_ATL_IIDOF(x), / NULL, / _Break}, _Break也是CComObjectRootBase的成员函数,看看它的定义: static HRESULT WINAPI _Break(...) { iid; _ATLDUMPIID(iid, _T("Break due to QI for interface "), S_OK); DebugBreak(); return S_FALSE; } |
如果查到这个接口将调用DebugBreak(),并返回S_FALSE,继续查询下去。DebugBreak()是什么效果大家自己试试吧,一定很熟悉的,呵呵。
至此全部十五个接口映射宏我们都已经讲完了,唉,真是不容易,特别是前面几个宏跟踪起来很麻烦。因为文本方式的限制,所以很多东西不容易表达清楚。有些叫法也是我自己这么叫的,可能与别人的习惯不同。没办法,大家将就将就了,呵呵。