其实从上一章“COM是个更好的C++”可以看出,COM最重要的就是将接口与实现分离。
上一章中接口定义头文件中采用C++抽象类的形式,如果调用方是C++环境当然不会有问题。但如果调用方不是C++的编译环境呢?
为了把“接口定义”与“特定实现过程所用到的语言”之间的关联尽可能的断开,我们必须把这两项分离开来,如果所有参与的各方统一使用一种语言(而非C++抽象基类)来定义接口,那么就有可能只定义一次接口,然后在必要的时候导出新的、与实现语言相关的接口定义来。COM提供了这样一种语言,只用到基本的、大家熟悉的C语法,同时加入某些用来“消除C语言二义性特征”的能力。这种语言被称为接口定义语言(Interface Definition Language, 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);
几乎所有的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,然后相应的处理不正常的结果。
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。
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接口不能从多个接口中继承,只能从一个接口继承。 简单的说不支持多重继承 **