COM编程笔记

COM编程理论知识

若有技术上的错误之处,请大家多多指正。 

为什么要用COM

  COM编程目标只有一个,就是希望软件能象积方块一样,是组装起来的。通过把软件划分成许多模块,每个模块完成各自的功能,做到高内聚低藕合,可以把不同的模块分给不同的人去做,然后积木一样合到一块,这有了组装的概念。

软件工程

       软件工程的核心就是模块化,最理想的情况就是100%内聚0%藕合。
       自然界是由各种各样的事物组成的,事物之间存在着千丝万缕的关系。我们可以认为事物是稳定的不变的,而事物之间的联系是多变的、运动的。我们在划分模块的时侯,有没有想过这个函数与哪些对象有关呢?一个函数实现一种功能,这个功能必定与某些事物联系,我们没有去掌握事物本身,而只考虑事物之间是怎么相互作用,而完成一个功能的。

模块化重用

搭积木式的软件构造方法的基础,是有各种各样的、可重用的部件、模块。但类库的重用,是基于源码的方式,这是它的重大缺陷。
       首先,类库限制了编程语言,类库总是用一种语言写的,那就不能拿到别的语言里用了。
       其次,类库每次都必须重新编译,只有编译后,才能与你的代码结合在一起生成可执行文件。开发完成后,EXE已经生成了,如果这时侯,类库提供厂商有更新版本,那你就必须重新编译、重新调试!我们希望换一个新的模块,是非常方便的事,不要重新编译。

二进制的代码重用

DLL的缺点

       另一种重用方式是DLL的方式。Windows里到处是DLL,它是Windows 的基础,但DLL也有它自己的缺点。

       总结一下它至少有四点不足。

函数重名问题

       DLL里是一个个的函数,我们通过函数名来调用函数,如果两个DLL有重名的函数怎么办?
       各编译器对C++函数的名称修饰,存在不兼容的问题。对于C++函数,编译器要根据函数的参数信息,为它生成修饰名,DLL库里存的就是这个修饰名,但是不同的编译器,产生修饰的方法不一样,所以你在VC 里编写的DLL,在BC里可能就用不了。可以用extern "C",来指示编译器用C函数编译,关闭函数名修饰功能,但这样又丧失了C++的重载多态性功能。

路径问题

放在自己的目录下面,别人的程序就找不到,放在系统目录下,就可能有重名的问题。真正的组件,应该可以放在任何地方,甚至可以不在本机,用户根本不需考虑这个问题。

DLL与EXE的依赖问题

我们一般都是用隐式连接的方式,就是编程的时侯指明:用什么DLL。这种方式很简单,在编译时就把EXE与DLL绑在一起了。如果DLL发行了一个新版本,我们很有必要重新链接一次,因为DLL里面函数的地址可能已经发生了改变。

DLL的缺点就是COM的优点

        首先,我们要先把握住一点,COM和DLL一样都是基于二进制的代码重用,所以它不存在类库重用时的问题。另一个关键点是:COM本身也是DLL,即使是ActiveX控件 .ocx ,实际上也是DLL。
        所以说,DLL在重用上还是有很大的优势的,只不过,我们通过制订复杂的COM协议,通过COM自身的机制,改变了重用的方法,以一种新的方法来利用DLL,来克服DLL本身所固有的缺陷,从而实现更高一级的重用方法。

       COM没有重名问题,Com不是通过函数名来调用函数,而是通过虚函数表,自然不会有函数名修饰的问题。路径问题也不存在,因为COM是通过查注册表来找组件的,放在什么地方都可以,即使在别的机器上也可以。也不用考虑和EXE的依赖关系了,它们二者之间是松散的结合在一起,可以轻松的换上组件的一个新版本,而应用程序混然不觉。 

COM编程必须掌握的COM理论知识

  要学COM的基本原理,推荐的书是《COM技术内幕》。但仅看这样的书,是远远不够的,我们最终的目的,是要学会怎么用COM去编程序,而不是研究COM本身的机制。所以我觉得对COM的基本原理,不需要花大量的时间去追根问底,没有必要,是吃力不讨好的事。其实,我们只需要掌握几个关键概念就够了。这里我列出了一些,我自己认为是用VC编程,所必需掌握的几个关键概念。均是用C++语言条件下的COM编程方式。

COM组件实际上是一个C++类

  COM组件实际上是一个C++类,而接口都是纯虚类。组件从接口派生而来:
  class IObject  
  {  
  public:  
    virtual Function1(...) = 0;  
    virtual Function2(...) = 0;  
    ....  
  };  
  class MyObject : public IObject  
  {  
  public:  
    virtual Function1(...){...}  
    virtual Function2(...){...}  
               ....  
  }; 

IObject就是接口,MyObject就是所谓的COM组件。切记接口都是纯虚类,它包含的函数都是纯虚函数,而且它没有成员变量。COM组件就是从这些纯虚类的派生类,COM组件实现了这些虚函数。COM组件是以 C++为基础的,特别重要的是虚函数和多态性的概念,COM中所有函数都是虚函数,都必须通过虚函数表VTable来调用,必需时刻牢记。

COM组件有三个最基本的接口类

COM组件有三个最基本的接口类,分别是IUnknown、IClassFactory、IDispatch。

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

QueryInterface 用于查询组件实现的其它接口,就是查看这个组件的父类中,还有哪些接口类;

AddRef用于增加引用计数;

Release用于减少引用计数。

 

IUnknow

1、为一个组件用户提供一种标准途径,通过该途径,用户可以在指定的组件里,要求使用一个特定的接口。QueryInterface可以完成该功能。

2、IUnknown提供两种方法:AffRef和Release。在组件实例里进行生存期方面的管理。

        IUnknown
        {
        public:
            BEGIN_INTERFACE
            virtual HRESULT STDMETHODCALLTYPE QueryInterface( 
                /* [in] */ REFIID riid,
                /* [iid_is][out] */ __RPC__deref_out void __RPC_FAR *__RPC_FAR *ppvObject) = 0;

            virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0;

            virtual ULONG STDMETHODCALLTYPE Release( void) = 0;

            template<class Q>
            HRESULT
#ifdef _M_CEE_PURE
            __clrcall
#else
            STDMETHODCALLTYPE
#endif
            QueryInterface(Q** pp)
            {
                return QueryInterface(__uuidof(Q), (void **)pp);
            }

            END_INTERFACE
        };

 

组件用户不能直接删除我们的C++实例。事实上,由于其他的客户端可能正在访问相同的一个组件对象,所以某一个客户也不应该试图删除该对象。只有组件自己可以根据内部的引用计数器来决定自己是否应该被删除。

引用计数

引用计数也是COM中的一个非常重要的概念。简单的说,COM组件是个DLL,当客户程序要用它时就要把它装到内存里。另一方面,一个组件也不是只给一个人用的,可能会有很多个程序同时都要用到它。但实际上,DLL只装载了一次,即内存中只有一个COM组件,那COM组件由谁来释放?只能由COM组件自己来负责。所以出现了引用计数的概念,COM维持一个计数,记录当前有多少人在用它,每多一次调用计数就加一,少一个客户用它就减一,当最后一个客户释放它的时侯,COM知道没有人用它了,那它就把它自己给释放了。引用计数是COM编程里非常容易出错的一个地方,但,所幸VC的各种类库里,已经基本上把AddRef的调用给隐含了,我们只需在适当的时侯调用Release。至少有两个时侯要记住调用Release,第一个是调用了 QueryInterface以后,第二个是调用了任何得到一个接口的指针的函数以后,记住多查MSDN 以确定某个函数内部是否调用了AddRef,如果是的话那调用Release的责任就要归你了。 IUnknown的这三个函数的实现,非常规范但也非常烦琐,容易出错,所幸的事我们不需要自己来实现它们。

  IClassFactory的作用是创建COM组件。我们已经知道COM组件实际上就是一个类,那我们平常是怎么实例化一个类对象的?是用‘new’命令!很简单吧,COM组件也一样如此。客户程序不可能知道组件的类名字,如果客户知道组件的类名字,那组件的可重用性就要打个大大的折扣了,事实上,客户程序只不过知道一个代表着组件的128位的数字串而已。所以客户无法自己创建组件,如果组件是在远程的机器上,你还能new出一个对象吗?所以创建组件的责任交给了一个单独的对象,这个对象就是类厂。每个组件都必须有一个与之相关的类厂,这个类厂知道怎么样创建组件,当客户请求一个组件对象的实例时,实际上这个请求交给了类厂,由类厂创建组件实例,然后把实例指针交给客户程序。这个过程在跨进程及远程创建组件时特别有用,因为这时就不是一个简单的new操作就可以的了,它必须要经过调度,而这些复杂的操作都交给类厂对象去做了。IClassFactory最重要的一个函数就是CreateInstance,就是创建组件实例,我们不会直接调用它,API函数都为我们封装好它了,只有某些特殊情况下才会由我们自己来调用它,这也是VC编写COM组件的好处,使我们有了更多的控制机会,而VB给我们这样的机会则是太少太少了。

  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的代码就会惊叹它实现的复杂了,因为你必须考虑各种参数类型的情况,所幸我们不需要自己来做这件事。

 

自动化接口的好处及缺点

   dispinterface接口、Dual接口以及Custom接口 

  自动化接口的好处及缺点,用这三个术语来解释可能会更好一些,可能并非那么精确。

  所谓的自动化接口,就是用IDispatch实现的接口。IDispatch的作用,它的好处就是脚本语言象VBScript、 JavaScript也能用COM组件了,从而基本上做到了与语言无关。

        它的缺点主要有两个:
        第一个就是速度慢效率低。通过虚函数表一下子就可以调用函数了,而通过Invoke则等于中间转了道手续,尤其是需要把函数参数,转换成一种规范的格式才去调用函数,耽误了很多时间。所以,一般若非是迫不得已,我们都想用VTable的方式调用函数,以获得高效率。
        第二个缺点,就是只能使用规定好的,所谓的自动化数据类型。如果不用IDispatch,我们可以想用什么数据类型就用什么类型,VC会自动给我们生成相应的调度代码。而用自动化接口就不行了,因为Invoke的实现代码是VC事先写好的,而它不能事先预料到我们要用到的所有类型,它只能根据一些常用的数据类型,来写它的处理代码,而且它也要考虑不同语言之间的数据类型转换问题。所以VC自动化接口生成的调度代码只适用于它所规定好的那些数据类型,但不能满足自定义数据结构的要求。可以自己写调度代码来处理你的自定义数据结构,但这并不是一件容易的事。考虑到IDispatch的种种缺点,它还有一个缺点,就是使用麻烦,现在一般都推荐写双接口组件,称为dual接口,实际上就是从IDispatch继承的接口。我们知道任何接口都必须从 IUnknown继承,IDispatch接口也不例外。那从IDispatch继承的接口实际上就等于有两个基类,一个是IUnknown,一个是IDispatch,所以,它可以以两种方式来调用组件,可以通过 IUnknown用虚函数表的方式调用接口方法,也可以通过IDispatch::Invoke自动化调度来调用。这就有了很大的灵活性,这个组件既可以用于C++的环境,也可以用于脚本语言中,同时满足了各方面的需要。

  相对比的,dispinterface是一种纯粹的自动化接口,可以简单的就把它看作是IDispatch接口 ,虽然它实际上不是,这种接口就只能通过自动化的方式来调用,COM组件的事件一般都用的是这种形式的接口。

  Custom接口就是从IUnknown接口派生的类,显然它就只能用虚函数表的方式来调用接口了 

COM组件有三种,进程内、本地、远程

   COM组件有三种,进程内、本地、远程。对于后两者情况必须调度接口指针及函数参数。

   COM是一个DLL,它有三种运行模式。和调用者在同一个进程内,也可以和调用者在同一个机器上,但在不同的进程内,还可以和调用者在两台机器上。
        这里有一个根本点需要牢记,就是COM组件它只是一个DLL,它自己是运行不起来的,必须有一个进程装载它,即COM组件必须在一个进程内。
        先说说调度的问题。对于WIN32程序,每个进程都拥有4GB的虚拟地址空间,每个进程都有其各自的编址,同一个数据块,在不同的进程里的编址,很可能是不一样的,所以存在着进程间的地址转换问题。这就是调度问题。对于本地和远程进程来说,DLL 和客户程序在不同的编址空间,所以,要传递接口指针到客户程序,必须要经过调度。Windows 已经提供了调度函数,不需要我们来做这个复杂的事情了。对远程组件来说,函数的参数传递是另外一种调度。DCOM是以RPC为基础的,要在网络间传递数据,必须遵守标准的网上数据传输协议,数据传递前要先打包,传递到目的地后要解包,这个过程就是调度,这个过程很复杂,不过Windows已经把一切都给我们做好了,一般情况下,不需要我们来编写调度DLL。

  COM组件必须在一个进程内。对于本地模式的组件,一般是以EXE的形式出现,所以它本身就已经是一个进程。对于远程DLL,我们必须找一个进程,这个进程必须包含了调度代码,以实现基本的调度。这个进程就是dllhost.exe。这是COM默认的DLL代理。实际上,在分布式应用中,我们应该用MTS来作为DLL代理,因为MTS有着很强大的功能,是专门的用于管理分布式DLL组件的工具。

  我们编程时很少关注到调度,这也是COM的一个优点之一,即平台无关性,无论你是远程的、本地的,还是进程内的,编程是一样的,一切细节都由COM自己处理好了,所以我们也不用深究这个问题,只要有个概念就可以了,如果你需要深入的了解调度的整个过程,推荐一本书《COM+技术内幕》。

COM组件的核心是IDL 

  我们希望软件是拼装出来的,各个模块如何才能亲密无间的合作,必须事先共同制订好它们之间交互的规范,这个规范就是接口。我们知道接口实际上都是纯虚类,它里面定义好了很多的纯虚函数,等着某个组件去实现它,这个接口就是两个完全不相关的模块能够组合在一起的关键试想一下如果我们是一个应用软件厂商,我们的软件中需要用到某个模块,我们没有时间自己开发,所以我们想到市场上找一找看有没有这样的模块,我们怎么去找呢?也许我们需要的这个模块在业界已经有了标准,已经有人制订好了标准的接口,有很多组件工具厂商已经在自己的组件中实现了这个接口,那我们寻找的目标就是这些已经实现了接口的组件,我们不关心组件从哪来,它有什么其它的功能,我们只关心它是否很好的实现了我们制订好的接口。这种接口可能是业界的标准,也可能只是你和几个厂商之间内部制订的协议,但总之它是一个标准,是你的软件和别人的模块能够组合在一起的基础,是COM组件通信的标准。

  COM具有语言无关性,它可以用任何语言编写,也可以在任何语言平台上被调用。但至今为止我们一直是以C++的环境中谈COM,那它的语言无关性是怎么体现出来的呢?或者换句话说,我们怎样才能以语言无关的方式来定义接口呢?前面我们是直接用纯虚类的方式定义的,但显然是不行的,除了C++谁还认它呢?正是出于这种考虑,微软决定采用IDL来定义接口。说白了,IDL实际上就是一种大家都认识的语言,用它来定义接口,不论放到哪个语言平台上都认识它。我们可以想象一下理想的标准的组件模式,我们总是从IDL开始,先用IDL制订好各个接口,然后把实现接口的任务分配不同的人,有的人可能善长用VC,有的人可能善长用VB,这没关系,作为项目负责人我不关心这些,我只关心你把最终的DLL 拿给我。这是一种多么好的开发模式,可以用任何语言来开发,也可以用任何语言来欣赏你的开发成果。 

COM是怎么跑起来

    COM组件的运行机制,即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();  
        ...........  
    }  

 

自注册的COM DLL

一个典型的自注册的COM DLL所必有的四个函数

  DllGetClassObject:用于获得类厂指针 

  DllRegisterServer:注册一些必要的信息到注册表中 

  DllUnregisterServer:卸载注册信息 

  DllCanUnloadNow:系统空闲时会调用这个函数,以确定是否可以卸载DLL 

  DLL还有一个函数是DllMain,这个函数在COM中并不要求一定要实现它,但是在VC生成的组件中自动都包含了它,它的作用主要是得到一个全局的实例对象。

注册表在COM中的重要作用 

  首先,要知道GUID的概念,COM中所有的类、接口、类型库都用GUID来唯一标识,GUID是一个128位的字串,根据特制算法生成的GUID,可以保证是全世界唯一的。 COM组件的创建,查询接口都是通过注册表进行的。有了注册表,应用程序就不需要知道组件的DLL文件名、位置,只需要根据CLSID查就可以了。当版本升级的时侯,只要改一下注册表信息就可以转到新版本的DLL。

 
 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值