COM接口聚合实现解惑C++虚表

最近看潘爱民的《COM原理与应用》,看到接口的聚合实现时,产生一个疑惑。COM的这个特性的背后隐藏着一个关于C++虚表的知识点。如果对C++的虚表没有一定的认识就会被绕进去,被搞得稀里糊涂。经过和朋友的一番探讨总算搞清楚。特整理成此文

我们知道支持被聚合使用的CA接口要实现一组和IUnkNown接口一致的非委托UnkNown接口,这一组接口完成实际的QueryInterface,AddRef 和 Release的工作。而真正的IUnkNow接口的工作就仅仅是判断该对象是否是被聚合使用(通过一个指向IUnkNown的指针成员m_pUnkNownOuter),如果该对象被聚合使用(m_pUnkNownOuter不等于Null)则将调用转向m_pUnkNownOuter所指的外部对象CB.

CB在初始化时,聚合使用了CA。如下:

HRESULT CB::Init()
{
    IUnkNown *pUnkNownOuter = (IUnkNown *)this;
    HRESULT result = ::CoCreateInstance(CLSID_CompA,pUnkNownOuter,
                        CLSCTX_INPROC_SERVER,
                        IID_IUnkNown,(void **)& m_pUnkNownInner) ;

    if (Failed(result))
            return E_FAIL;
    
    result = m_pUnkNownInner->QueryInterface(IID_SomeInterface,(void **)&m_pCAInterface);
        if (Failed(result))
    {
        m_pUnkNownInner->Release();
        return E_FAIL;
    }

    pUnkNownOuter->Release();
    return S_OK;
}

1) CB调用CoCreateInstance聚合使用CA时,CB的m_pUnkNownInner指向了CA对象,而CA对象的m_pUnkNownOuter成员也指向了CB对象。

2) CB调用m_pUnkNownInner->QueryInterface(IID_CAInterface,(void **)&m_pCAInterface); 来请求CA的接口,CA的QueryInterface如下:

HRESULT CA::QueryInterface(const IID& iid,void **ppv)
{
    if  ( m_pUnkNownOuter != NULL )
    {
        return m_pUnkNownOuter->QueryInterface(iid,ppv);
    }
    else
    {
        return NondelegationQueryInterface(iid,ppv);
    }
}

由于CA对象的m_pUnkNownOuter不等于Null,调用将被转到CB (return m_pUnkNownOuter->QueryInterface(iid,ppv);)
那么来看看CB的QueryInterface:

HRESULT CB::QueryInterface(const IID& iid,void **ppv)
{
    if ( iid == IID_IUnkNown )
    {
        *ppv = (IUnkNown *) this ;
        ((IUnkNown *)(*ppv))->AddRef() ;
    } else if ( iid == IID_OtherInterface )
    {
        *ppv = (IOtherInterface *) this ;
        ((IOtherInterface *)(*ppv))->AddRef() ;
    } else if ( iid == IID_SomeInterface)
    {
        return m_pUnkNownInner->QueryInterface(iid,ppv) ;
    }
else
    {
        *ppv = NULL;
        return E_NOINTERFACE ;
    }
    return S_OK;
}

CB又将调用转给了CA,CA又再转给CB,,, 死循环!!

不过,实际上,例子程序运行正常,并没有产生死循环。这当中发生了一些我们不知道的事情。这就是C++的虚表在作怪!下来,我们来分析为什么程序运行时没有进入死循环。

先给出CA的定义:
class INondelegatingUnkNown
{
public:
  virtual HRESULT  __stdcall  NondelegationQueryInterface(const IID& iid,void **ppv) = 0 ;
    virtual ULONG     __stdcall  NondelegatingAddRef() = 0;
    virtual ULONG     __stdcall  NondelegationRelease() = 0;
};

class CA : public ISomeInterface,public INondelegatingUnkNown
{
protected:
     ULONG           m_Ref;

public:
     CA(IUnkNown *pUnkNownOuter);
     ~CA();

public :
    // Delegating IUnkNown
    virtual HRESULT      __stdcall  QueryInterface(const IID& iid,void **ppv) ;
    virtual ULONG      __stdcall  AddRef() ;
    virtual ULONG      __stdcall  Release() ;

    // Nondelegating IUnkNown
    virtual HRESULT   __stdcall  NondelegationQueryInterface(const IID& iid,void **ppv);
    virtual ULONG      __stdcall  NondelegatingAddRef();
    virtual ULONG      __stdcall  NondelegationRelease();

    virtual HRESULT __stdcall SomeFunction( ) ;

    private :
        IUnkNown  *m_pUnkNownOuter;  // pointer to outer IUnkNown
};

仔细读《COM原理与应用》第105页,第4行。“(2) CoCreateInstance函数返回之后,得到了m_pUnkNownInner指针,指向对象A的非委托IUnkNown。”
在CB的Init中,虽然m_pUnkNownInner是一个IUnkNow*指针,但得到的是Nondelegating IUnkNown接口,而不是IUnkNown接口。
所以在后面的第二步 result = m_pUnkNownInner->QueryInterface(IID_SomeInterface,(void **)&m_pCAInterface); 时实际被调用的是NondelegationQueryInterface(const IID& iid,void **ppv);

请仔细看CA的Nondelegating IUnkNown接口实现:
HRESULT CA::NondelegationQueryInterface(const IID& iid,void **ppv)
{
    if ( iid == IID_IUnkNown )
    {
       *ppv = (INondelegatingUnkNown *) this ;
         ((IUnkNown *)(*ppv))->AddRef() ;
    } else if ( iid == IID_SomeInterface )
    {
        *ppv = (ISomeInterface *) this ;
        ((ISomeInterface *)(*ppv))->AddRef() ;
    }
    else
    {
        *ppv = NULL;
        return E_NOINTERFACE ;
    }
    return S_OK;
}

那么这究竟是怎么做到的呢?!毕竟m_pUnkNownInner是一个IUnkNow*指针,而且在CB中,我们是这样调用的m_pUnkNownInner->QueryInterface,用得是QueryInterface!而不是NondelegationQueryInterface!怎么会调用到NondelegationQueryInterface呢?
在讲出为什么之前,请大家试用改一下例子程序,把NondelegationQueryInterface的次序小小地调整一下。如下:

class INondelegatingUnkNown
{
public:
  virtual HRESULT  __stdcall  NondelegationQueryInterface(const IID& iid,void **ppv) = 0 ;
    virtual ULONG     __stdcall  NondelegatingAddRef() = 0;
    virtual ULONG     __stdcall  NondelegationRelease() = 0;
};

变成:
class INondelegatingUnkNown
{
public:
    virtual ULONG     __stdcall  NondelegatingAddRef() = 0;
  virtual HRESULT  __stdcall  NondelegationQueryInterface(const IID& iid,void **ppv) = 0 ;
    virtual ULONG     __stdcall  NondelegationRelease() = 0;
};

然后再运行代码,相信你一定会运行时出错的。这是因为我们在CB中调用m_pUnkNownInner->QueryInterface这一句时,并不仅仅是根据函数名QueryInterface来调用函数的,因为CA是一个有虚函数的对象,所以在CA对象中存在一个虚表,当调用函数时,首先是根据函数名来计算函数在虚表中的索引值,然后再用这个索引到虚表中去调用实现的函数。因为QueryInterface在IUnkNown中是第一个被定义的,所以它在虚表中的索引是0,当调用m_pUnkNownInner->QueryInterface时,会去调用虚表中的第一个函数,由于我们改变了INondelegatingUnkNown定义的顺序,INondelegatingUnkNown的虚表中的第一个函数是NondelegatingAddRef,它是没有参数的。所以当发生带着两个参数的调用时,产生了运行时的错误

讲到这里,问题好像清楚了,又好像有哪里没说明白。到底是哪个虚表?哪个对象的虚表?

其实,在我们的讨论中只有两个对象,一个CB,一个CA其他的什么IUnkNown,INondelegatingUnkNown,ISomeInterface只不过是接口而已。很显然CB我们不用关心,主要是CA。

Main

基本上,CA的对象应该是这副德性:

VT

我们注意CA::NondelegationQueryInterface的实现中
if ( iid == IID_IUnkNown )
    {
       *ppv = (INondelegatingUnkNown *) this ;
         ((IUnkNown *)(*ppv))->AddRef() ;
    }
这里对this做了一次强制转换,(INondelegatingUnkNown *) this 表达式的结果就仅仅是INondelegatingUnkNown虚表的指针。而*ppv需要的正好仅仅是这个虚表的指针。所以在在CB中调用m_pUnkNownInner->QueryInterface,程序去找虚表中的第一项。所以实际被调用的就是CA::NondelegationQueryInterface。

相关文章

迭代器模式(Iterator)迭代器模式(Iterator)[Cursor]意图...
高性能IO模型浅析服务器端编程经常需要构造高性能的IO模型,...
策略模式(Strategy)策略模式(Strategy)[Policy]意图:定...
访问者模式(Visitor)访问者模式(Visitor)意图:表示一个...
命令模式(Command)命令模式(Command)[Action/Transactio...
生成器模式(Builder)生成器模式(Builder)意图:将一个对...