COM是一系列标准规范,COM库是遵守这些标准的函数集合,用于管理COM对象,比如对象的注册、卸载、创建和内存管理等等。一般,COM库是在操作系统层次上实现的系统级代码。用户以COM库为中介间接地使用COM组件,因为COM库和COM组件都遵守COM规范,所以两者能够按照统一的方式在二进制代码级别进行交互。如果我们使用COM库管理上面实现的C++对象,那么这个对象必须要遵守标准。
1. 标准DLL入口点函数
COM组件(DLL)有几个标准的导出函数,都以“Dll”三个字符开头,其中,最重要的是组件入口点函数,它的标准化名字是DllGetClassObject,在上面例程中,我们已经仿真出了这个入口点函数,只不过我们起的名字是DllGetClassFactoryObject,以作区别。在使用组件时,客户程序不直接调用入口函数,而是COM库代劳。COM库中有一个函数CoGetClassObject,当它执行时,要调用DllGetClassObject,所以,如果组件中没有这个入口点函数,将无法被COM库使用,也就不是一个组件。
下面是标准入口点函数DllGetClassObject函数的原型:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppObject);
一般而言,一个组件含有多个组件对象,每个对象至少一个接口,要获得组件中某个对象的某个接口,可以通过前两个参数CLSID和IID指定,得到的对象用ppObject指针返回。
可以推知:当把组件DLL中的若干导出函数标准化之后,COM库就可以形成固定的模式,以一种统一的方式进行组件对象管理。这就是DLL标准化的意义所在!
2. 标准对象创建API
一旦入口点函数标准化,COM就可以将上面的DLL当作一个组件来管理,即使它还不是一个真正的COM组件。我们可以将DLL中的对象信息(所属类的ID即CLSID)和DLL所在路径记录在注册表中,然后让COM库利用此信息间接地调用LoadLibrary负责DLL的动态装载。载入后就可以创建和使用对象。
COM为此提供的API是CoGetClassObject:
第一个参数是准备装载的对象类标识符CLSID,第二个参数是关于组件对象的类别信息,一般使用的是服务器类别CLSCTX_SERVER(具体细节后面章节还要介绍)。
3. 标准对象注册
COM库管理的组件对象的信息都集中在注册表HKEY_CLASSES_ROOT\CLSID子键下,每个对象对应一个以CLSID字符串形式命名的子键,在这个子键下,COM库可以找到所需的信息并完成对象的创建。在这些信息当中,还有一个比较重要的子键是InprocServer32,它存放着DLL的路径。
当CoGetClassObject执行时,它首先根据CLSID查找注册表,找到DLL的位置,接着调用LoadLibrary函数加载DLL,然后再调用GetProcAddress函数获得DllGetClassObject函数的入口点并调用该函数,最后,根据客户提供的参数返回客户所需对象的指针。此后,所有事情只在对象和客户之间进行:返回的指针是一个间接指向该对象的类厂的vtable指针,这个指针的任何调用都是C++虚函数调用,COM库不再参与这个过程,它只是提供找到并装载组件对象的服务。
4. 类厂对象的标准化
在COM中,每个组件对象都是由相应的类厂对象“生产”的,而所有的类厂对象创建组件对象都是一样的模式。因此,类厂中提供其它对象创建功能的函数也变成了标准。这个函数的标准化名字是CreateInstance,它和我们实现的Create_cs功能是一样的。CreateInstance函数和其它几个相关函数合在一起,构成了一个标准类厂接口,此接口包装在共同的抽象基类IClassFactory当中。实现一个COM组件的类厂,这个抽象基类是必须要继承的。
既然类厂成为了标准化类,COM库就可以预先定义好类厂,然后用统一的方式使用类厂,比如:使用MFC开发组件时,就是用微软做好的现成的类厂。
5. DLL标准化的例程
本小节的例程也是在上一小节的基础上修改。其中,我们用到了COM库函数和标准的接口,它们是实现COM组件的必备内容,读者要仔细体会。(编者注:本例程比较重要,所以程序的所有代码都会列出以方便读者理解)
(1)建立组件程序项目。
新建一个类型为“Win32 Dynamic-Link Library” 的空项目,项目名称CS_NEARCOM,把上例DLL_VTBL的三个文件Ichar_set.h、char_set.h和char_set.cpp拷贝并添加到本项目中。
(2)修改含有抽象基类的接口文件Ichar_set.h。
COM组件的使用需要一些GUID,所以添加两个GUID到接口文件中,一个是组件类的ID即CLSID,另一个是接口ID即IID。在Visaul C++中,GUID是一个表示128位随机数的结构,关于这个结构的定义包含在objbase.h中,另外,后面将要用到的接口IClassFactory和IID_IClassFactory(类厂的IID)以及COM库函数和DllGetClassObject的声明,也都包含在其中。因此,Ichar_set.h要包含objbase.h,同时,删掉DEF_EXPORT宏、类ICS_Factory和DllGetClassFactoryObject函数。修改后的Ichar_set.h文件如程序清单8-9所示。
程序清单8-9 修改后的Ichar_set.h文件
两个GUID是借助工具产生的。Microsoft提供了两个工具用于产生GUID:UUIDGen.exe和GUIDGen.exe,前者是一个命令行程序,后者是一个基于对话框的应用程序,它们包含在Microsoft Visual Studio的安装目录的Common\Tools文件夹下。一般使用后者生成GUID并拷贝到项目程序中。当运行GUIDGen.exe时,会出现如图8-4所示的对话框。点击New GUID按钮产生新GUID并显示在Result中,选择Copy按钮可以将它拷贝到剪贴板,然后再粘贴到程序中。
(3)修改含有派生类的文件char_set.h。
为了后面和COM库兼容,类厂类CS_Factory在文件中重新进行了定义(见程序清单8-10):从标准接口类IClassFactory派生。这个接口类含有五个纯虚函数,它们必须要在派生类CS_Factory实现,特别是QueryInterface和CreateInstance两个函数。前者要被COM库函数CoGetClassObject使用来获得类厂接口(类厂的vtable),后者要被客户程序使用来创建组件对象。在这里,读者要着重注意这两个函数,而其它的函数只要了解其作用即可,可以不必深究,后面还要详细介绍。(www.xing528.com)
图8-4 用GUIDGen.exe产生GUID的对话框
程序清单8-10 重定义了类厂的char_set.h文件
(4)修改组件类和类厂的实现文件char_set.cpp。
此处的代码中,char_set类的实现部分没有发生变化,我们重点设计类厂类,特别是QueryInterface和CreateInstance两个函数,类厂的其它函数并没有给出实现相应功能的代码,只是简单给出了返回语句(不用担心,类厂的其它函数暂时还用不上,下一小节将给出它们的完备代码和具体功能)。另外一个需要注意的是标准导出函数DllGetClassObject,在上例的基础上换了名字,并且增加了参数,它在objbase.h文件中声明,函数原型是:
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID FAR* ppv);
其作用是根据组件类的CLSID获得类厂的接口,接口指针用ppv返回。
程序清单8-11是修改后的组件类和类厂的实现文件char_set.cpp。
程序清单8-11 修改后的组件类和类厂的实现文件
(5)创建DEF文件。
为了导出DllGetClassObject函数,要创建模块定义文件CS_NEARCOM.DEF。该文件的代码如下:
(6)构建组件模块DLL。
选择Build命令构建组件CS_NEARCOM.dll,这个DLL几乎就和真正的COM组件一样了。下面我们演示一下如何使用这个DLL。
(7)建立客户应用程序。
使用组件的客户应用程序的建立方法和上例一样,命名本项目为CLIENT3,把CS_NEARCOM文件夹下的Ichar_set.h文件拷贝到当前项目中,然后添加Clent3.cpp文件并编写代码使用组件。程序代码如程序清单8-12所示。
程序清单8-12 客户应用程序Client3.cpp
(8)手工修改注册表。
上面的客户例程用到了COM库函数CoGetClassObject,它的工作流程是固定的:从系统注册表中找到组件DLL的信息,然后加载DLL,并调用标准导出函数DllGetClassObject,从而获得类厂对象指针。因此,如果要让函数CoGetClassObject能正确执行,还必须要修改注册表。修改方法是:运行Regedit32.exe或Regedit.exe,打开注册表,找到HKEY_CLASSES_ROOT\CLSID子键。在这添加子键,名称为组件的CLSID,即{09D0355E-798C-4d5f-B2FC-6F4F68A5764B}(注意:必须带着{}括号,括号内的必须和你的CLSID一致)。然后再添加下一级子键InprocServer32,并为此子键添加一个未命名的值:类型为REG_SZ,串值为<PATH>\CS_NEARCOM.dll,其中,<PATH>是我们的CS_NEARCOM.dll所在路径。
(9)编译运行客户应用程序。
可以发现,程序运行结果和前面的例子一样。和前面的例程相比较,本例程的组件程序和客户程序在建立好之后,就可以分别进行修改和编译,没有谁先谁后的问题。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。