COM知识
组件的简介
定义:
组件是具有特定功能的,能够跨进程,跨网络,跨编程预研,跨操作系统的即插即用的独立对象,是可以在二进制级别上进行继承和重用而且可以被独立生产获得和配置的软件单元。
优点:
- 组件易替换,当要在庞大复杂的应用程序中进行版本升级时,可以只修改或替换相关组件,而不影响其他众多的程序组件,同理更能适应业务的需求更改。
- 可实现二进制代码重用,很多时候的重用都是仅限程序源代码级别的重用,这就要求使用的编程语言和重用的代码是一致的。而组件之间可以在二进制上进行集成和重用,因此只需一次编码就能多处应用。
- 有助于并行开发,只要组件设计的接口正确,所有组件都可以同时并行开发。
COM简介
组件之间的接口是双方进行通信的基础,也是组件的关键部分。COM(Component Object Model,组件对象模型)就是Microsoft力推的组件标准。它可以是一个DLL也可以是一个EXE,一个COM组件至少有一个COM对象,每个COM对象可以实现多个接口,COM对象被封装起来,客户无法访问对象的内部实现细节,而访问COM对象的唯一途径是使用COM接口。
COM接口有两个含义:1、是一组可以调用的函数 2、是组件及其客户之间的协议。其中最核心的接口是IUnknown,每个COM组件都必须实现该接口,并且其他所有接口都必须从IUnknown接口派生。
当客户程序调用组件功能时,先创建一个COM对象,然后通过COM接口调用它所提供的服务,待服务结束后,如果客户不再需要该COM对象,应释放掉对象所占用的资源,包括对象本身。
重用C++对象的方式
- 源代码级别的重用,缺陷是所有技术都随着源代码的提供全部暴露
- 将代码打包成DLL,要使用DLL首先要注意以下几点
- 用__declspec(dllexport)将成员函数引出,它可以告诉编译器将函数入口放进引出函数表中。
- DLL和EXE分别维护自己的内存链表,要让他们自己负责分配和释放属于自己的内存。
- 做Unicode/ASCII兼容,最好是将所有函数参数标准化为Unicode,因为Unicode是ASCII的超集,假如客户程序是以ASCII形式编译,则可以将ASCII转换成Unicode。
该方法有两个缺陷,一是该方法需要客户程序包含其头文件,源文件中的实现是隐藏了,假如头文件中的类定义有私有成员,依然会有私有成员被暴露的问题;二是客户程序在使用对象前需要为其分配内存空间,假如DLL有更新改变了数据成员的大小,所有客户程序都需要重新编译。解决方案是在客户程序包含其头文件中定义抽象基类,让对外暴露的接口全部定义成纯虚函数,具体的派生类及其实现隐藏在DLL中,而每次客户程序使用对象前都只是分配一个虚函数表指针的大小。
DllGetClassObject
如果在DLL中实现多个对象,可以有两种选择:
1、为每个引出的类提供一个入口点
2、通过给一个标准的入口传递一个参数,表明要使用的类
COM就是通过第二个方法实现是个对象,而标准入口就是DllGetClassObject,其定义如下:
C++ STDAPI DllGetClassObject(REFCLSID relsid, REFIID riid, void** ppObject) { ...... } |
STDAPI 是一个宏定义
C++ #define STDAPI EXTERN_C HRESULT STDAPICALLTYPE |
EXTERN_C 指示编译器这部分代码按C语言的进行编译,目的是为了实现C++与C及其它语言的混合编程。
HRESULT 返回值
STDAPICALLTYPE __stdcall的宏定义,函数调用约定参数从右向左压入堆栈
第一个参数是CLSID(CLASS ID)类型,REFCLSID是CLSID的一个引用
C++ #define REFCLSID const IID & |
代表调用者要访问的对象,CLSID是一种GUID(global unique identifier),在COM中对象和接口都用GUID标识,分别为CLSID和IID。
第二个参数代表接口的ID,也是CLSID的一个引用,根据riid参数,ppObject返回相应的对象指针。
第三个参数用于存放返回的对象指针。
引用计数
为了防止多个客户调用同一个对象时,其中一个客户释放了对象,则其他客户就可能出现操作一个已不存在对象的问题,而通过引用计数就可以解决这个问题。
每当客户得到一个指向该对象的接口指针时,通过AddRef将引用计数加1,当客户调用Release就将引用计数减1,只有引用计数为0时才释放对象
自动化对象的核心--IDispatch
只要一个对象实现了IDispatch接口,那么它就是一个自动化对象。
C++ IDispatch : public IUnknown { public: virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount( /* [out] */ __RPC__out UINT *pctinfo) = 0;
virtual HRESULT STDMETHODCALLTYPE GetTypeInfo( /* [in] */ UINT iTInfo, /* [in] */ LCID lcid, /* [out] */ __RPC__deref_out_opt ITypeInfo **ppTInfo) = 0;
virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames( /* [in] */ __RPC__in REFIID riid, /* [size_is][in] */ __RPC__in_ecount_full(cNames) LPOLESTR *rgszNames, /* [range][in] */ UINT cNames, /* [in] */ LCID lcid, /* [size_is][out] */ __RPC__out_ecount_full(cNames) DISPID *rgDispId) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE Invoke( /* [in] */ DISPID dispIdMember, /* [in] */ REFIID riid, /* [in] */ LCID lcid, /* [in] */ WORD wFlags, /* [out][in] */ DISPPARAMS *pDispParams, /* [out] */ VARIANT *pVarResult, /* [out] */ EXCEPINFO *pExcepInfo, /* [out] */ UINT *puArgErr) = 0;
}; |
GetTypeInfoCount用于判断对象是否提供类型信息,若提供则出参pctinfo返回1,否则为0
GetTypeInfo返回对象的类型信息,iTInfo指定待获取的类型信息,0标识获取该接口实现的类型信息;lcid指定类型信息的本地化表示;ppTInfo指向一个类型信息对象的ITypeInfo接口指针
GetIDsOfNames用于返回方法或属性成员及其参数名字的DISPID,riid为保留参数,必须为IID_NULL;rgszNames指定成员的名字,是一个字符串数组,第一个字符串为方法或属性成员的名字,后续的字符串为成员的参数名字;cNames指定数组中名字的个数;lcid指定本地化标识;rgDispId是一个客户程序分配的数组,每个数组成员包含rgszNames名字所对应的DISPID