1.38k likes | 1.57k Views
Component Object Model COM 组件对象模型. 概览. 组件和接口 QueryInterface 函数 引用计数 动态链接 类厂 组件复用:包容与聚合 服务器. 应用程序. 一般由单个二进制文件组成 编译后至重新编译和发行生成新下一版本前,一般不会变化 操作系统、硬件和客户需求的改变必须等到整个应用程序被重新编译后才能认可 解决方案 将单个的应用程序分隔成多个独立部分-组件. 显而易见的优点. 随着技术的不断发展用新的组件取代已有的组件 程序随着新组件不断取代旧的组件而趋于完善 用已有的组件建立全新的应用. 组件应用程序.
E N D
概览 • 组件和接口 • QueryInterface函数 • 引用计数 • 动态链接 • 类厂 • 组件复用:包容与聚合 • 服务器
应用程序 • 一般由单个二进制文件组成 • 编译后至重新编译和发行生成新下一版本前,一般不会变化 • 操作系统、硬件和客户需求的改变必须等到整个应用程序被重新编译后才能认可 • 解决方案 • 将单个的应用程序分隔成多个独立部分-组件
显而易见的优点 • 随着技术的不断发展用新的组件取代已有的组件 • 程序随着新组件不断取代旧的组件而趋于完善 • 用已有的组件建立全新的应用 组件应用程序 改进后的组件应用程序 组件A 组件B 组件A 组件B 组件C 组件C 新改进后的组件D 组件D 组件E 组件E
比较 • 传统的做法 • 应用程序分割成文件、模块或类 • 编译并链接成一个铁板一块般的应用程序 • 组件建立的应用程序 • 微型应用程序:每个部分都已经编译、链接好可以使用的 • 可以在运行时同其它组件连接。 • 需要修改和改进时,只需将某个构成组件用新的版本替换
COM • Component Object Model • 组件对象模型,是关于如何建立组件以及如何通过组件构建应用程序的一个规范。 • 开发COM的目标是为了使应用程序更易于定制、更为灵活。 • OLE1 DDE • OLE2 COM
使用组件的优点 • 应用程序定制 • 组件库 • 分布式组件
对组件的需求 • 组件的优点直接来源于可以动态地将它们插入或卸出。 • 所有组件必须满足两个条件: • 组件必须动态连接 • 目标是为在应用程序运行的过程中替换组件 • 组件必须封装其内部实现细节 • 保持同样的连接方式 • 客户通过接口同其它组件进行连接
封装对组件的限制 • 组件必须将其实现所用的编程语言封装起来 • 组件必须以二进制的形式发布 • 组件必须可以在不妨碍已有用户的情况下被升级 • 组件在网络上的位置可以被透明地重新分布
限制的考虑 • 语言的无关性 • 内部:Object C,C++,EspressoBeans • 市场:VB only,对手任何语言编写组件 • 版本 • 向后兼容的能力 • 改变某个组件的功能以使之适应新应用程序的需要、并同时支持老的应用程序
COM组件 • COM规范就是一套为组件架构设置标准的文档 • COM组件是… • COM组件以动态链接库或可执行文件的形式发布的可执行代码组成。 • COM组件满足下面的限制条件 • COM组件是完全与语言无关的 • COM组件可以以二进制的形式发布 • COM组件在不妨碍已有用户的情况下被升级 • COM组件在网络上的位置可以被透明地重新分布
COM组件 • COM组件不是… • COM不是计算机语言 • COM与DLL相比或相提并论不合适 • COM不是一个API函数集合 • COM库 • COM具有一个被称为COM库的API • 提供组件管理服务 • 保证对所有组件大多数重要的操作都可以按照相同的方式完成 • 节省开发和实现时间,支持分布式或网络化
COM综述 • 提供了一个所有组件都应遵守的标准 • 允许使用组件的多个不同版本,对用户几乎透明 • 使得可以按相同的方式来处理类似的组件 • 定义了一个与语言无关的框架 • 支持对远程组件的透明链接 • COM强制开发人员必须将客户和组件严格地隔离开
接口的作用 • 在COM中接口就是一切 • 对客户来说,一个组件就是一个接口集 • 客户只能通过接口才能与COM组件交互
接口对COM程序的重要性 • 可复用应用程序结构 接口A-C A C 接口A-B 接口C-D 接口B-D B D 接口D-E 接口B-E E
接口的简单实现 class IX{ public: virtual void Fx1()=0; virtual void Fx2()=0; }; class IY{ public: virtual void Fy1()=0; virtual void Fy2()=0; }; class CA: public IX,public IY{ virtual void Fx1(){cout<<“Fx1”<<endl;} virtual void Fx2(){cout<<“Fx2”<<endl;} virtual void Fy1(){cout<<“Fy1”<<endl;} virtual void Fy2(){cout<<“Fy2”<<endl;} };
说明 • 对纯虚函数的继承被称为接口继承 • 派生类所继承的只是基类对函数的描述 • 抽象类没有提供任何可供继承的实现细节 • 注意! • COM是与语言无关的,对于接口,它有一个二进制的标准 • 表示COM接口的内存块必须具有一定的结构 • 必须继承IUnknown接口
约定 # define interface struct #include <objbase.h> interface IX{ virtual void _stdcall Fx1()=0; virtual void _stdcall Fx2()=0; } interface IY{ virtual void _stdcall Fy1()=0; virtual void _stdcall Fy2()=0; } 组件图形化表示 IX IY
模拟实现 interface IX{…}; interface IY{…}; class CA:public IX,public IY{…}; main{ CA *pA=new CA; IX* pIX=pA; pIX->Fx1(); PIX->Fx2(); IY* pIY=pA; pIY->Fy1(); pIY->Fy2(); delete pA; return 0; }
代码分析 • 非接口通信 • 指向CA的指针 • 使用了new和delete控制组件的生命周期 C++ • 实现细节 • 类并非组件 • 接口并非总是继承的 • 多重接口及多重继承 系统-组件-接口-函数 • 命名冲突 • COM接口是一个二进制标准 • 客户同接口的连接不是通过其名称或其成员函数的名称完成的,而是通过在内存块中的位置完成的。 • 解决冲突的方法是不使用多重继承,可以合用指向实现的指针
接口理论 • 接口的不变性 • 一旦公布了一个接口,那么它将永远保持不变 • 升级时一般不修改已有接口,而是加入新接口 • 多态 • 按照同一种方式处理不同的对象 • 接口表示的行为越多,它的特定性越强,被其它组件复用的可能性越小
虚拟函数表 interface IX{ virtual void _stdcall Fx1()=0; virtual void _stdcall Fx2()=0; virtual void _stdcall Fx3()=0; virtual void _stdcall Fx4()=0; } IX 虚拟函数表 pIX vtbl指针 &Fx1 &Fx2 &Fx3 &Fx4
虚拟函数表与接口 • 定义纯抽象基类就定义了相应的内存结构 • 只有派生类实现抽象基类时才会分配 • COM接口内存结构与C++编译器为抽象基类生成的内存结构相同-巧合 • IX即是一个接口 • IX也是一个抽象基类
实例数据 class CA: public IX{ virtual void _stdcall Fx1(){cout<<“Fx1”<<endl;} virtual void _stdcall Fx2(){cout<<m_Fx2<<endl;} virtual void _stdcall Fx3(){cout<< m_Fx3 <<endl;} virtual void _stdcall Fx4(){cout<< m_Fx4 <<endl;} CA(double d) :m_Fx2(d*d), m_Fx3(d*d*d),m_Fx4(d*d*d*d) { } double m_Fx2; double m_Fx3; double m_Fx4; };
vtbl指针及实例数据 IX 虚拟函数表 客户 pA vtbl指针 &Fx1 Fx1 &Fx2 &m_Fx2 Fx2 &Fx3 &m_Fx3 Fx3 &Fx4 &m_Fx4 Fx4
共享vtbl实例数据 int main () { CA* pA1=new CA(1.5); CA* pA2=new CA(2.75); … }
多重实例 vtbl指针 &m_Fx2 虚拟函数表 客户 &m_Fx3 pA1 pA2 &Fx1 Fx1 &m_Fx4 &Fx2 Fx2 &Fx3 Fx3 vtbl指针 &Fx4 Fx4 &m_Fx2 &m_Fx3 &m_Fx4
不同类实例 class CB: public IX{ virtual void _stdcall Fx1(){cout<<“CB::Fx1”<<endl;} virtual void _stdcall Fx2(){cout<<“CB:: Fx2”<<endl;} virtual void _stdcall Fx3(){cout<< “CB:: Fx3”<<endl;} virtual void _stdcall Fx4(){cout<< “CB:: Fx4”<<endl;} } void foo(IX * pIX){pIX->Fx1(); pIX->Fx2(); } int main(){ CA* pA=new CA(1.77); CB* pB=new CB; IX* pIX=pA; foo(pIX); pIX=pB; foo(pIX); };
多重实例 虚拟函数表 vtbl指针 &Fx1 Fx1 客户 &Fx2 &m_Fx2 Fx2 pA1 pA2 &m_Fx3 &Fx3 Fx3 &Fx4 &m_Fx4 Fx4 虚拟函数表 &Fx1 vtbl指针 Fx1 &Fx2 Fx2 &m_Fx2 &Fx3 Fx3 &m_Fx3 &Fx4 Fx4 &m_Fx4
概览 • 组件和接口 • QueryInterface函数 • 引用计数 • 动态链接 • 类厂 • 组件复用:包容与聚合 • 服务器
接口查询 • 客户同组件的交互都是通过接口完成的 • 客户查询组件的其它接口时也要通过接口 • IUnknown UNKNWN.H interface IUnknown { virtual HRESULT _stdcall QueryInterface (const IID& iid, void** ppv)=0; virtual ULONG _stdcall AddRef()=0; virtual ULONG _stdcall Release()=0; };
IUnknown • 所有的COM接口都要继承IUnknown • 所有COM接口都可以当作IUnknown接口处理 • 组件的任何一个接口都可以用来获取它所支持的其它接口。 IX 虚拟函数表 客户 CA pIX vtbl指针 QueryInterface QueryInterface AddRef AddRef Release Release Fx Fx
获取IUnknown指针 • 自定义函数(非标准) IUnknown* CreateInstance() { IUnknown* pI=static_cast<IX*>(new CA); pI->AddRef(); return pI; } • 标准方式 HRESULT _stdcall CoCreateInstance(…);
QueryInterface • 查询某个组件是否支持某个特定的接口 virtual HRESULT _stdcall QueryInterface (const IID& iid, void** ppv); • IID :接口标识符 {0x32bb8320,0xb41b,0x11cf, {0xa6,0xbb,0x0,0x80,0xc7,xb2,0xd6,0x82}} • ppv:请求的接口指针地址 • HRESULT:32位结构 • S_OK • E_NOINTERFACE • 宏SUCCEEDED和FAILED处理结果
使用QueryInterface void foo(IUnknown pI) { IX* pIX=NULL; HRESULT hr=pI->QueryInterface(IID_IX,(void**) &pIX); if (SUCCEEDED(hr)) { pIX->Fx(); } }
实现类及其接口的继承关系 interface IX: IUnknown {/*…*/ }; interface IY: IUnknown {/*…*/ }; class CA: public IX, public IY {/*…*/ }; IUnknown IUnknown IX IY CA
实现QueriInterface HRESULT _stdcall CA::QueryInterface(const IID& iid,void** ppv) { if (iid==IID_IUnknown){ *ppv=static_cast<IX *>(this); }else if (iid==IID_IX){ *ppv=static_cast<IX*>(this); } else if (iid==IID_IY){ *ppv=static_cast<IY*>(this); }else{ *ppv=NULL; return E_NOINTERFACE; } static_cast<IUnknown*>(*ppv)->AddRef(); reurn S_OK; }
类型转换 • 将this转换成IX得到的地址和转换成IY得到的地址是不同的 static_cast<IX*>(this) != static_cast<IY*>(this) static_cast<void*>(this) != static_cast<IY*>(this) • 旧式表达 (IX*)this != (IY*)this (void*)this != (IY*)this • 不使用如下转换 *ppv= static_cast<IUnknown*>(this);
多重继承的内存结构 CA::this ΔIY (IX*)CA::this IX vtbl指针 虚拟函数表 (IY*)CA::this IY vtbl指针 QueryInterface AddRef CA实例数据 IX Release void foo(IX* pIX); void bar(IY* pIY) int main(){ CA* pA=new CA; foo(pA); bar(pA); delete pA; return 0; } Fx CA QueryInterface AddRef IY Release Fy
QueryInterface的实现规则 • 1.QueryInterface返回的总是同一IUnknown指针 • 2.若客户曾经获取过某个接口,那么它将总能获取此接口 • 3.客户可以再次获取已经拥有的接口 • 4.客户可以返回到起始接口 • 5.若能够从某个接口获取某特定接口,那么从任意接口都将可以获取此接口
同一IUnknown • 组件的实例只有一个IUnknown接口 • 为确定两个接口是否指向同一组件,可以通过这两个接口查询IUnknown接口,比较返回值 BOOL SameComponent(IX* pIX, IY* pIY){ IUnknown* pI1=NULL; IUnknown* pI2=NULL; pIX->QueryInterface(IID_IUnknown,(void**) &pI1); pIY->QueryInterface(IID_IUnknown,(void**) &pI2); return pI1==pI2; }
可以获取曾经得到的接口 • 如果QueryInterface曾经成功过,那么同一组件的后续QueryInterface将总是成功 • 如果QueryInterface失败,那么后续调用将失败 • 保证接口集不会发生变化
可以再次获取已经拥有的接口 • 拥有IX接口,查询IX接口 • 所有接口都继承了IUnknown,许多函数需要一个IUnknown指针作为参数 void f(IUnknown* pI){ IX* pIX=NULL; HRESULT hr=pIX->QueryInterface(IID_IX, (void**) &pIX); … } void main(){ IX* pIX=GetIX(); f(pIX); }
可以从任何接口返回起始接口 void f(IX* pIX){ HRESULT hr; IX* pIX2=NULL; IY* pIY=NULL; hr=pIX->QueryInterface(IID_IY, (void**) &pIY); if (SUCCEEDED(hr)){ hr=pIX->QueryInterface(IID_IX, (void**) &pIX2); } … }
若能够获取某特定接口,那么从任意接口都将可以获取此接口若能够获取某特定接口,那么从任意接口都将可以获取此接口 • 若可以通过接口IX得到IY,IY得到IZ,那么通过IX也将得到IZ • 防止顺序依赖 • 以上规则的目标是为了是QI使用更简单、更富逻辑性、一致性以及确定性
QueryInterface定义了组件 • 组件所支持的接口集就是QueryInterface能够为之返回接口指针的那些接口 • 客户了解组件所支持接口的唯一方法是进行查询 • DCOM中提供IMultiQI
新版本组件的处理 • COM中接口不会变化,具有唯一接口标识符(IID) • 可以建立新接口,并指定新的IID • 无缝处理多个版本 • QI收到老IID查询时返回老接口 • 收到新IID时返回新接口 • 客户方面 • 已有的客户运行不受影响 • 新客户可以自行决定使用老的或新的接口 • 接口的标识同其版本是完全绑在一块的
新老版本组件组合 Bronco pIFly IFly FastBronco pIFly pIFlyFast IFly IFlyFast
何时建立新版本 • 接口中函数的数目 • 接口中函数的顺序 • 某个函数的参数 • 某个函数参数的顺序 • 某个函数产生的类型 • 函数可能的返回值 • 函数返回值的类型 • 函数参数的含义 • 接口中函数的含义 • 只要修改会导致已有客户的正常运行
版本约定 • 新版本命名方式约定 • 在老版本后加一个数字 ,IFly2 • 隐含合约 • 仅保证函数名称及参数不变不足以保证修改组件不会妨碍客户正常运行 • 客户也许按照一定的方式或次序使用接口中函数 • 所有的接口都有隐含合约 • 避免隐含合约 • 使得接口不论成员函数如何被调用都能工作 • 强制客户按一定的方式来使用此接口并在文档中声明这一点