COM组件的接口和对象

  • Post author:
  • Post category:其他


一、 前言

在COM规范中,最基本的两个要素就是对象与接口,因为COM就是由这两者来共同实现的。COM对象在组件中是被封装起来的,客户代码只能通过接口来访问COM对象并享受其服务,由于客户与COM直接打交道的是COM接口,所以COM接口是COM最关键的要素。COM规范的核心内容就是对接口的定义,甚至可以说“在COM中接口就是一切”。组件与组件之间、组件与客户之间都要通过接口进行交互。接口成员函数将负责为客户或其他组件提供服务。与标识COM对象的CLSID类似,每一个COM接口也使用一个GUID来进行标识,该标识也被称为IID(interface identifier,接口标识符)。

二、 COM接口

COM接口通常是一组以函数的逻辑集合,继承IUnKnown接口。COM对象可以提供多个COM接口,每个接口提供不同的服务,因此COM接口与COM对象一样,都是用GUID来标识的,客户通过GUID来获取接口指针,再通过接口指针获取对应的服务。

COM接口实际限定了组件与使用该组件的客户程序或其他组件所能进行的交互方式,任何一个具备相同接口的组件都可对此组件进行相对于其他组件透明的替换。只要接口不发生变化,就可以在不影响整个由组件构成的系统的情况下自由的更换组件。通常在程序设计阶段需要将接口设计的尽可能完美,以减少在开发阶段对COM接口的更改。尽管如此,在实际应用中是很难做到这一点的,往往需要在现有接口基础上对其做进一步的发展。与C++中对类的继承有些类似,对COM接口的发展也可以通过接口继承来实现。但是COM接口的继承只能是单继承而不允许从多个基接口进行派生,而且派生接口只是继承了对基接口成员函数的说明而没有继承其实现。

对C++程序员来说,接口定义就是一组纯虚函数,按逻辑捆绑在一起,作为结构体的成员。同时,在C++中,结构体和类几乎是相同的,因此可以这样从一个接口派生另一个接口:

interface IMath : public IUnknown

{

virtual HRESULT __stdcall Add (int a, int b, int* pResult) = 0;

virtual HRESULT __stdcall Subtract (int a, int b, int* pResult) = 0;

}

关键字interface是结构体的别名。

对于接口,通常是采用抽象基类来定义,并利用类的多重继承来实现该组件。所谓的抽象基类是只包含一个或多个虚函数声明而未包括虚函数的具体实现的类。抽象基类不能被实例化,而只能用作基类使用,并要求其派生类完成其所有虚函数的实现。

由抽象基类指定的内存结构是符合COM规范的,因此抽象基类IMath可以认为是一个COM接口,但这还不是一个严格意义上的COM接口。对于一个真正意义上的COM接口,在设计时应遵循以下几个规则:

1) 接口必须直接或间接地从IUnknown继承。

接口IUnknown的定义:

可以看出IUnknown实质上就是一个含有纯虚函数的抽象类。

interface IUnknown{

virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) = 0;

virtual ULONG __stdcall AddRef() = 0;

virtual ULONG __stdcall Release() = 0;

};

2) 接口必须具有唯一的标识(IID)。

3) 一旦分配和公布了IID,有关接口定义的任何因素都不能被改变。

4) 接口成员函数应具有HRESULT类型的返回值。

5) 接口成员函数的字符串参数应采用Unicode类型。

IUnknown接口是COM的核心,因为所有其他的COM接口都必须从IUnknown继承。它包含三个接口函数:QueryInterface、AddRef和Release,其中QueryInterface用于接口查询,从COM对象的一个接口获得另一个接口,一个对象可能实现了多个接口,这样就可以通过QueryInterface在对象多个接口之间跳转从而获得多个接口提供的服务;AddRef与Release则用于管理COM对象的生命周期,当COM对象不再使用时需要释放,因此COM使用了引用计数的方法来对对象进行管理,当有一个用户获得接口指针后调用AddRef将引用计数加1,相反,当一个用户用完接口指针后就调用Release来使引用计数减1,这样当引用计数为0时,COM对象就可以从内存中释放。由于IUnknown提供了接口查询与生命周期控制两个功能,因此COM的每个接口都应该继承于它。

COM对象的接口原则

为了规范COM的接口机制,微软向COM开发者发布了COM对象的接口原则。

(1)IUnknown接口的等价性

当我们要等到两个接口指针,我如何判断它们从属于一个对象呢。COM接口原则规定,同一个对象的Queryinterface的IID_IUnknown查询出来的IUnknown指针值应当相等。也就是说,每个对象的IUnknown指是唯一的。我们可以通过判断IUnknown指针是否相等来判断它们是否指向同一个对象。

IUnknown *pUnknown1 = NULL, *pUnknown2 = NULL;

pObjectA->QueryInterface(IID_IUnknown,(void **) &pUnknown1);

pObjectB->QueryInterface(IID_IUnknown,(void **) &pUnknown2);

if (pUnknown1 == pUnknown2)

{

cout << “I am sure ObjectA is ObjectB.”;

}

else

{

cout << “I am sure ObjectA is not ObjectB.”;

}

(2)接口自反性,对一个接口来说,查询它本身应该是允许的。

设pPsychics是已赋值IPsychics的接口。

那么pPsychics->QueryInterface(IID_IPsychics,(void **) &XXX);应当成功。

(3)接口对称性,当我们从一个接口查询到另一个接口时,那么我们再从结果接口还可以查询到原来的接口。

例如:

IPsychics *pSrcPsychics = …something, *pTarget = NULL;

IDynamics *pDynamics = NULL;

如果pSrcPsychics->QueryInterface(IID_IDynamics,(void **) &pDynamics);成功的话。

那么pDynamics->QueryInterface(IID_IPsychics,(void **) &pTarget);也相当成功。

(4)接口传递性。如果我们从第一个接口查询到了第二个接口,又从第二个接口查询到了第三接口。则我们应该能够从第三个接口查询到第一个接口。其它依此类推。

(5)接口查询时间无关性。当我们在某时查询到一个接口,那么在任意时刻也应该查询到这个接口。

三、COM对象

COM对象其实就类似于C++中的对象,也就是说某个类的实例,包含了一组数据和操作。在COM模型中,COM对象的位置对于客户来说是透明的,即客户代码不需要直接初始化一个COM对象,而是COM库通过一个全局标识码GUID去对其进行初始化工作。GUID是一个128位的标识符,基本保证了COM对象的唯一性,另外COM接口也是用GUID来标识

在开发描述COM对象的C++类时,接口定义的是一组纯虚函数,开发COM类时可以充分利用这一点。例如,可以这样实现IMath的类:

class CComClass : public IMath

{

protected:

long m_lRef; // Reference count

public:

CComClass ();

virtual ~CComClass ();

// IUnknown methods

virtual HRESULT __stdcall QueryInterface (REFIID riid, void** ppv);

virtual ULONG __stdcall AddRef ();

virtual ULONG __stdcall Release ();

// IMath methods

virtual HRESULT __stdcall Add (int a, int b, int* pResult);

virtual HRESULT __stdcall Subtract (int a, int b, int* pResult);

};

现在,假设要用CComClass实现两个COM接口。可以使用多重继承,从IMath和另一个接口派生CComClass。例如:

class CComClass : public IMath, public ISpelling

{

protected:

long m_lRef;

public:

CComClass ();

virtual ~CComClass ();

// IUnknown methods

virtual HRESULT __stdcall QueryInterface (REFIID riid, void** ppv);

virtual ULONG __stdcall AddRef ();

virtual ULONG __stdcall Release ();

// IMath methods

virtual HRESULT __stdcall Add (int a, int b, int* pResult);

virtual HRESULT __stdcall Subtract (int a, int b, int* pResult);

// ISpelling methods

virtual HRESULT __stdcall CheckSpelling (wchar_t* pString);

};

这种方法有两个优点。首先,它很简单,为了声明一个包含n个接口的类,可以在该类的基类列表中包含n个接口。其次,只需要实现IUnknown一次。如果单独实现每一个接口,就必须为每一个接口实现QueryInterface、AddRef和Release。但是,使用多重继承,所有接口都支持的方法合并到同一个实现。

使用多重继承编写COM类,更有意思的一点发生在客户调用QueryInterface获取接口指针时。假设用户需要获取IMath指针:


ppv = (IMath

) this;

如果用户要获取ISpelling指针,则要强制转换为ISpelling*类型:


ppv = (ISpelling

) this;

如果忽略强制转换,虽然可以正常编译,但是使用时就会发生问题。因为,通过多重继承产生的类会包含多个虚表和多个虚表指针,如果不进行强制类型转换,就无法知道this指针引用哪个虚表。换句话说,上面两个语句虽然使用相同的this指针,但是返回不同的值。如果客户需要一个ISpelling指针,却返回一个纯this指针,而this刚好指向IMath的虚表,客户就会使用IMath虚表调用ISpelling方法。因此,使用多重继承的COM类要进行强制转换,获取正确的虚表指针。

4.嵌套类

如果两个接口的方法没有使用相同的名字和标记,使用多重继承就不会有什么问题。如果IMath和ISpelling都包含Init方法,它们的参数列表相同,但是需要单独实现,这时,就不能使用多重继承定义一个类实现它们。因为,使用多重继承时,成员函数的名字必须是唯一的。

由于这个限制,MFC使用嵌套类实现COM接口。可以在一个C++类中实现COM接口的任意组合,与接口的特性无关。

假设CComClass实现IMath和ISpelling,并且它们都包含一个名为Init的方法,不含参数:

virtual HRESULT __stdcall Init () = 0;

这时不能用多重继承,因为C++不支持在一个类中有两个相同的函数。可以定义两个子类,每个实现一个接口:

class CMath : public IMath

{

protected:

CComClass* m_pParent; // Back pointer to parent

public:

CMath ();

virtual ~CMath ();

// IUnknown methods

virtual HRESULT __stdcall QueryInterface (REFIID riid, void** ppv);

virtual ULONG __stdcall AddRef ();

virtual ULONG __stdcall Release ();

// IMath methods

virtual HRESULT __stdcall Add (int a, int b, int* pResult);

virtual HRESULT __stdcall Subtract (int a, int b, int* pResult);

virtual HRESULT __stdcall Init () = 0;

};

class CSpelling : public ISpelling

{

protected:

CComClass* m_pParent; // Back pointer to parent

public:

CSpelling ();

virtual ~CSpelling ();

// IUnknown methods

virtual HRESULT __stdcall QueryInterface (REFIID riid, void** ppv);

virtual ULONG __stdcall AddRef ();

virtual ULONG __stdcall Release ();

// ISpelling methods

virtual HRESULT __stdcall CheckSpelling (wchar_t* pString);

virtual HRESULT __stdcall Init () =0;

};

为了创建CMath和CSpelling嵌套类,先在CComClass内部声明它们。然后,在CComClass内包含一对CMath和CSpelling对象:

class CComClass : public IUnknown

{

protected:

long m_lRef; // Reference count

class CMath : public IMath

{

[…]

};

CMath m_objMath;

class CSpelling : public ISpelling

{

[…]

};

CSpelling m_objSpell;

public:

CComClass ();

virtual ~CComClass ();

// IUnknown methods

virtual HRESULT __stdcall QueryInterface (REFIID riid, void** ppv);

virtual ULONG __stdcall AddRef ();

virtual ULONG __stdcall Release ();

};

CComClass只派生于IUnknown。如果用户调用QueryInterface获取IMath指针,CComClass就传递一个指向CMath对象的指针:


ppv = (IMath

) &m_objMath;

如果要获取ISpelling指针,CComClass就返回一个指向m_objSpell的指针:


ppv = (ISpelling

) &m_objSpell;

理解嵌套类方法的关键是,子对象必须把对它们IUnknown方法的调用委派给父类中等效的方法。注意到,每一个嵌套类中没有引用计数,取而代之的是CComClass指针。该指针是一个反向指针,指向父对象。通过该反向指针调用CComClass中的IUnknown实现委派操作。通常,父类的构造函数这样初始化反向指针:

CComClass::CComClass ()

{

[…] // Normal initialization stuff goes here.

m_objMath.m_nParent = this;

m_objSpell.m_pParent = this;

}

嵌套类的IUnknown的实现如下:

HRESULT __stdcall CComClass::CMath::QueryInterface (REFIID riid, void** ppv)

{

return m_pParent->QueryInterface (riid, ppv);

}

ULONG __stdcall CComClass::CMath::AddRef ()

{

return m_pParent->AddRef ();

}

ULONG __stdcall CComClass::CMath::Release ()

{

return m_pParent->Release ();

}

这种类型的委派是必要的。如果客户在一个子对象实现的接口中调用AddRef或者Release,会修改父对象的引用计数,而不是子对象的引用计数。其次,如果客户调用子对象中的QueryInterface,父对象就必须接管调用,因为只有父对象知道存在哪个嵌套类,以及该嵌套类实现了哪个接口。



版权声明:本文为u010523811原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。