COM 学习笔记 (2) : 接口查询

IUnknown 接口概述

引述

客户对组件的了解是非常有限的,为知道某个组件是否支持某个特定的接口,客户可以在运行时询问组件。即使组件不支持所需要的某个接口,客户也可以在请求失败时很好地处理这种情形

IUnknown 接口

COM 中所有内容最终都起于接口、又最终归于接口。所有的 COM 接口都继承于 IUnknown 接口,该接口的定义如下:

// 该接口在头文件 unknwn.h 中定义,interface 是与 struct 绑定的宏,这样所有成员默认是 public 的,刚好符合接口的封装需求

interface IUnknown {
	virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) = 0;
	virtual ULONG __stdcall AddRef() = 0;
	virtual ULONG __stdcall Release() = 0;
};

接口查询

因为 所有的 COM 接口都继承于 IUnknown 接口,所以客户只需要一个 IUnknown 接口的指针,而且无需知道它所拥有的接口指针到底是什么类型的,就可以查询其他接口

由于所有的 COM 接口都继承了 IUnknown,所以每个接口的虚函数表(vtbl)中的前三个函数都是 QueryInterface、AddRef 和 Release。这便使得所有 COM 接口都可当做 IUnknown 接口来处理

纯虚函数 QueryInterface

IUnknown 包含一个名为 QueryInterface 的纯虚成员函数,客户可以通过此函数来查询某个组件是否支持某个特定的接口。若支持,则 QueryInterface 将返回一个指向此接口的指针,否则返回值将是一个错误代码。然后客户可以接着查询其他接口或将组件卸载

  • QueryInterface 的函数原型
HRESULT __stdcall QueryInterface(const IID &iid, void **ppv);
  • 参数 iid

iid 是一个接口标识符(IID)结构, 用于标识客户所需的接口,从客户的角度上来说,指定一个 IID 以使得 QueryInterface 函数利用此接口标识符来查询接口

  • 参数 ppv

ppv 是一个二级指针,QueryInterface 函数将存放客户所请求的接口的地址到其中

  • 返回值(HRESULT 类型)

HRESULT 是具有特定格式的 32 位值,此函数返回 S_OK 或者 E_NOINTERFACE。对其返回值进行判断时应该使用 SUCCEEDED 宏或 FAILED 宏,不应该直接比较


// 使用示例
void foo(IUnknown *pI) {
    // 为接口定义一个指针
    IX *pIX = nullptr;
    
    // 使用 IUnknown 的 QueryInterface 函数来查询接口 IX(接口标识符是 IID_IX)
    HRESULT hr = pI->QueryInterface(IID_IX, (void **)&pIX);

    // 检查返回值
    if (SUCCEEDED(hr)) {
        // 可以使用接口了 (比如调用某个函数)
        pIX->Fx();
    }
} 

QueryInterface 的实现

QueryInterface 的实现其实比较简单。它只需根据某个给定的 IID(接口标识符),返回相应接口的指针。若组件支持,函数就返回 S_OK 以及相应的接口的指针,否则函数就应返回 E_NOINTERFACE,并将指针值置为 nullptr

  • 示例代码
// 一些示例接口、实现类
interface IX : IUnknown { /*...*/ };
interface IY : IUnknown { /*...*/ };
class CA : public IX, public IY { /*...*/ };

// QueryInterface 在 CA 类上的实现
HRESULT __stdcall CA::QueryInterface(const IID &iid, void **ppv) {

    if (iid == IID_IUnknown) {
        // 客户需要 IUnknown 接口
        *ppv = static_cast<IX *>(this);

    } else if (iid == IID_IX) {
        // 客户需要 IX 接口
        *ppv = static_cast<IX *>(this);

    } else if (iid == IID_IY) {
        // 客户需要 IY 接口
        *ppv = static_cast<IY *>(this);

    } else {
        // 不支持的接口,返回 E_NOINTERFACE,并将返回指针置为 nullptr
        *ppv = nullptr;
        return E_NOINTERFACE;
    }
    
    // 增加引用计数(暂时先不关心)
    static_cast<IUnknown *>(*ppv)->AddRef();

    // 接口受支持,返回 S_OK
    return S_OK;
}

完整示例代码

// 本代码修改自《COM技术内幕》第 35 页

#include <iostream>
#include <objbase.h>

using namespace std;

void trace(const char* msg) { cout << msg << endl; }

interface IX : IUnknown {
    virtual void __stdcall Fx() = 0;
};

interface IY : IUnknown {
    virtual void __stdcall Fy() = 0;
};

interface IZ : IUnknown {
    virtual void __stdcall Fz() = 0;
};

extern const IID IID_IX;
extern const IID IID_IY;
extern const IID IID_IZ;

class CA : public IX, public IY {
    // IUnknown implementation
    virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv);
    virtual ULONG __stdcall AddRef() { return 0; }
    virtual ULONG __stdcall Release() { return 0; }

    // Interface IX implementation
    virtual void __stdcall Fx() { cout << "Fx" << endl; }
	
    // Interface IY implementation
    virtual void __stdcall Fy() { cout << "Fy" << endl; }
};

HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv) {
	if (iid == IID_IUnknown) {
	    trace("QueryInterface: Return pointer to IUnknown");
	    *ppv = static_cast<IX *>(this);
	}
	else if (iid == IID_IX) {
	    trace("QueryInterface: Return pointer to IX");
	    *ppv = static_cast<IX *>(this);
	}
	else if (iid == IID_IY) {
	    trace("QueryInterface: Return pointer to IY");
	    *ppv = static_cast<IY *>(this);
	}
	else {
	    trace("QueryInterface: Interface not supported.");
	    *ppv = NULL;
	    return E_NOINTERFACE;
	}

	reinterpret_cast<IUnknown *>(*ppv)->AddRef();
	return S_OK;
}

IUnknown* CreateInstance() {
	IUnknown* pI = static_cast<IX*>(new CA);
	pI->AddRef();
	return pI;
}

static const IID IID_IX = 
{ 0x32bb8320, 0xb41b, 0x11cf, {0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82} };

static const IID IID_IY =
{ 0x32bb8321, 0xb41b, 0x11cf, {0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82} };

static const IID IID_IZ =
{ 0x32bb8322, 0xb41b, 0x11cf, {0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82} };

int main() {
    HRESULT hr;
    trace("Client: Get an IUnknown pointer.");
    IUnknown* pIUnknown = CreateInstance();

    trace("Client: Get interface IX.");
    IX *pIX = NULL;
    hr = pIUnknown->QueryInterface(IID_IX, (void**)& pIX);
    if (SUCCEEDED(hr)) {
        trace("Client: Succeeded getting IX.");
        pIX->Fx();
    }

    trace("Client: Get interface IY.");
    IY *pIY = NULL;
    hr = pIUnknown->QueryInterface(IID_IY, (void**)& pIY);
    if (SUCCEEDED(hr)) {
        trace("Client: Succeeded getting IY.");
        pIY->Fy();
    }

    trace("Client: Ask for an unsupported interface.");
    IZ *pIZ = NULL;
    hr = pIUnknown->QueryInterface(IID_IZ, (void**)& pIZ);
    if (SUCCEEDED(hr)) {
        trace("Client: Succeeded getting IZ.");
        pIZ->Fz();
    }
    else {
        trace("Client: Could not get interface IZ.");
    }

    trace("Client: Get interface IY from interface IX.");
    IY *pIYfromIX = NULL;
    hr = pIX->QueryInterface(IID_IY, (void**)& pIYfromIX);

    if (SUCCEEDED(hr)) {
        trace("Client: Succeeded getting IY.");
        pIYfromIX->Fy();
    }

    trace("Client: Get interface IUnknown from IY.");
    IUnknown *pIUnknownFromIY = NULL;
    hr = pIX->QueryInterface(IID_IUnknown, (void**)& pIUnknownFromIY);
    if (SUCCEEDED(hr)) {
        cout << "Are the IUnknown pointers equal?    ";
        if (pIUnknownFromIY == pIUnknown) {
	    cout << "Yes, pIUnknownFromIY == pIUnknown." << endl;
        }
        else {
            cout << "No, pIUnknownFromIY != pIUnknown." << endl;
        }
    }

    delete pIUnknown;
    return 0;
}

若运行将有如下输出结果

Client: Get an IUnknown pointer.
Client: Get interface IX.
QueryInterface: Return pointer to IX
Client: Succeeded getting IX.
Fx
Client: Get interface IY.
QueryInterface: Return pointer to IY
Client: Succeeded getting IY.
Fy
Client: Ask for an unsupported interface.
QueryInterface: Interface not supported.
Client: Could not get interface IZ.
Client: Get interface IY from interface IX.
QueryInterface: Return pointer to IY
Client: Succeeded getting IY.
Fy
Client: Get interface IUnknown from IY.
QueryInterface: Return pointer to IUnknown
Are the IUnknown pointers equal?    Yes, pIUnknownFromIY == pIUnknown.

参考资料

[1] (美)Dale Rogerson , 李忠 , 王晓波 . COM 技术内幕 – 微软组件对象模型 [M] . 北京 : 清华大学出版社 , 1991.

COM 学习笔记 (1) : COM 简明概念

COM 的简明概念

面向组件编程的优点

  • 应用的可定制性

用户希望能够定制他们的应用,而程序员可以为用户建立应用定制方案 —— 通过组件架构本身的可定制性(用户能使用需要的组件来将某个组件替换掉)

  • 组件库

组件架构拥有快速应用开发的优点,程序员可以某个组件库中取出所需的组件并将其快速地组装到一起,构造出所需的应用(如同搭积木)

  • 分布式组件

对于 C/S(客户机 / 服务器)模式的应用而言,应用已经被划分成位于远程的各个功能部分,因为任一组件均可做替换,所以可以将某个组件替换为专门负责同某个远程组件通信的组件

COM 的特点

COM 可以满足前面讨论过的所有组件架构的要求。它使用 DLL 来提供可在运行时被替换掉的组件。COM 借助于如下一些手段保证这些组件可以充分利用动态链接所带来的各种好处

  • 提供了一个所有组件都应遵守的标准
  • 允许使用组件的多个不同的版本,而且这一点对于客户机而言几乎是透明的
  • 使得可以按相同的方式来处理类似的组件
  • 定义了一个与语言无关的架构
  • 支持对远程组件的透明链接

COM 接口(Interface)

在 COM 中,接口是一个包含一个函数指针数组的结构,数组的每一个元素包含的是一个由组件实现的函数的地址

对 COM 而言,接口就是这个内存结构,其他的都不是 COM 需要关心的细节

在 C++ 中,我们使用抽象基类(abstract base class)来实现 COM 接口

因为一个 COM 组件可以支持任意数目的接口,因为对于此类组件,应使用抽象基类的多重继承来实现之

COM 接口的关键要素

  • COM 接口在 C++ 中是用纯抽象基类实现的
  • 一个 COM 组件可以支持多个接口
  • 一个 C++ 类可以使用多继承来实现多个接口的组件

参考资料

[1] (美)Dale Rogerson , 李忠 , 王晓波 . COM 技术内幕 – 微软组件对象模型 [M] . 北京 : 清华大学出版社 , 1991.