二、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 *)强制转换,可以获得这个虚函数表的顶端地址,这正是我们所需要的。或许会问为什么得到的是虚函数表的地址,而不是一个类实例的地址呢?别忘了,接口是没有数据的,它只有纯虚函数。对于客户来说,它只能通过接口定义的虚函数来访问它,而不可能访问实现接口的类的成员变量,组件的数据对客户来说是不可见的,所以只用得到虚函数表的地址就行了。
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 *)强制转换,可以获得这个虚函数表的顶端地址,这正是我们所需要的。或许会问为什么得到的是虚函数表的地址,而不是一个类实例的地址呢?别忘了,接口是没有数据的,它只有纯虚函数。对于客户来说,它只能通过接口定义的虚函数来访问它,而不可能访问实现接口的类的成员变量,组件的数据对客户来说是不可见的,所以只用得到虚函数表的地址就行了。