COM编程大致梳理

比较旧的东西,以前写的文章现在发出来,转载请注明~!

1       COM编程思想--面向组件编程思想

1.1     面向组件编程 

       众所周知,由C到C++,实现了由面向过程编程到面向对象编程的过渡。而COM的出现,又引出了面向组件的思想。其实,面向组件思想是面向对象思想的一种延伸和扩展。

       下面,我就简单介绍一下面向组件的思想。在以前,应用程序总是被编写成一个单独的模块,就是说一个应用程序就是一个单独的二进制文件。后来在引入了面向组件的编程思想后,原本单个的应用程序文件被分隔成多个模块来分别编写,每个模块具有一定的独立性,也应具有一定的与本应用程序的无关性。一般来说,这种模块的划分是以功能作为标准的。这样做的好处有很多,比如当对软件进行升级的时候,只要对需要改动的模块进行升级,然后用重新生成的一个新模块来替换掉原来的旧模块(但必须保持接口不变),而其他的模块可以完全保持不变。

       总结一下:面向组件编程思想,归结起来就是四个字:模块分隔。这里的“分隔”有两层含义,第一就是要“分”,也就是要将应用程序(尤其是大型软件)按功能划分成多个模块;第二就是要“隔”,也就是每一个模块要有相当程度的独立性,要尽量与其他模块“隔”开。这四个字是面向组件编程思想的精华所在,也是COM的精华所在!

1.2     COM的几个重要概念

1.2.1     组件

       上面已经解释过组件,现在我只想强调一下组件需要满足的一些条件。首先是封装性,组件必须向外部隐藏其内部的实现细节,使从外部所能看到的只是接口。然后是组件必须能动态链接到一起,而不必像面向对象中的class一样必须重新编译。现在我只想强调一下组件需要满足的一些条件。首先是封装性,组件必须向外部隐藏其内部的实现细节,使从外部所能看到的只是接口。然后是组件必须能动态链接到一起,而不必像面向对象中的class一样必须重新编译。

1.2.2     接口

       由于组件向外部隐藏了其内部的细节,因此客户要使用组件时就必须通过一定的机制,也就是说要通过一定的方法来实现客户与组件之间的通信,这就需要接口。所谓接口就是组件对外暴露的、向外部客户提供服务的“连接点”。 。外部的客户见不到组件内部的细节,它所能看到的只是接口,这有点像OSI网络协议分层模型,每一层就像一个组件,它内部的实现细节对于其他层是不可见的;而每一层通过“服务接入点”向其上层提供服务。

1.2.3     客户

       这里所说的客户不是指使用软件的用户,而是指要使用某一个组件的程序或模块。也就是说,这里的客户是相对组件来说的。

2       COM原理

 

2.1     COM与虚函数列表

       COM中的接口实际上是一个函数地址表,当组件实现了这个接口后,这个函数地址表中就填满了组件所实现的那些接口函数的地址。而客户也就是通过这个函数地址表获得组件中那些接口函数的指针,从而获得组件所提供的服务的。从某种意义上说,我们可以把接口理解为c++中的虚拟基类;或者说,在c++中可以用虚拟基类来实现接口!这是因为COM中规定的接口的存储结构,和c++中的虚拟基类在内存中的结构是一致的,我们可以简单的用纯粹的C++的语法形式来描述COM是个什么东西:

 class IObject
  {
  public:
    virtual Function1(...) = 0;
    virtual Function2(...) = 0;
    ....
  };

 


  class MyObject : public IObject
  {
  public:
    virtual Function1(...){...}
    virtual Function2(...){...}
....
  };

IObject就是我们常说的接口,MyObject就是所谓的COM组件。记住接口都是纯虚类。COM中所有函数都是虚函数,都必须通过虚函数表VTable来调用,这一点是无比重要的,必需时刻牢记在心。为了让大家确切了解一下虚函数表是什么样子,从《COM+技术内幕》中COPY了下面这个示例图:

 

 

 

 

 

2.2     COM基本接口类    

2.2.1     IUnknown

     COM规范规定任何接口都必须从IUnknown继承,IUnknown包含三个函数,分别是 QueryInterface、AddRef、Release。这三个函数是无比重要的,而且它们的排列顺序也是不可改变的。

       引用计数。AddRef用于增加引用计数。Release用于减少引用计数。首先我们考虑com对象只实现一个接口的情况,不妨把接口成为IsomeInterface, 因为IsomeInterface继承与IUnknown,所以ISomeInterface接口成员函数中包含IUnknown的三个函数。假设有一个客户程序的许多个逻辑模块使用到了该com对象, 从而在客户程序很多地方保持了该接口指针的引用, 比如说有三个地方分别用了pSomeInterface1 pSomeInterface2, pSomeInterface3只想该接口指针。 在客户程序这三个逻辑块中, 它可以调用接口成员函数一伙的接口所提供的服务,如果他一直要该接口提供的服务,把他就需要控制该对象使它一直保持的内存中。如果用完了该对象,那他就应该通知接口不再需要服务。 由于每个逻辑模块并不知道其他的逻辑模块是否在继续使用COM对象, 他们只能知道自己是否还需要该对象。而对于COM对象来说, 只要有任意一个逻辑模块还需要使用它, 那么他就必须驻留在内存中不能释放自己。COM采用了引用计数来解决这个问题。 当客户得到一个指向该对象接口的指针时,计数加1,用完计数减1;当对接口指针复制或赋值,引用计数加1.如果一个COM对象实现了多个接口,则可以采用同蝼蚁计数计数。只要计数不为0 它就继续生存下去,反之,则表示客户不再使用该对象,它就可以被清除了。

       接口查询。当客户创建COM对象之后, 创建函数总会为我们返回一个接口指针, 因为搜有的接口都继承了IUnknown,所以我们就可以通过QueryInterface一个接口指针。

       接口原则性:

(1)对于同一个COM对象的不同接口,查询到的IUnknown接口必须完全相同,也就是说每个IUnknown接口指针是唯一的,因此对于两个接口指针我们可以通过判断其查询到的IUnknown指针是否相同来判断他们是否指向同一个对象。 反之如果查询的不是IUnknown接口,而是其他接口,则通过不同而途径得到的接口指针允许不一样。这就允许有的对象可以在必要的时候才动态生成接口指针, 不用的时候可以把接口指针释放掉。

(2)接口对成型。对每一个接口查询其自身总应该成功。

(3)自反省。如果从一个接口指针查询到另一个接口指针, 则从第二个接口指针再回到第一个接口指针也必定成功。

(4)接口的传递性

(5)接口查询时间无关性

       我们来看看 QueryIInterface的实现方法, 我们考虑这种支持多接口对象的的实现方法。在c++中实现多接口COm对象有两种简单方法, 一种是使用多重继承,八所支持的接口类作为基类,然后在对象类中实现接口函数。另一种试试先内嵌接口类成员。 我们这里使用多重集成的办法实现多个接口支持。

       我们用字典对象做列子,字典对象实现两个接口:IDictionary和ISpellCheck首先我们来看一看

 

 

 

 

 

 

 

 

 

 

 

 

接口的转换过程中存在虚列表的裁剪问题。

2.2.2   IClassFactory

       IClassFactory的作用是创建COM组件。COM类用一个全局唯一的ID(GUID)来标识,称为CLSID,COM利用类厂(ClassFactory)来得到实例化的COM对象。系统用公共空间保存所有可被重用的COM类的CLSID和其具体位置的对应(在Windows下是保存在注册表中),这样所有的用户只要知道CLSID,都能顺利找到COM类。然后COM类可以利用类厂生成COM对象。COM类和类厂的实现代码可以在DLL中,也可以在EXE中,可以在本地,也可以在网络的另一端。COM对象要实现多个接口(Interface),每个接口都包含一组函数,也用一个GUID来标识,称为IID。QueryInterface()就可以根据IID来得到接口指针。IClassFactory最重要的一个函数就是CreateInstance,顾名思议就是创建组件实例,一般情况下我们不会直接调用它,API函数都为我们封装好它了。

       COM规定,每个COM对象类应该有一个相应的类厂对象,如果一个组件实现了多个COM对象类,则会有多个类厂。记下来我们看看类厂是如何避实用的。因为类厂本身也是一个COM对象,他被用于其他COM对象的创建过程。那么类厂对象又是谁创建的呢?答案是DllGetClassObject函数, 这个函数并不是COM库函数,而是一个组件程序的导出函数(类似DLL导出函数),我们来看下一下这个函数(ATL中自动生成)

 

STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID* ppv)

{

              return _AtlModule.DllGetClassObject(rclsid, riid, ppv);

}

 

       在COM库中有三个函数用于COM接口的创建,他们分别是CoGetClassObject,CoCreateInstance,CoCreateInstanceEx。CoGetClassObject一般用来创建类厂接口。我们一般使用CoCreateInstance来创建COM接口指针。但是CoCreateInstance不能创建远程机器上对象如果要创建远程对象要使用CoCreateInstanceEx。

2.2.3     IDispatch

         它的作用何在呢?除了C++还有很多别的语言,比如VB、 VJ、VBScript、JavaScript等等。可以这么说,如果没有这么多乱七八糟的语言,那就不会有IDispatch。COM组件是C++类,是靠虚函数表来调用函数的,对于VC来说毫无问题,这本来就是针对C++而设计的,以前VB不行,现在VB也可以用指针了,也可以通过VTable来调用函数了,VJ也可以,但还是有些语言不行,那就是脚本语言,典型的如 VBScript、JavaScript。不行的原因在于它们并不支持指针。现在网页上用的都是这些脚本语言,而分布式应用也是COM组件的一个主要市场,它不得不被这些脚本语言所调用,既然虚函数表的方式行不通,只能另寻他法,IDispatch应运而生。 调度接口把每一个函数每一个属性都编上号,客户程序要调用这些函数属性的时侯就把这些编号传给IDispatch接口就行了,IDispatch再根据这些编号调用相应的函数,仅此而已。当然实际的过程远比这复杂,仅给一个编号怎么调用一个函数,还要知道要调用的函数要带什么参数,参数类型什么以及返回什么东西,而要以一种统一的方式来处理这些问题是件很头疼的事。IDispatch接口的主要函数是Invoke,客户程序都调用它,然后Invoke再调用相应的函数,如果看看MS的类库里实现 Invoke的代码就会惊叹它实现的复杂了,因为必须考虑各种参数类型的情况,所幸我们不需要自己来做这件事。在ATL中        

 

2.3     CLSID

       CLSID其实就是一个号码,或者说是一个16字节的数。观察注册表,在HKEY_CLASSES_ROOT\CLSID\{......}主键下,LocalServer32(DLL组件使用InprocServer32) 中保存着程序路径名称。CLSID 的结构定义如下:

typedef struct _GUID {

       DWORD Data1;  // 随机数

       WORD Data2;   // 和时间相关

       WORD Data3;   // 和时间相关

       BYTE Data4[8];      // 和网卡MAC相关

} GUID;

 

typedef GUID CLSID;  // 组件ID

typedef GUID IID;    // 接口ID

 

2.4     COM组件核心IDL

          COM具有语言无关性,它可以用任何语言编写,也可以在任何语言平台上被调用。但至今为止我们一直是以C++的环境中谈COM,那它的语言无关性是怎么体现出来的呢?或者换句话说,我们怎样才能以语言无关的方式来定义接口呢?前面我们是直接用纯虚类的方式定义的,但显然是不行的,除了C++谁还认它呢?正是出于这种考虑,微软决定采用IDL来定义接口。说白了,IDL实际上就是一种大家都认识的语言,用它来定义接口,不论放到哪个语言平台上都认识它。

       什么是IDL和MIDL?

       IDL是接口定义语言。MIDL是Microsoft的IDL编译器。在用IDL对接口和组件进行了描述后,可以用MIDL进行编译,生成相应的代理和存根DLL的C代码。为得到一个代理/存根DLL,需要编译和链接MIDL生成的C文件。宏REGISTER_PROXY_DLL将完成代理/存根DLL在注册表中的注册操作。

       客户与一个模仿组件的DLL进行通信,这个DLL可以完成参数的列集,此组件被称为代理。一个代理就是同另一个组件行为相同的组件组件还需要一个存根的DLL,以便对从客户传来的数据进行散集。存根也将对传回给客户的数据进行列集。

2.5     COM组件运行机制

构造一个创建COM组件的最小框架结构
    IUnknown *pUnk=NULL;
    IObject *pObject=NULL;
    CoInitialize(NULL);
    CoCreateInstance(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IUnknown, (void**)&pUnk);
    pUnk->QueryInterface(IID_IOjbect, (void**)&pObject);
    pUnk->Release();
    pObject->Func();
    pObject->Release();
    CoUninitialize(); 

 这就是一个典型的创建COM组件的框架,看看CoCreateInstance内部做了一些什么事情。以下是它内部实现的一个伪代码:
    CoCreateInstance(....)
    {
    .......
    IClassFactory *pClassFactory=NULL;
    CoGetClassObject(CLSID_Object, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory, (void **)&pClassFactory);
    pClassFactory->CreateInstance(NULL, IID_IUnknown, (void**)&pUnk);
    pClassFactory->Release();
    ........
   } 

 它的意思就是先得到类厂对象,再通过类厂创建组件从而得到IUnknown指针。继续深入一步,看看CoGetClassObject的内部伪码:

 CoGetClassObject(.....)
   {
    //通过查注册表CLSID_Object,得知组件DLL的位置、文件名
    //装入DLL库
    //使用函数GetProcAddress(...)得到DLL库中函数DllGetClassObject的函数指针。
    //调用DllGetClassObject 
   }
    DllGetClassObject是干什么的,它是用来获得类厂对象的。只有先得到类厂才能去创建组件.
    下面是DllGetClassObject的伪码:
    DllGetClassObject(...)
    {
    ......
    CFactory* pFactory= new CFactory; //类厂对象
    pFactory->QueryInterface(IID_IClassFactory, (void**)&pClassFactory);
    //查询IClassFactory指针
    pFactory->Release();
    ......
    }
    CoGetClassObject的流程已经到此为止,现在返回CoCreateInstance,看看CreateInstance的伪码:

        CFactory::CreateInstance(.....)
    {
    ...........
    CObject *pObject = new CObject; //组件对象
    pObject->QueryInterface(IID_IUnknown, (void**)&pUnk);
    pObject->Release();
    ...........
    } 

//见MyCom的例子

 

 

2.6      连接点

     COM 中的典型方案是让客户端对象实例化服务器对象,然后调用这些对象。然而,没有一种特殊机制的话,这些服务器对象将很难转向并回调到客户端对象。COM 连接点便提供了这种特殊机制,实现了服务器和客户端之间的双向通信。使用连接点,服务器能够在服务器上发生某些事件时调用客户端。

     有了连接点,服务器可通过定义一个接口来指定它能够引发的事件。服务器上引发事件时,要采取操作的客户端会向服务器进行自行注册。随后,客户端会提供服务器所定义接口的实现。客户端可通过一些标准机制向服务器进行自行注册。COM 为此提供了 IConnectionPointContainer 和 IConnectionPoint 接口。

//见MyCom的例子

 

转载于:https://www.cnblogs.com/just-bg/p/9399429.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值