2.COM接口

2.1 再谈接口与实现

其实从上一章“COM是个更好的C++”可以看出,COM最重要的就是将接口与实现分离。

上一章中接口定义头文件中采用C++抽象类的形式,如果调用方是C++环境当然不会有问题。但如果调用方不是C++的编译环境呢?

为了把“接口定义”与“特定实现过程所用到的语言”之间的关联尽可能的断开,我们必须把这两项分离开来,如果所有参与的各方统一使用一种语言(而非C++抽象基类)来定义接口,那么就有可能只定义一次接口,然后在必要的时候导出新的、与实现语言相关的接口定义来。COM提供了这样一种语言,只用到基本的、大家熟悉的C语法,同时加入某些用来“消除C语言二义性特征”的能力。这种语言被称为接口定义语言(Interface Definition Language, IDL)

2.2 IDL

IDL语法就是简单的C语法。IDL支持struct,union,enum,typedef。

IDL属性位于方括号中,多个属性之间以逗号作为分割符,即所谓的属性列表,以区别于基本的IDL文本流。属性总是出现在相应主题的定义之前。

[
	v1_enum,helpstring("This is a color!")
]
enum COLOR { RED, GREEN, BLUE}

当我们在IDL中定义COM方法时,我们必须要显式地指明调用方或被调用方将写入或读每一个参数。

void Method1([in] long arg1,
			 [out] long *parg2,
			 [in,out] long *parg3);

2.3 方法和结果

几乎所有的COM方法都会返回一个HRESULT类型的错误号。对于许多COM兼容的实现语言(如VB,JAVA),这些HRESULT被运行时库或虚拟机截取,然后被映射成为语言中特定的异常(exception)

HRESULT是32位的整数。包含比如网络错误、服务器失败等信息

//31位:严重程度位
//30-29位:保留
//28-16位:操作码
//15-0位:信息码
#define SUCCEEDED(hr)	(long(hr) >= 0)
#define FAILED(hr)		(long(ht) < 0)
//这两个宏充分利用了“当把HRESULT当作有符号的整数时,严重程序位也就是符号位”这一事实

S_OK	一般操作,成功执行
S_FALSE	成功地返回逻辑错误
E_FAIL	一般性失败
E_NOTIMPL	方法没有实现
E_UNEXPECTED 在不准确的时间调用了方法

为了让方法返回一个与“方法的物理HRESULT”不相关的逻辑结果,COM IDL支持retval参数属性,表示: * 相关的物理方法参数 实现上是 操作的逻辑结果 * ,在支持retval的环境中,该参数应该被映射为操作的结果。

HRESULT Method2([in] short arg1,
				[out,retval] short* arg2);

在Java语言中,被映射为下面的函数:

public short Method2(short arg1);

在Visual Basic中,方法定义下如:

Function Method2(arg1 as Integer) As Integer

Microsoft C++把这个方法映射为:

virtual HRESULT _stdcall Method2(short arg1, short* arg2) = 0;

这样C++的调用代码如下:

short sum = 10;
short s;
HRESULT hr = pItf->Method2(20, &s);
if(FAILED(hr))
	throw hr;
sum += s;

等同于下面的JAVA代码

short sum = 10;
short s = itf.Method2(20);
sum += s;

如果该方法返回的HRESULT表示一个不正常的结果,那么Java虚拟机将把HRESULT映射为Java的异常。而C++代码片断必须要手工检查方法返回的HRESULT,然后相应的处理不正常的结果。

2.4 接口和IDL

IDL的interface关键字用来作为接口定义的开始。接口定义有四个部分:接口名字、基接口名字、接口体、接口属性。

[attribute1, attribute2, ...]
interface IThisInterface: IBaseInterface {
	typedef1;
	typedef2;
	..
	method1;
	method2;
	..
}

COM接口需要一个不同于 * 逻辑名字 * 的 * 实质名字* 。why? 请考虑下面的场景:两个开发人员都选择了同一个逻辑名字ICalculator,如果客户只是简单地使用字符串“ICalculator”来询问对象是否支持这个接口,就有可能获得的不是客户想要的对象接口,尽管接口共享了同一个逻辑名字,但它们是完全不同的接口。

为了消除这样的冲突,所有的COM接口在设计时都被分配了一个二进制的名字,这就是接口的 * 实质名字 * ,这些实质名字被称为全局唯一标识符(Globally Unique Identifier, GUID)

GUID是个128位的大数,可保证在时间和空间两个方面都是唯一,GUID也常被称为接口ID(IID),也被称为类ID(CLSID)

BDA4A270-A1BA-11D0-8C2C-0080C73925BA

为了创建一个新的GUID,COM暴露了一个API函数,可以用来产生新的128位值。该函数使用了非集中式的唯一性算法,所以理论上可以保证结果值不会重复出现。函数原型如下:

HRESULT CoCreateGuid(GUID *pguid);

每个COM接口都必须要有两个IDL属性,[object]属性是必需的,它说明该接口定义是一个COM接口,而不是DCE风格的接口

为了把接口的 实质名字 和它在IDL中的定义联系起来,我们要使用另一个 [uuid] 接口属性,接口一个参数,即GUID的标准文本形式。示例如下:

[object, uuid(BDA4A270-A1BA-11D0-8C2C-0080C73925BA)]
interface ICalculator : IBaseInterface {
	HRESULT Clear(void);
	HRESULT Add([in] long n);
	HRESULT Sum([out, retval] long *pn);
}

接口ICalculator也有一个IID,通过IDL产生的常量IID_ICalculator,我们可以在程序中访问这个IID。

因为很少有编译器支持128位整数,所以COM定义了一个C结构,用来表示GUID

typedef struct _GUID{
	DWORD Data1;
	WORD Data2;
	WORD Data3;
	BYTE Data4[8];
} GUID
typedef GUID IID;
typedef GUID CLSID;

为了允许对GUID值进行比较,COM提供了等价性的测试函数,重载了“==”操作符和“!=”操作符。

既然接口运行时的名字是GUID, 而不是字符串,这就意味着前一章讲的Dynamic_Cast方法需要重新修订。实际上,整个IExtensibleObject接口都需要重新考察,终终要换到COM中的等价接口:IUnknown。

1.5 IUnknown

IUnknow与上一章定义的IExtensibleObject接口用于同样的目的。

class IExtensibleObject {
public:
	virtual void *Dynamic_Cast(const char* pszType) = 0;
	virtual void DuplicatePointer(void) = 0;
	virtual void DestroyPointer(void) = 0;
};

下面是IUnknown的C++定义

extern "C" const IId IID_IUnknow;
interface IUnknow {
	virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) = 0;
	virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
	virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
}

IUnknown的IDL定义可以从SDK的include目录下的unknwn.idl文件中找到:

[
  local,
  object,
  uuid(00000000-0000-0000-C000-000000000046),
  pointer_default(unique)
]
interface IUnknown
{
  typedef [unique] IUnknown *LPUNKNOWN;

  HRESULT QueryInterface(
    [in] REFIID riid,
    [out, iid_is(riid), annotation("__RPC__deref_out")] void **ppvObject);
  ULONG AddRef();
  ULONG Release();
}
//[local]属性禁止为该接口产生网络代码。这个属性可以放宽COM的要求,否则,所有的远程方法必须返回HRESULT。

再看看我们实现的calculator.idl

[object, uuid(BDA4A270-A1BA-11D0-8C2C-0080C73925BA)]
interface ICalculator : IUnknown {
	import "unknwn.idl";	//引入IUnknown的定义,相当于C++中的 #include "unknown.h"
	HRESULT Clear(void);
	HRESULT Add([in] long n);
	HRESULT Sum([out, retval] long *pn);
}

** COM限制了接口的继承性:COM接口不能从多个接口中继承,只能从一个接口继承。 简单的说不支持多重继承 **

2.6 资源管理和IUnknown

2.7 类型强制转换和IUnknown

2.8 实现IUnknown

2.9 使用COM接口指针

2.10 优化QueryInterface

2.11 数据类型

2.12 IDL属性和COM属性

2.13 异常

2.14 我们走到哪儿了?

你可能感兴趣的:(C++)