- COM是构造二进制兼容软件组件的规范。它不是编程语言、代码库或者编译器,而是个二进制规范
- COM的好处
- com组件易替换
- com组件适合于改变业务需求
- com组件使复用性成为可能
使用com,将很容易对某些代码实现一次编写和多处使用。而且你可以对这个组简进行任何纠正和改进,而不必改变使用这个组件的应用程序。- com组件有助于并行开发
一旦设计接口之后,就可以将其分布到几个程序中,组件的实现可以并列进行。
- COM的局限性
- 版本问题
每个COM组件都有GUID,是操作系统标识这个组件的唯一ID.这些GUID存放在Windows Registry(注册表)中。这样每次改变组件接口时,都赋予新的GUID。这个机制可以作为版本号,表示组件改变了接口。使用该组件的软件要注意。它能保证,一旦开始使用一个组件,就总是使用这个组件的同一版本。如果要使用新的版本,则要取得新的GUID。
版本问题是无法避免的。因此,在开发期间,你需要特别注意所用的组件的版本。每次更新都会导致新的版本。在开发期间每次生成新构件时,要有检查组件版本的习惯。- 旧接口应当停用
COM要求的是接口一旦建立,就无法抛弃它。这就保证一旦程序利用特定版本的组件,就总是支持这个版本的功能。
作为开发人员,我们需要保证旧版本组件接口保持不动。可以在组件中增加功能或更新现有功能。但是不能删除现有功能。这个特点造成的唯一真正问题是组件不断膨胀的演变。到一定时候(如果组件变得太大或旧功能太多),可能要把所要功能移植到新组件中。- com接口要认真规划
接口是COM组件的集成部分。接口是组件之间相互访问的通信机制。每次组件或其接口改变时,就有一个新版本与组件相关联。认真规划可以防止开发期间产生太多接口版本。
- 接口
- 接口定义了其它软件和组件能利用的公用功能。COM接口使应用程序和其它组件可以和COM组件的功能进行通信。组件功能通过虚拟函数表(virtual function table)访问,也称vtable或VTBL。vtable不包含实际函数,只是包含组件函数的一组指针。组件要访问其它组件的功能时,要通过这个vtable。
- 客户机不能直接访问vtable,另一指针叫接口指针,增加了与接口的另一层间接。使得这个接口得以实现。即客户机见到了vtable表中指针的指针。
- COM接口的vtable唯一要求是表的第一个字段应为IUnknow的指针。IUnknow是任何组件变为COM组件必须实现的唯一接口。其它所有接口都要从IUnknow接口继承而来。
- 一个COM组件可以包含任意多个接口的实现方法。
- 组件要靠接口通信。接口定义并发布后,就不能改变。COM的规则之一是保证软件接口在发布后不变,如果改变了接口,则要作为另一新接口发布,这样可以保证前面的接口不变。由于这种不变性,所以在接口生成之前一定要慎重规划。
3.1. 接口特征
- 接口不是类
- 接口不是对象
- 接口有唯一性
每个接口有一个唯一标识符GUID。来保证不会与其它接口发生冲突。- 接口是不可变的。
接口没有版本,因此避免了版本问题。接口的增加、删除功能或者改变语法之后的新版本成为全新的接口,要指定新的接口标识符。
3.2 接口类型
- IUnknow接口:它是COM组件必须实现的唯一接口。
- IDispatch接口:如果要从脚本语言访问组件,则还要实现IDispatch接口。
- 自定义接口:表示系统尚未支持的接口。需要提供调用代码(如果没有提供)。
- 双接口:Automation对象(自动化对象)实现IDispatch和vtable接口时,就称为双接口(dual-interface)组件。
- IUnknow接口
IUnknow接口是最重要的接口。
每个COM组件都必须实现这个接口。
因为要用这个接口来管理有对象支持的所有其它接口。
IUnknow接口包含三个方法:
- QueryInterface
用于寻找对象提供的其它接口。- AddRef
要使用这些接口时,调用AddRef- Release
完成某个接口使用后,调用Release
内存管理:接口指针的寿命管理总是通过每个COM接口中的AddRef和Release方法完成的。
- IDispatch接口
IDispatch接口是从IUnknow接口派生而来。这个接口主要用于脚本语言。而过开发的应用程序不要从脚本语言访问,最好不要使用IDispatch(避免增加开销)
IDispatch接口包括的函数允许访问COM对象的方法和属性。IDispatch接口使VB和其他脚本语言可以操作对象的方法和属性。
IDispatch接口有连个重要的方法:
- GetIDsOfName
- Invoke
双接口
Automation对象(自动化对象)实现IDispatch和vtable接口时,就称为双接口(dual-interface)组件。
在双接口中,vtable中的前三个项目是IUnknow的成员,后四个是IDispatch的成员,再后面是双接口成员的地址。
所有VB组件都支持双接口,因此不必另外实现提供双接口的类类型库
类型库是描述一个或几个COM对象类型的文件或部分文件。类型库特别有用,可以在编译时访问。在VB中,类型库组合在同一个二进制文件中。接口一般规则
- 设计远程使用
COM提供灵活性。可以设计成无法作为"远程"的。
一般来说,所有COM接口都应设计成支持分布式处理- 从IUnknow派生
所有COM接口必须直接或间接从IUnkonw接口派生。也就是说。任何在COM对象上实现的接口必须以QueryInterface、AddRef和Release为前的三个方法。- 生成唯一标识符
接口的真名是128位GUID。因此,对于每个新定义接口,要产生新的接口标识符(interface identifier)或IID- 接口是不可变的
接口发布之后,与某个IID之间的合同是不能改变的。前面介绍过,这个规则保证软件能肯定接口在发布之后保持相同。- 函数应返回HRESULT
接口的所有方法返回类型为HRESULT,但是IUnknow::AddRef 和IUnknow::Release是例外。
HRESULT就是一个状态码
当不准备远程使用时,可以忽略这条规则- 字符串参数应当为Unicode
所有COM接口传递的所有字符串都是Unicode字符串
- 接口设计
COM接口中应包括下列元素:
- 接口标识符Interface identifier
每个接口要有一个GUID,作为编程名称,这个接口ID(IID)是接口的唯一标识符。带有特定IID的接口设计编译为二进制形式并发布后,所有构成接口其它元素的规定均不能改变。- 接口签名Interface signature
接口签名定义下列内容:1. 接口中的方法个数和顺序 2. 每个方法中每个参数的数值、顺序和类型 3. 每个方法的返回值类型- 接口词法Interface semantics
接口词法是合同的一部分,是对签名描述的补充。
使用MIDL定义自定义接口
MIDL:接口定义语言Microsoft interface Definition Language,它是定义COM接口的说明性语言。
1. COM服务器的三个关键要求
- 接口(Interface)
接口是服务器与客户机之间的协议,客户机通过接口与服务器通信。- 组件类(CoClass)
组件类(Component Class)提供所定义接口的实现方法。- 组类型库(Type Library)
是编译的IDL文件,向支持COM的环境传送接口的信息。
2. IDL文件
2.1 下面创建CustomComWapper.idl文件
//import语句,将引入另一个idl文件中包含的额定义并在本文中使用
import "oaidl.idl" //输入文件oaidl.idl中的接口定义,包括IDispatch
import "ocidl.idl"
//1. 定义类型库
[//方括号中列出 类型库属性清单
uuid(DAD1DA4G-0C17-455C-B8FE-314B56DD10CCD), //类型库唯一标识符 LIBID
version(1.0),//版本
helpstring("CustomComTypeLibraryLib 1.0 Type Library")
]
//类型库属性清单后紧跟着类型库定义
library CustomComTypeLibraryLib
{
//importlib将引入二进制(编译类型库)
//所有类型库都要用importlib输入Stdole3d.tlb中定义的基础类型库。用importlib指令输入类型库时,需要保证向客户机提供这个库(要在Com服务器上安装这个库)
importlib("stdole32.tlb");
importlib("stdole2.tlb");
//--- if the follwing import fails then it means that
//--- the type library is not on the system path ,
//---you can fix the problem in two ways:
//---1.Add it to system wide PATH environment variable
//---2. Add it to the executable file list in Developer Studio.
importlib("axdb18enu.tlb");
//--interface definition
//2. 在类型库中 定义接口ICustomInterfaceOne
//当然你也可以定义多个接口
[ //方括号中为属性清单
object, //object属性,告诉编译器这是个Com接口定义而不是RPC(远程过程调用)接口定义。
uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx),//接口的唯一标识符IID
dual,//dual属性表示客户机有两个不同的方法可以访问这个接口提供的属性和方法,即双接口
nonextensible,
helpstring("ICustomInterfaceOne interface"),//帮助字符串
pointer_default(unique)//该属性指定除参数表中所列特征外所有指针的缺省特征;unique表示指针可以说Null,但不支持别名
]
//属性清单后面是接口定义本身。这里定于ICustomInterfaceOne接口,是从IDispach接口继承来的
//该接口中定义了两个属性 和一个方法
interface ICustomInterfaceOne:IDispach
{
[propget,id(NameId),helpstring("属性 姓名")] HRESULT Name([out,retval] BSTR* pVal);
[propget,id(NameId),helpstring("属性 姓名")] HRESULT Name([in] BSTR newVal);
[propget,id(AgeId),helpstring("属性 年龄")] HRESULT Age([out,retval] DOUBLE* pVal);
[propget,id(AgeId),helpstring("属性 年龄")] HRESULT Age([in] DOUBLE newVal);
HRESULT DoSomething();//方法
}
//3. 定义组件类 实现ICustomInterfaceOne
[
uuid(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx),//组件类的唯一标识符 CLSID
helpstring("ICustomInterfaceOne interface"),//帮助字符串
]
coclass ComCustomCoClassOne
{
[default] interface ICustomInterfaceOne;//组件了指向签名定义的接口ICustomInterfaceOne
[source]interface iAcadObjectEvents;
}
}
2.2 编译IDL文件
使用MIDL.EXE编译后将生成如下文件
- 接口代理文件 CustomComWapper__p.c
包含IDL中所定义接口的调用代码- 头文件 CustomComWapper.h
包含C++接口和类型定义,并说明接口ID(IID_ICustomInterfaceOne) 和 CLSID(CLSID_ComCustomCoClassOne)的符号化常量。- 接口UUID文件 CustomComWapper_i.c
GUID定义文件 :包含头文件所说明的IID、CLSID和LIBID定义- 类型库文件 CustomComWapper.tlb
表示IDL文件的二进制版本。
2.3 实现IUnknow和自定义接口
添加自定义类CCustomClassOne实现接口ICustomInterfaceOne
#include <windows.h> //包含windows.h文件,其中包含后面要用的一些API声明
#include "CustomComWapper.h" //编译idl生成的文件
class CCustomClassOne :public ICustomInterfaceOne
{
...
public :
//STDMETHOD宏和STDMETHOD_宏描述函数的返回类型和调用规则
//STDMETHOD宏假设函数的返回类型为HRESULT
//STDMETHOD_宏允许在第一个参数中指定函数的返回类型
//QueryInterface方法接茬请求的接口,看看这个组件是否支持,它比较riid与各种支持的接口ID(因为程序员是知道要支持那些接口的) ,然后返回相应的指针,将this调整为相应的基础类
//在返回请求接口指针之前,我们要对齐采用AddRef方法,这个规则保证组件不会再客户机用完接口之前突然死亡
STDMETHOD (QueryInterface)(REFIID riid,void ** ppv)
{
if(riid == IID_IUnknow)
*ppv = static_cast<IUnknow*>(this);
else if(riid == IID_IDispatch)
*ppv = static_cast<IDispatch*>(this);
else if(riid == IID_ICustomInterfaceOne)
*ppv = static_cast<ICustomInterfaceOne*>(this);
//当然如果还支持其它接口在这里添加else if就好了
else
*ppv = NULL;
if(*ppv != NULL)
{
static_cast<IUnknow*>(*ppv)->AddRef();
return S_OK;
}
else
return E_NOINTERFACE;
}
//STDMETHOD_宏允许在第一个参数中指定函数的返回类型
//AddRef和Release功能相类似,都是用Win32API专门控制变量m_ulRefCount的增减。
STDMETHOD_ (ULONG ,AddRef)()
{
InterlockedIncrement(reinterpret_cast<LPLONG>(&m_ulRefCount));
return m_ulRefCount;
}
//Release方法,除了调用win32API中专门的方法类减少引用计数外,
//还应该在引用数为0时删除对象实例
STDMETHOD_ (ULONG ,Release)()
{
if(!InterlockedDecrement(reinterpret_cast<LPLONG>(&m_ulRefCount)))
{
delete this;
return 0;
}
return m_ulRefCount;
}
//下面是实现接口的方法和属性 属性省略
STDMETHOD (DoSomething)()
{
}
//下面实现属性
...
private:
ULING m_ulRefCount;
}