龙盟编程博客 | 无障碍搜索 | 云盘搜索神器
快速搜索
主页 > 软件开发 > VC开发 >

COM技术纵横谈(4)

时间:2009-12-30 15:42来源:未知 作者:admin 点击:
分享到:
下面我们来看看HRESULT值。这是一个32位的返回值,其最高位表示函数调用是否成功。第十六位包含的就是函数的返回值,其余的15位包含的是此类型以及返

  下面我们来看看HRESULT值。这是一个32位的返回值,其最高位表示函数调用是否成功。第十六位包含的就是函数的返回值,其余的15位包含的是此类型以及返回值起源的更详细信息。为了确定函数调用是否成功,需要使用SUCCEED和FAILED宏。

  CoCreateInstance函数是由COM函数库提供的,它的作用就是按照查询的组件和接口到系统中去寻找其所在的文件(一般总是EXE或者DLL文件)然后创建该组件并查询其接口。一般来说,这个函数的具体实现是与系统相关的,以后将会提到,在windows系统中,将查询注册表已确定某个特定的组件在哪个文件中。上例查询的是CLSID_COM1这个组件,由于一个组件可能包含有多个接口,所以我们使用IID_IX来制定所需要的接口,CLSCTX_INPROC_SERVER是一个常数,其指定组件所在的是一个DLL(由于DLL运行在客户的内存空间,所以可以称为是进程内组件)。最后一个参数传入的是接口指针,它将返回查询到的接口指针。可以想见,一个组件指针可能同时被几个客户所使用,所以需要一种手段来让组件实例知道自己正在被几个客户所使用,这样他才能再合适的时候销毁自己以让出内存空间。如果销毁的实际不当,比如还有个指针正在使用中,那么以后对该指针的调用就将失败并且用户程序将崩溃。COM采用相当简单的一种手段来进行所谓的引用计数:维护一个组件或接口的全局变量,但该变量的值为零时,销毁自己的时间就到了。CoCreateInstance实际上产生了该组件的实例,并在内部已经调用IUnknown的AddRef()函数来将引用计数置1了。正因为如此,例子最后调用的Release()函数就是做了清理工作:这个接口指针已经完成了它的工作,所以调用Release()告诉它:把你的引用计数减一。如果不这样做,组件将永远保留在内存中,直到应用程序结束的时候才从栈中被清除。

  对AddRef和Release函数的调用是为了更好的控制组件的生命期,当然如果处理得当,可以适当的减少AddRef/Release对以提高性能。一种特殊的情况就是当一个组件的生命期完全被包含在另一个组件内时,我们对被包含的那个组件可以不予计数。我不准备详细讨论优化问题,因为对于一般应用来说,保证程序的强壮和稳定才是最重要的。这里还得介绍一下ProgID。ProgID其实程序员给某个CLSID指定的易记名。某些语言如visual basic使用ProgID而非CLSID来表示组件。这里请注意,程序员对ProgID的命名只不过是遵循一个约定俗成的规定,并没有对具体的实现有任何的强制标准,所以其名字发生冲突也是有可能的。一般来说,ProgID具有如下格式:

  以我的注册表为例:

    INSHandler.INSHandler.1
    ImgUtil.CoSniffStream.1
    StaticMetafile
    Netscape.Help.1 

  不过由于ProgID没有专门的命名规则,所以出现不同于上述格式的名字也是完全有可能的。有时候客户并不关心它所连接的组件版本,换而言之,客户只需要知道该组件存在就心满意足了。所以,组件经常会有一个与版本无关的ProgID,此ProgID被映射成是所安装的最新版本的组件。完成从ProgID到CLSID的转换非常简单,只需要利用COM库中提供的两个函数CLSIDFromProgID和ProgIDFromCLSID就可以了。

    CLSID clsid;
    CLSIDFromProgID(L"Netscape.Help.1", &clsid); 

  上面的L""是一个扩展宏,用来转换普通的ANSI字符串成为Unicode串。

  下面需要讨论的问题是:假设现在我已经写好了一个组件,怎么才能在注册表中登记它的接口呢?非常简单,我们只需要在组件中实现下面两个函数就可以了。

    __declspec(dllexport) DllRegisterServer();
    __declspec(dllexport) DllUnregisterServer(); 

  具体而言,DllRegisterServer的实现实际上是通过直接调用注册表函数来实现的。为了注册或者取消某个组件的注册,需要用的函数一般有:

    RegOpenKeyEx
    RegCreateKeyEx
    RegSetValueEx
    RegEnumKeyEx
    RegDeleteKey
    RegCloseKey 

  使用这些函数是需要#include 或者,并在additional librarys里加上advapi32.lib。现在的一个问题是:客户怎样选择自己所需要的组件呢?开发人员需要的是一种无需创建组件实例就能知道它是否能提供所需接口的方法。轮询系统中的所有组件和接口不失为一种解决的方法,但这样做的系统开销相当大。为此引进了称为组件类别的方案。

  一个组件类别实际上就是一个接口集合。我们分配给该集合一个GUID以唯一的标示它,它被称作CATID。对于任何一个组件,如果它实现了某个组件类别的所有接口,那么它就可以把自己注册成是该组件类别的一个成员。这样一来,客户只需要选择合适的组件类别并查询其下所有列出的组件就可以了。对组件而言,并不限制它只能属于一个组件类别。反过来,属于某个组件类别的组件并不限于只实现改组件类别中的接口。如果乐意,你可以写一个组件支持实现所有组件级别并且还有额外的接口。组件类别是怎样被实现的?使用Component Category Manager(由windows提供),它是一个实现了ICatRegister和ICatInformation接口的组件。ICatRegister可以完成新组件类别的登记或取消,也可以将某个组件登记入某个组件类别,或取消之。ICatInformation则可以用来获取系统中某个组件类别的数据。

  组件中分配了一块内存,然后建起通过一个参数(可能是一个返回的指针)传递给了客户,这是一种非常常见的做法。问题是:谁来释放这块内存?这主要是由于组建和客户可能是有不同的程序员实现的,他们之间没有办法建立一种分配和释放内存的标准办法。COM解决中各问题的办法是提供一个接口(IMalloc),它可以有CoGetMalloc返回。为了分配内存,只需要调用IMalloc::Alloc,而调用改函数所分配的内存可以有IMalloc::Free负责释放。为了更加简单的实现,COM库提供了两个更加简单的函数:
    void CoTaskMemAlloc(ULONG cb /* size in bytes of block to be allocated */);
    void CoTaskMemFree(void* pv); 

  如果你认真看了我的文章,到现在为止你大体上已经有了一个概念:COM究竟是一种什么概念,它在哪些程度上需要程序员来实现,哪些则是由操作系统所提供的COM库完成的。

  不十分严格的说,COM的目的是把各种各样的函数分类,然后封装成一个个物件,这些物件在windows系统中以DLL或者EXE的形式具体存在,并且通过注册表,window随时随地可以知道某个特定组件的代码是在那个对应得DLL或者EXE里。这里提一下,怎么告诉windows你需要哪个组件呢?我们使用GUID,其复杂的算法保证了世界上没有两个个接口的ID标示号码是完全一样的!从而可以唯一的确定组件,包括内含的接口,在客户需要该组件的时候windows也就可以正确的装载它了。

  同样也是因为这个唯一性,客户在任何时候都可以直截了当的,明确无误的询问windows,我要的就是这个组件里的这个接口!告诉我你有吗?这时候,通过一个CoCreateInstance函数,windows将返回接口指针,或者干脆的告诉你,没有找到!那么,windows内部在执行这个函数的时候具体做了些什么呢?首先它查询了注册表,找寻你所要的组件(组件也就是接口集,而所谓接口也就是一组函数所组成的集合的代名词,这么说你明白了吗?)如果没有找到该组件,查询自然失败,函数返回,如果找到了,那么进一步的,内核将向windows返回该组件的IUnknown*指针,windows随后利用Unknown::QueryInterface函数查询你所指定的那个接口是不是被该组件所实现(或者说支持)说到这里你一定可以发现,凡是接口,一般来说总是要由你的代码去实现,IUnknown这个所有COM组件都必须实现的接口,其目的之一就是让Windows知道如何查询你的组件。直到组件里实现了哪些接口的只有你自己--写这个组件的人,所以你有责任妥当的好好些QueryInterface函数以便返回正确的指针,windows随后将该指针转给CoCreateInstance的调用处,整件事情也就结束了。现在你了解了吗?

精彩图集

赞助商链接