65.9K
CodeProject 正在变化。 阅读更多。
Home

从零开始的 COM - 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (112投票s)

2004年4月18日

CPOL

13分钟阅读

viewsIcon

302751

downloadIcon

9276

一篇关于 COM 的文章。

Screen shot from the demo application

引言

本文档讨论 COM 和 COM 组件。通过实现一个简单的 COM 组件,它解释了 COM 技术背后的一些细节。本文档首先提供一些关于 COM 的背景信息,然后引导读者在示例中实现并改进一个简单的 COM 组件。示例从在同一个文件中实现一个 COM 组件和一个 COM 客户端开始。示例的改进在于将 COM 组件与其客户端分离。客户端和组件首先被分离到两个不同的文件中,然后组件会被放入一个 DLL 中,该 DLL 可以被加载到客户端的地址空间中。最后的改进是将组件注册到 Windows 注册表中,这样客户端就不再与组件绑定,而是能够通过类工厂创建它。在接下来的插图中,组件与其客户端之间的关系用一条链表示,当组件通过类工厂创建时,这条链将被完全断开。在最后一部分,通过在新的 COM 组件中重用已实现的组件来解释 COM 封装。本文档仅涉及第一部分,其他部分将在另外两篇文章中进行解释。演示应用程序的代码(客户端 + 3 个组件服务器)与文章中解释的示例非常相似,并且只使用一个窗口来可视化组件本身。下图说明了第一部分和第二部分。

Summary of part1 and part2

假设

您熟悉基本的 C++ 和 Windows 编程。

第一部分 - 背景

一种创建软件组件的二进制标准

在面向对象编程中,如果未使用通用的标准框架,不同供应商创建的软件对象无法相互交互。COM 或 Component Object Model 是一种用于创建与语言无关的软件组件的二进制标准,它旨在将应用程序分解为独立的模块。COM 的实现对用户(COM 客户端)是隐藏的,这意味着 COM 组件以二进制形式提供,并且已经编译、链接并准备好使用,它们只是一堆 1 和 0,也就是说,它们只是机器代码,可以在客户端与它们交互时执行。以下是 COM 的一些优点:

  1. 语言无关性(可以使用不同的语言创建 COM 组件)。
  2. 重用应用程序架构。
  3. 无需重新构建即可轻松扩展应用程序的功能。

COM 组件的功能

COM 中的对象或组件是任何通过接口机制暴露其功能的结构。在 C++ 应用程序中,接口被定义为抽象基类。接口是一个 C++ 类,其中只包含纯虚成员函数。这意味着接口不包含任何实现,只规定了供其他类实现的函数签名。纯抽象基类定义了 COM 所需的接口的特定内存结构,当我们定义一个纯抽象基类时,我们实际上是在定义一个内存块的布局。在派生类中实现抽象基类之前,不会为该结构分配内存。因此,在下面的代码片段中,IComponent 既是一个接口(因为它遵循 COM 规范的内存布局),也是一个纯抽象基类。

//----------------------------------------------------------------//
// Definition of component's functionallity  through an interface //
//----------------------------------------------------------------//
// The keyword "interface" is just an alias for "struct"
interface IComponent 
{
    // Methods for the implementation of the component's functionalities:
    virtual void __stdcall Function1()=0;
    virtual void __stdcall Function2()=0;
     virtual void __stdcall Function3()=0;
};

如以下图像所示,在 C++ 中构成 COM 组件接口的纯抽象基类定义的内存块分为两部分(顶部图说明了演示应用程序中的 Component2)。

  1. 虚函数表或 vtbl,它是一个指向虚函数实现的指针数组。
  2. 指向 vtbl 的指针,称为 vtbl 指针。

Component and Interface

结论:在 COM 中,组件的功能通过它们的接口获得,这些接口是指向组件方法的地址或入口。

COM 对象和接口的常见需求(基本操作)

在一个支持多种功能和接口的组件中,需要能够轻松访问这些接口。另一个要求是客户端应该能够管理组件的存在,并在使用完组件后释放它。COM 组件是一个支持通过所谓的 IUnknown 接口进行这些操作的组件。通过继承和实现这个接口,COM 对象允许客户端访问两个基本操作:

  1. 通过 QueryInterface 方法在对象上的多个接口之间导航。
  2. 通过称为 AddRefRelease 的方法处理的引用计数机制来控制对象的生命周期。

这三个方法构成了 IUnknown 接口,所有其他接口都继承自它。所有 COM 接口都必须继承自 IUnknown。这意味着 vtbl 的前三个条目对于所有 COM 接口都是相同的。它们是 IUnknown 接口中三个方法实现的地址。因此,在下面的代码中,IComponent 接口通过继承 IUnknown 接口成为一个 COM 接口。

//------------------------------------------------------------//
// Being a COM interface by inheriting from IUnknown interface//
//------------------------------------------------------------//
interface IComponent :public IUnknown;
{
    virtual void __stdcall Function1()=0;
    virtual void __stdcall Function2()=0;
     virtual void __stdcall Function3()=0;
};

因为每个 COM 组件都继承自 IUnknown,所以拥有 IUnknown 接口指针的客户端不知道它拥有什么类型的接口指针,它只能使用 QueryInterface 方法查询其他接口。这就是为什么这个接口被称为未知接口或 IUnknown。下图显示了 IComponent 接口在继承自 IUnknown 接口后成为 COM 接口。

Inheritting from IUnknown interface

结论:IUnknown 接口是 COM 中的基础接口,它包含所有接口和对象的基本操作。

COM 组件的类(实现)

当组件的功能通过 COM 接口由其方法(如 Function1-Function3)定义时,可以通过从新创建的 COM 接口派生一个类来定义组件的类。在下面的示例中,该类称为 CComponent

//----------------------------------// 
//Definition of the Component's class
//----------------------------------// 
class CComponent:public IComponent
{
public:

//-------------------------------------------------------------------------//
//IUnknown methods,which are the basic oparation
//required for all COM components:
//-------------------------------------------------------------------------//
   virtual HRESULT __stdcall QueryInterface(const IID& iid,void** ppv); 
   virtual ULONG __stdcall AddRef();
   virtual ULONG __stdcall Release();

//-------------------------------------------------------//   
//The new methods, which the interface supports:
//-------------------------------------------------------//
   virtual void __stdcall Function1();
   virtual void __stdcall Function2();
   virtual void __stdcall Function3();
 
private:
    // Component's member data
    long m_cRef;// The reference count variable
};

下图说明了 CComponent 类的内存布局。

Inheritting from IUnknown interface

如图所示,像 CComponent 类的 "this" 指针这样的接口指针指向 vtbl 指针。在 COM 中,组件仅通过方法访问,绝不能直接通过变量访问,纯抽象基类只有纯虚函数,不包含实例数据。为了调用组件的方法之一(例如 Function1),可以使用 IUnknown 指针查询或请求一个指向 IComponent 接口的指针,并通过使用该指针,可以调用所需的方法。

//---------------------------------------------//
//Getting a pointer to the IComponent interface:
//---------------------------------------------//
IComponent* pIComponent=NULL;
//The "IID_IComponent" is the interface ID for the IComponent interface 
pIUnknown->QueryInterface(IID_IComponent,(void**) &pIComponent;
// Calling the component's method:
pIComponent->Function1();

如前所述,我们可以通过 QueryInterface 方法在 COM 对象上的接口之间导航。接口封装了组件的实现细节。每当我们想访问一个组件时,我们想获得一些所需的功能,所以很明显,在使用组件之前,我们已经知道了组件的功能。如果我们不知道组件有哪些功能,我们就无法使用它。这意味着组件的客户端应该知道组件支持哪些类型的功能或接口。每个 COM 接口都有一个接口标识符,客户端可以使用它来查询特定的接口。接口标识符是 128 位的值,在前面的代码片段中,IComponent 接口由 IID_IComponent 标识,这是 IComponent 接口的接口标识符。因此,每当客户端想使用组件的某个功能时,它应该知道哪个接口实现了该功能,并且在通过 QueryInterface 方法查询该接口时,必须提供一个接口标识符。组件可以与窗口类比,其接口与窗口的菜单类比。窗口菜单就像一个接口;它是通过鼠标指针选择其项目来获得各种功能的入口。事实上,演示应用程序中三个组件的接口就像菜单,并且可以通过菜单项访问组件方法。下图显示了其中一个组件。

窗口对象被用作组件的成员数据,以可视化组件,并且如图所示,该组件是一个窗口,其菜单充当其接口,并且可以通过菜单项使用鼠标指针(类似于接口指针)来访问组件的方法。

Components Interface

结论:COM 类是特定接口的特定实现。

COM 对象实例化

如前所述,获取组件功能唯一的方法就是通过其接口,所以一旦我们获得一个指向组件 IUnknown 接口的指针,我们就可以访问组件支持的所有其他接口(使用 QueryInterface 方法),并且因为所有 COM 接口都继承自 IUnknown 接口,所以每个接口指针也是一个 IUnknown 接口指针。COM 对象是通过使用 new 运算符创建的,并且可以通过将指向对象的指针转换为接口指针或 IUnknown 指针来获得创建对象的接口指针,如下所示:

//When new operator is used to allocate a single object,
//it gives a pointer to that object

IUnknown* pIUnknown = static_cast<IComponent*>(new CComponent);

接口标识符

接口标识符是一种 GUID(全局唯一标识符)类型的结构。GUID 有两种格式:字符串和数字。在 Windows 注册表中,GUID 的字符串格式出现在各种位置,然而,当 GUID 在客户端应用程序或实际 COM 对象实现中使用时,需要数字表示。_GUID 结构定义在 basetyps.h 头文件中,如下所示:

//-------------------------------------------//
// GUID definition:
//------------------------------------------//
typedef struct _GUID
 {          
    unsigned long  Data1;   // 32 bits
    unsigned short Data2;   // 16 bits
    unsigned short Data3;   // 16 bits
    unsigned char  Data4[8];// 8*8 = 64 bits
  //-----------------------------------------// 
  //    Total bits = 128 bits
} GUID;

您的开发环境将包含一个名为 UUIDGEN.EXEGUIDGEN.EXE 的工具,它会为您提供一个或多个 GUID,您可以将其包含在源代码中。我在本文的所有示例中都使用了 GUIDGEN.EXE 来创建 GUID。

从零开始的示例

现在,您已经能够创建一个简单的 COM 对象并让客户端使用其功能。使用以下步骤,您可以创建文章开头插图中所示的示例。

您可以下载每个部分的源代码,并复制粘贴一些部分以快速创建您的应用程序。

步骤 1

使用 AppWizard,创建一个简单的 Win32 控制台应用程序并选择一个空项目。此应用程序将作为 COM 客户端,同时也将包含组件本身。

第二步

创建一个扩展名为 "cpp" 的源文件并为其命名。我使用了名称 ClientAndComponent.cpp

步骤 3

我们来为组件确定一个基本功能,例如,组件应该能够在屏幕上打印短语 "COM from scratch"。如前所述,COM 组件通过接口机制暴露其功能,因此需要创建一个接口,并且如果组件要成为 COM 组件,该接口应该继承自 IUnknown 接口。所以,让我们将接口命名为 IComponent,让它继承自 IUnknown 接口,并在一个名为 Print 的方法中定义所需的功能。

//-------------------------//
//Interface definition:
//-------------------------//
interface IComponent:IUnknown
{
//The new method for assigning component's functionality
virtual void __stdcall Print(const char* msg)=0;
};

步骤 4

为了标识 IComponent 接口,请使用开发环境中的 UUIDGEN.EXEGUIDGEN.EXE 工具为其创建一个接口标识符。

//-----------------------------------------------//
 //Interface identifier, which is a 128-bit value.// 
 //-----------------------------------------------//
 // {853B4626-393A-44df-B13E-64CABE535DBF} string format
static const IID IID_IComponent = 
{ 0x853b4626, 0x393a, 0x44df, //Data1,Data2,Data3
{ 0xb1, 0x3e, 0x64, 0xca, 0xbe, 0x53, 0x5d, 0xbf } };  //Data4

步骤 5

可以通过从定义的接口(IComponent)派生类来定义组件的类。

//----------------------------------// 
//Definition of the Component's class
//----------------------------------// 
class CComponent:public IComponent
{
public:

//---------------------------------------------------------------------------//
//IUnknown methods,which are the basic oparation 
//required for all COM components:
//---------------------------------------------------------------------------//
   virtual HRESULT __stdcall QueryInterface(const IID& iid,void** ppv); 
   virtual ULONG __stdcall AddRef(){return 0;} 
   virtual ULONG __stdcall Release(){return 0;} 

//-------------------------------------------------------//   
//The new method, which the interface should support.
//-------------------------------------------------------//
   virtual void __stdcall Print(const char* msg);
};

步骤 6

现在有必要实现这些方法。

Implementation of the Component's methods

步骤 7

创建一个函数来实例化组件类的对象。

Instantiating of Components object

步骤 8

最后一步是创建客户端,该客户端通过组件的接口使用组件的功能。

//----------------//
//   Client
//----------------//
void main()
{
IUnknown* pIUnknown=CreateInstance();
IComponent* pIComponent=NULL;
pIUnknown->QueryInterface(IID_IComponent,(void**)&pIComponent);
pIComponent->Print("COM from scratch.");
}

IUnknown 接口方法的更好实现

  • AddRef 方法

    为了控制组件类实例化对象的生命周期,需要一个变量作为引用计数。当组件在使用中时,此变量应递增,当客户端不再使用组件时,应递减。AddRef()Release() 方法可用于递增和递减此变量。因此,通过向组件类添加一个私有成员变量并调用 AddRef()Release() 方法,可以在组件不再需要时控制组件的生命周期,并在该变量达到零值时释放为组件分配的内存。因此,为了控制组件的生命周期,请重新定义组件类。

    //
    // Component
    //
    class CComponent : public IComponent
    {
        //IUnknown interface's methods:
        virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ;
        // method for incrementing the reference count variable "m_cRef"
        virtual ULONG __stdcall AddRef();
        // method for decrementing the reference count variable "m_cRef"
        virtual ULONG __stdcall Release();
    
        //IComponent interface's method: 
        virtual void __stdcall Print(const char* msg);
    
    public:
         CComponent() ;// Constructor
        ~CComponent();//  Destructor
    
    private:
    
        long m_cRef ;// The reference count variable
           
    };

    在构造函数中将引用计数变量初始化为零。

    /////////////////////////////////
    CComponent::CComponent()
    {
         Print("Constructing the component...") ;
         m_cRef=0;
    }
    
    ////////////////////////////////////////
    CComponent::~CComponent()
    {
     Print("Destructing the component...") ;
    }

    实现 AddRef() 方法。

    //////////////////////////////////////
    ULONG __stdcall CComponent::AddRef()
    {
        Print("Incrementing the reference count variable...");
        return InterlockedIncrement(&m_cRef);
    }
  • Release() 方法

    如上所述,Release() 方法用于递减引用计数,在该方法中,如果引用计数达到零,可以通过删除 this 指针来销毁组件对象。

    //////////////////////////////////////
    ULONG __stdcall CComponent::Release() 
    {
      Print("Decrementing the reference count variable...");
      if(InterlockedDecrement(&m_cRef) == 0)
        {
            delete this ;
            return 0 ;
        }
      return m_cRef ;
    }
  • QueryInterface 方法

    程序中 QueryInterface 方法的一个限制是它不会告知客户端是否查询了不支持的接口。HRESULT 是 COM 错误报告中的关键类型,它是一个简单的 32 位值。COM 组件使用 HRESULT 向其客户端报告情况。与其他 COM 接口一样,QueryInterface 方法返回一个 HRESULTHRESULT 的最高有效位(严重性字段)报告函数调用是成功还是失败。最后 16 位包含函数返回的代码。另外两位保留供将来使用,其余 13 位提供有关返回代码类型和来源的更多信息。下图说明了这一点。

    HRESULT type

    因此,使用以下实现,组件将能够告知其客户端某个特定接口是否不受组件支持。

    ///////////////////////////////////////////////////////////////////////////
    HRESULT __stdcall CComponent::QueryInterface(const IID& iid, void** ppv)
    {
        if (iid == IID_IUnknown)
        {
            Print("Returning pointer to IUnknown...") ;
            *ppv = static_cast<IComponent*>(this) ;
        } 
        else if (iid == IID_IComponent)
        {
            Print("Returning pointer to IComponent interface...") ;
            *ppv = static_cast<IComponent*>(this) ;
        }
        else
        {
            Print("Interface is not supported!.") ;
            *ppv = NULL ;
            return E_NOINTERFACE ;
        }
    
        //The reinterpret_cast operator allows any pointer to be converted into 
        //any other pointer type.
        reinterpret_cast<IUnknown*>(*ppv)->AddRef() ;
        // Incrementing the Reference count variable
        
        return S_OK ;
    }

    现在客户端可以通过调用组件的 Release() 方法来释放组件。

    //----------------//
    //   Client
    //----------------//
    void main()
    {
    IUnknown* pIUnknown=CreateInstance();
    IComponent* pIComponent=NULL;
    pIUnknown->QueryInterface(IID_IComponent,(void**)&pIComponent);
    pIComponent->Print("COM from scratch.");
    pIComponent->Release();// Releasing the Component
    }

下面的屏幕截图来自第一部分的示例。

The output screen shot

摘要

  1. 通过组件构建应用程序有很多优点。
  2. 应该使用一种通用标准来创建软件组件,使其与语言无关,而 COM 是创建软件组件的二进制标准。
  3. 就像硬件组件一样,一些功能被分配给软件组件。
  4. COM 组件的功能通过接口机制获得。
  5. 在 C++ 应用程序中,接口被定义为抽象基类。
  6. C++ 编译器为纯抽象基类生成的内存布局与 COM 对接口所需的内存布局相同。
  7. 由于纯抽象基类只有纯虚函数且不包含实例数据,因此 COM 组件仅通过其方法访问,而绝不能直接通过成员变量访问。
  8. 通过继承 IUnknown 接口,一个接口就变成了一个 COM 接口,该接口包含三个方法:
    • QueryInterface,用于在对象上的多个接口之间导航,并返回指向被查询接口的指针。
    • AddRef,用于控制对象的生命周期。
    • Release,用于控制对象的生命周期。
  9. 在 COM 中,每个接口指针也是一个 IUnknown 接口指针。
  10. COM 组件是通过使用 new 运算符创建的。
  11. COM 组件使用 HRESULT 类型向其客户端报告情况。
  12. 每个接口都由一个接口标识符标识,这是一个 128 位的值,类型为 GUID。
  13. C++ 类和 COM 类之间有一个重要的区别。在 C++ 中,类是一种类型,而 COM 类仅仅是对对象的定义,不携带类型信息,尽管 C++ 程序员可能会使用 C++ 类来实现它。

第二部分将在下一篇文章中介绍。

© . All rights reserved.