COM编程大体梳理

比较旧的东西,之前写的文章如今发出来,转载请注明~!c++

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是个什么东西:spa

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

 


  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的例子

相关文章
相关标签/搜索