1、进程内组件和进程外组件
使用dll实现组件程序,则客户长须在调用组件程序的服务时,需要将dll装进自身的进程,所以客户程序和组件运行于同一进程空间,此类组件称为进程内组件。使用exe程序的组件,在被调用时自身具有进程空间,因此客户程序和组件运行在不同的进程空间,此类组件称之为进程外组件。
1.1、进程内组件
在客户程序与组件简历链接之后,客户程序可以得到直接指向组件接口虚函数表的接口指针,因此可以直接调用组件的成员函数,效率很高。DLL文件本身独立于客户程序,它在运行时被装入客户程序的内存,可以被多个进程使用。
DLL程序包含一个引出函数表,包含函数名、函数序号和函数地址,客户进程在动态加载dll时简历一张表,将客户调用同DLL里函数地址链接起来。只要不修改DLL引出函数的名称或参数信息,那么即使修改并重建了DLL程序,也不需要重新修改客户程序。为了增强dll的通用性,通常DLL的引出函数使用_stdcall调用习惯,而且每个函数定义前面加上 extern "C"说明符。一个典型的DLL引出函数声明如下:
extern "C" int _stdcall SomeFunction(int n);</span>
COM组件的dll工程中还需要DEF文件来描述DLL程序的模块信息,在“LIBRARY”部分指定DLL的文件名,在"EXPORTS"部分列出所有的引出函数并给每个函数分配一个唯一的序号。或者,可以直接在函数生命是使用_declspec(dllexport)说明该函数是一个dll的导出函数:
extern "C" _declspec(dllexport) int _stdcall SomeFunction(int n);</span>
客户程序可以使用三个系统函数操作dll中的方法:LoadLibrary装载dll,GetProcAddress取导出地址的函数,FreeLibrary释放dll模块。一般地过程如:①客户程序私用LoadLibrary函数装载dll,返回模块的实例句柄共以后操作该dll模块使用;②客户程序调用GetProcAddress获得dll引出函数的地址,可以按函数序号或函数名获取;③DLL使用完成后,调用FreeLibrary释放DLL模块。另外,除了引出函数之外DLL还可以引出全局变量。
1.2、进程外组件
进程外组件的存在形式为一个exe可执行文件,该文件执行时会独占一个进程。客户进程如何加载进程外组件涉及到进程间通信等问题。在视频处理应用尤其是Directshow中此类组件使用较少,暂且略去以后有需要再来研究。
2、通过注册表管理COM对象
注册表是系统级的信息存储,客户程序和组件都可以访问。组件的注册指组件程序将其实现的COM对象的信息以及接口信息保存到注册表中,具有这种自注册能力的组件称为可自注册组件。
2.1、注册表结构
COM标准规定,注册表必须包含COM库在完成各种操作时所要求的各项信息。Windows系统中的注册表是一个巨大的树形结构,在一个根节点下包含了一些键key和值value,每一个键又包含子键和值,如此层层向下延伸,形成了树形层次结构。
2.2、COM组件注册表信息
COM组件所使用的注册表节点主要是HKEY_CLASSES_ROOT这个键,其中最主要的是CLSID子键。在该子键下列出了当前系统中已经注册的所有组件的信息。CLSID下面的每个子键都代表了一个COM组件,这个组件的路径是调用组件时最关键的问题。对于进程内组件,则组件子键下包含InprocServer32子键,其缺省值就是dll文件的完整路径。对于进程外组件,则组件子键下包含LocalServer32子键,其缺省值就是dll文件的完整路径。除了CLSID外,还可以将COM组件以字符串化的组件名来查找,组件名被保存于ProgID子键中。
除了CLSID子键之外,HKEY_CLASSES_ROOT键还有Interface子键保存了当前系统中COM接口的信息,以及TypeLib子键保存当前系统中类型库的信息。
2.3、COM组件的注册
进程外组件以可执行文件的形式存在,可以直接执行,所以可以在执行过程中完成自身注册;而进程内组件不能直接运行,必须被某个进程调用才能获得控制。Windows提供了专用于注册进程内组建的工具regsvr32.exe,只要dll提供了入口函数DllRegisterServer和DllUnregisterServer函数,regsvr32.exe就可以实现组件的注册或注销。注册和注销操作不是由Regsvr32.exe提供的,而是由两个入口函数实现。
3、类厂
客户程序不会直接调用组件dll的引出函数,而是调用COM库的函数进行组件对象的创建。组件程序提供的标准入口函数DllGetObjectClass用于提供该组件程序的组件信息。
3.1、类厂和DllGetObjectClass函数
杜宇每一个COM类,都有一个专门的COM对象用于该类实例的创建。这个特殊的对象即是COM类的类厂,支持一个特殊的接口IClassFactory。该接口中有一个重要的成员函数CreateInstance用于创建对应的COM对象。声明方式如:
class IClassFactory: public IUnknown { virtual HRESULT _stdcall CreateInstance(IUnknown *pUnknownOuter, const IID& iid, void **ppv) = 0; virtual HRESULT _stdcall LockServer(BOOL bLock) = 0; }
COM标准规定,每一个COM类都应对应一个类厂,如果一个COM组件中包含多个COM对象,则应由多个类厂。
COM组件的导出函数DllGetObjectClass实现创造类厂的功能,该函数的声明类似于:
HRESULT DllGetClassObject(const CLSID &clsid, const IID& iid, (void**)ppv);
该函数的第一个参数为待创建对象的CLSID,第二个和第三个参数用于保存接口IID和类厂的指针接口。
3.2、COM库与类厂的交互
COM库中包含三个API用于对象的创建,分别为CoGetClassObject,CoCreateInstance,CoCreateInstanceEx。通常客户程序调用这三个函数之一完成对象的创建并返回对象接口的指针。COM库和类厂也通过这三个函数进行交互。
(1)CoGetClassObject:函数的声明如下:
HRESULT CoGetClassObject(const CLSID &clsid, DWORD dwClsContext, COSERVERINFO *pServerInfo, const IID &iid, (void**)ppv);
该函数首先找到由CLSID指定的类的类厂,如果是进程内组件则调用DLL模块的DllGetClassObject函数,并把clsid、iid和ppv传入创建类厂并返回类厂对象接口指针。第二个参数dwClsContext指定组件的类别(进程内/外组件、进程内控制对象)。第三个参数pServerInfo为指定服务器信息,对进程内组件应为null。
(2)CoCreateInstance:函数声明如下:
HRESULT CoCreateInstance(IUnknown *pUnknownOuter, DWORD dwClsContext, const IID& iid, (void **)ppv);
该函数是对CoGetClassObject的一层封装,内部实际调用了CoGetClassObject和生成类厂的CreateInstance函数完成对COM对象的创建。仅适用于创建本地COM组件。该函数也是实际使用中最常用的方法。
(3)CoCreateInstanceEx的功能与CoCreateInstance类似,只是可以用来创建远程COM组件。
3.3、类厂对组价生存期的控制
通常,类厂对象并不会长期保留,而是在每次创建对象的过程中建立新的。如果客户程序需要将类厂对象长期保留,那么可以调用IClassFactory接口的LockServer方法来锁定和解锁类厂。如果需要锁定类厂对象,则调用LockServer(TRUE);如果需要解锁,则调用LockServer(FALSE)。
4、COM库
4.1、COM库的初始化
是哦那个COM库的函数之前必须对COM库进行初始化,方法为:
HRESULT CoInitialize(IMalloc *pMalloc);
其中的参数指定一个内存分配器,通常情况下可以设置为NULL。通常一个进程只会对COM库初始化一次。在程序退出之前,必须对COM库进行反初始化,方法为:
void CoUninitialize(void);
4.2、组件程序的加载和卸载
(1)进程内组件的加载:在CoCreateInstance中,会调用CoGetClassObject。COM库根据注册表中的信息,找到相应CLSID对应的组件dll文件的完整路径,然后调用LoadLibrary,并调用dll中的DllGetClassObject导出函数创建相应的类厂,并返回IClassFactory接口,此时CoGetClassObject任务完成。然后客户程序调用类厂对象的CreateInstance函数负责COM对象的创建。
(2)进程内组件的卸载:当组件程序满足以下两个条件时才能被卸载:组件中对象数为0,类厂的锁计数器为0。此时DllCanUnloadNow返回TRUE。客户程序在空闲处理中调用CoFreeUnusedLibraries函数,该函数会检测当前进程中所有组件程序,当发现某个组件的DllCanUnloadNow返回TRUE时,将会调用FreeLibrary函数将组件从内存中卸载。