COM聚合技术中的QueryInterface

COM聚合技术中的QueryInterface最近在看COM聚合技术时遇到一个关于QueryInterface的问题。在《COM技术内幕》和《COM原理与应用》中都是寥寥数句带过,看起来很易理解,我却看了许久才有所领悟。先说明一下,为了节省篇幅,对于一些约定俗成的代码和变量,下文不再进行说明,如内部组件指向外部组件的m_pUnknownOuter和外部组件指向内部组件的m_pUnknownInner等,这些内容在相关书籍都有描述。问题

大家好,又见面了,我是你们的朋友全栈君。

最近在看COM聚合技术时遇到一个关于QueryInterface的问题。在《COM技术内幕》和《COM原理与应用》中都是寥寥数句带过,看起来很易理解,我却看了许久才有所领悟。

先说明一下,为了节省篇幅,对于一些约定俗成的代码和变量,下文不再进行说明,如内部组件指向外部组件的m_pUnknownOuter和外部组件指向内部组件的m_pUnknownInner等,这些内容在相关书籍都有描述。

问题描述:

在外部组件CB聚合内部组件CA时,内部组件的非委托未知接口示意如下:

struct INondelegatingUnknown
{
    virtual HRESULT __stdcall NondelegatingQueryInterface(const IID&, void**) = 0;
    virtual ULONG __stdcall NondelegatingAddRef() = 0;
    virtual ULONG __stdcall NondelegatingRelease() = 0;
};

这里,查询函数为NondelegatingQueryInterface。

NondelegatingQueryInterface的实现示意代码如下:

HRESULT __stdcall CA::NondelegatingQueryInterface(const IID& iid, void** ppv)
{
    if (iid == IID_IUnknown)
    {
        *ppv = static_cast<INondelegatingUnknown*>(this);
    }
    else if (iid == IID_IY)
    {
        *ppv = static_cast<IY*>(this);
    }
    .....
}

同时,CA还要实现内部组件的委托未知接口:

ULONG __stdcall CA::QueryInterface(const IID& iid, void** ppv)
{
    return m_pUnknownOuter->QueryInterface(iid, ppv);
}

现在假设外部组件CB实现了接口IX, 内部组件CA实现了组件IY,那么根据上述两本书中的描述,在CB查询IY接口时使用如下代码:

m_pUnknownInner->QueryInterface(IID_IY, ppv);

那么问题来了,通过上述代码来看,理论上应该会调用CA::QueryInterface(),而根据QueryInterface的实现,又会调用回外部组件的查询函数CB::QueryInterface(),从而形成了一个死循环!而实际运行当然不会出现这种情况,在查询IY接口时,会调用NondelegatingQueryInterface而非QueryInterface!原因何在?

书中对于这个问题的解释很简单,在外部组件CB创建CA时,获取m_pUnknownInner即内部组件的IUnknown接口时,使用NondelegatingQueryInterface进行了查询,注意该函数的实现,查询IUnknown接口时对CA的this指针进行了强制转换,转换成了非委托未知接口。书中特意强调“通过这一转换,我们可以保证返回的是一个非委托的未知接口指针,当向委托接口指针查询IID_IUnknown时,他返回的将总是一个指向其自身的指针”。我不是很明白这段话的意思,但是从现象上看,正是由于这个强制转换使得外部组件在查询内部组件的接口时能够正确运行。

其实这个问题涉及了一些很基础的知识,在学习C++的时候我自以为理解了这些基础,可是当遇到问题时甚至不知道原来和这些基础的内容有关!

首先我在这里推荐几篇文章,对于理解这个问题很有帮助:

http://blog.csdn.net/haoel/article/details/3081328

http://blog.csdn.net/haoel/article/details/3081385

这两篇讲解C++内存对象布局很好。如果读者对这些内容了解并且对上述问题也很清楚那么就不必看我献丑了…谢谢…

好,继续说问题。

简单来说,问题是明明调用了QueryInterface函数,结果却是调用了NondelegatingQueryInterface。那么再看一下内部组件CA的数据结构:

class CA : public IY, public INondelegatingUnknown
{
public:
    virtual HRESULT __stdcall QueryInterface();
    virtual ULONG __stdcall AddRef();
    virtual ULONG __stdcall Release();

    virtual HRESULT __stdcall NondelegatingQueryInterface(const IID&, void**);
    virtual ULONG __stdcall NondelegatingAddRef();
    virtual ULONG __stdcall NondelegatingRelease();
};

看到这段数据结构,再联想之前的强制转换,会不会有什么想法呢?

如果没有,那么再看下IUnknown的数据结构:(注意这不是系统中的定义,而是对IUnknown的示意,不过也差不多就是了)

interface IUnknown
{
    virtual HRESULT __stdcall QueryInterface() = 0;
    virtual ULONG __stdcall AddRef() = 0;
    virtual ULONG __stdcall Release() = 0;
};

比较NondelegatingQueryInterface的结构,可以发现二者在结构上一致的。

在《COM技术内幕》中还有这样一段话“COM并不关心接口的名字是什么,而只关心vtbl的结构。”这回是不是突然感觉好像明白了什么?

是的,因为IUnknown的结构和NondelegatingQueryInterface一致,因此在强制转换时,将this强制转换成了NondelegatingQueryInterface,而此时外部组件获得的m_pUnknownInner指针的值并不是内部组件CA的地址,而是CA中NondelegatingQueryInterface结构的地址!读者可能会疑惑,在CA的实现中并没有NondelegatingQueryInterface结构啊?如果你看了我推荐了那两篇博客文章,此时你就可能已经完全明白了。不过,我们还是来一步步分析吧。

首先,我们要验证的第一个问题是,对于多重继承,将派生类的指针强制转换成基类类型之后,是否就会出现和上述问题一些样的现象?

示例代码如下:

#include <iostream>
using namespace std;

class Base1
{
public:
    virtual void func1() = 0;
    virtual void func2() = 0;
};

class Base2
{
public:
    virtual void anotherFunc1() = 0;
    virtual void anotherFunc2() = 0;
};

class Derived : public Base1, public Base2
{
public:
    virtual void func1() { cout << "Base1::func1" << endl; }
    virtual void func2() { cout << "Base1::func2" << endl; }

    virtual void anotherFunc1() { cout << "Base2::anotherFunc1" << endl; }
    virtual void anotherFunc2() { cout << "Base2::anotherFunc2" << endl; }
};

int main()
{
    Derived d;

    cout << "------------------------" << endl;
    d.func1();
    d.func2();
    d.anotherFunc1();
    d.anotherFunc2();

    cout << "------------------------" << endl;
    Base1* pB1 = (Base1*)&d;
    pB1->func1();
    pB1->func2();
    
    cout << "-------Caution----------" << endl;
    pB1 = (Base1*)(Base2*)&d;
    pB1->func1();
    pB1->func2();

    system("pause");
    return 0;
}

代码中,最后的一个测试,我们将d先强转成了Base2,然后又强转成了Base1,那么运行结果:

------------------------
Base1::func1
Base1::func2
Base2::anotherFunc1
Base2::anotherFunc2
------------------------
Base1::func1
Base1::func2
-------Caution----------
Base2::anotherFunc1
Base2::anotherFunc2

在调用Base1的函数时,实际运行的确实是Base2的函数!

可以分析得出,在由&d转换成Base2*时,指针值发生了变化,也就是说,新的指针pB1和&d的值已经不同了:

    cout << "-------Pointer----------" << endl;
    cout << (int*)&d << endl;
    cout << (int*)pB1 << endl;

结果如下:

-------Pointer----------
0012FE5C
0012FE60

指针的值确实发生了变化,转换后的指针偏移了4 Byte,那么为什么会有这样的结果?答案就是C++类的虚函数表。

在C++的类中,如果使用了继承关系,类的结构中就会有一个虚函数表,读者可以自己测试一下,如果是一个没有任何内容的空类,其大小为1 Byte,这个是系统自动填充的内容。如果是其中包含了一个虚函数,那么其大小就为4 Byte,而这个4Byte就是虚函数表指针的大小。多重继承的情况下,在类的结构中会有多个基类的虚函数表,比如上例,Derived类继承了Base1和Base2,那么其中就有2个虚函数表,在我们调用虚函数时,会从对应的虚函数表中进行查询:

COM聚合技术中的QueryInterface

在多重继承中,派生类中对于基类中虚函数表和各成员的排列顺序与继承的顺序一致,最后才是派生类自己的成员:

COM聚合技术中的QueryInterface

由于这样的数据结构,在进行强制转换时,实际上是将虚函数表的指针传出,故转换后指针的值发生了变化。至于为什么是传的虚函数表的指针而不是某个成员的指针呢?因为在内存结构中虚函数表是位于最上部的,虚函数表类似于header。

好了,现在对于最开始的问题基本已经明白了。外部组件CB创建CA时需要获取内部组件CA的IUnknown指针,创建过程中使用NondelegatingQueryInterface进行IUnknown的获取,该函数中将指向CA组件自己的指针强制转换成了非委托未知接口的指针,根据CA的继承关系,转换后的指针发生了变化,该指针实际上是NondelegatingUnknown的虚函数表的指针,因此,外部组件CB使用m_pUnknownInner查询时,实际上使用的是NondelegatingUnknown其中的函数。

还有一个遗留的小问题:虽然我们获取了NondelegatingUnknown的指针,可是函数名不同为什么依然可以调用?还记得书中那句话么:“COM并不关心接口的名字是什么,而只关心vtbl的结构。”NondelegatingUnknown和Unknown在结构上是相同的,在传递给m_pUnknownInner时,发生了隐式转换,所以根据函数在内存中的位置,可以找到对应函数,而且,虚函数的调用是运行时确定,运行时的程序不过是一堆0和1,函数名什么的对于这些机器码没有什么意义,那些不过是高级语言给我们看的罢了。

以上是我个人的分析和总结,并不一定是真实的实现,因为我也在网上看到了一些不同的分析。欢迎大家一起讨论。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/162929.html原文链接:https://javaforall.net

(0)
全栈程序员-站长的头像全栈程序员-站长


相关推荐

  • 充分条件和必要条件的口诀_充分必要条件的例子100个

    充分条件和必要条件的口诀_充分必要条件的例子100个充分条件:如果条件A是结论B的充分条件:A与其他条件是并连关系,即A、C、D….中任意一个存在都可以使得B成立(就像是个人英雄主义),如下图:<imgsrc="https://p

    2022年8月6日
    7
  • Postman下载与安装操作步骤【超详细】

    Postman下载与安装操作步骤【超详细】工欲善其事必先利其器,一套超详细清晰的下载安装postman教程,让小伙伴们轻轻松松安装好postman,快来试一试吧!!!

    2022年6月16日
    58
  • powerdesigner创建数据库模型(概念模型举例)

    1.启动PowerDesigner(我用的PowerDesigner16.7破解版)选择新建概念模型进行数据库设计的E-R模型辅助设计2.概念模型的设计实体:选择实体图形,在“图纸”点击划出实体来,双击为其命名,选择Attributes添加其所有属性。注意所有的name都可以用中文标示,以好理解;但是code必须用英文标示,以方便库的操作处理(PowerDesigner转化数据库.sql文件,所有的表名称,属性等都采用code)。为每个属性命名,并选择相应的数据类型,PowerDesigner

    2022年4月11日
    71
  • Hans Berger脑电图之父的人生摘要「建议收藏」

    Hans Berger脑电图之父的人生摘要「建议收藏」摘要:在1938年当脑电图被学术界接受之时,第二次世界大战要开始了,因为英美法都是敌对国,Berger访美计划搁浅。同时大概因为只是英美法学者对脑电图重视,德国本土学者根本不相信,德国当时的纳粹政权禁止研究脑电图。【转载】HansBerger(1873-1941)(上图)是德国精神病学家,精神生理学家,他对神经科学的贡献是发明了和命名了脑电图-Electroencephalography,EEG,德语是Elektrenkephalogramm。此外,Berger发现了“阿尔法波-AlphaWave”,

    2022年8月11日
    8
  • JUnit中对Exception的判断

    JUnit中对Exception的判断

    2021年9月15日
    54
  • modbus通讯协议解析

    modbus通讯协议解析1.什么是modbus协议,主要应用在哪些方面?(来源于:http://www.emtronix.com/product/ModBus_software.html) Modbus协议是一种已广泛应用于当今工业控制领域的通用通讯协议。通过此协议,控制器相互之间、或控制器经由网络(如以太网)可以和其它设备之间进行通信。Modbus协议使用的是主从通讯技术,即由主设备主动查询和操作从设备。一般将主控

    2022年7月13日
    18

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号