清凉世界

tofy专栏

ATL接口映射宏详解[3]

二、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学习
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭