第一章 组件
随手翻过,无感而发。
第二章 接口
-
接口的作用
COM所有的内容,起于接口,终于接口。
在COM中,接口就是一切。对于客户而言,一个组件就是一个接口集。客户只能够通过接口才能够与COM打交道。
接口可以保护系统免受外界变化的影响,还可以使客户用同样的方式来处理不同的组件。
2. 接口的背后
COM接口在C++ 中是通过纯虚函数实现的。
定义一个纯抽象基类,所定义的实际上是一块内存块的结构。纯抽象基类所有的实现都是一些具有相同的基本结构的内存块。但是,这块内存只有到派生类实现此抽象基类的时候才会被分配,并继承该内存结构。
纯抽象基类所定义的内存结构可以分作两个部分:虚函数表(vtabl)以及指向虚函数表的指针(vtabl指针)。
COM 接口的内存结构与C++编译器为抽象基类所生成的内存结构是相同的。因此,可用抽象基类(纯虚函数)来定义COM接口。由此意义上讲,Interface既是接口,也是抽象基类。
3. vtabl指针
vtabl是在由抽象基类函数指针到函数的过程中增加了一个层次。当C++编译器生成代码的时候,实现抽象基类的类可能会将特定于实例的信息同vtabl一起保存。
vtabl的作用并不仅仅是方便为实例数据提供一个保存位置,实际上它还可以与同一类的不同实例共享同一vtabl。
接口的威力在于继承该接口的所有类均可以被用户按照同一方式进行处理。
第三章 QueryInterface函数
1. IUnknown
COM中所有的接口均继承了IUnknown,每个接口vtbl中的前面三个函数都是QueryInterface、AddRef和Release。
所有的接口都支持QueryInterface接口。因此,用户可以利用组件中的任何一个接口来获取组件所支持的其他接口。
IUnknow指针的获取:CreateInstance
在创建组件的时候,客户可以使用CreateInstance而无须再使用new操作符。
组件的实例只有一个IUnkown接口,所以无论通过哪个接口查询组件实例的IUnkown接口,都将返回同指针值。所以,有时候为了确认某两个接口是否指向同一个组件,可以通过这两个接口查询IUnkown接口,并对返回值进行比较即可。
2. QueryInterface
QueryInterface的形式: HRESULT _stdcall QueryInterface(const IID& iid, void ** ppv);
如果拥有了一个指向IUnknown的指针,利用IUnknown中包含的成员函数QueryInterface,客户可以查询组件支持的某个特定接口。如果支持,QueryInterface将返回该接口的指针,否则返回错误代码。
示例代码:
void foo(IUnknown* pI)
{
IX* pIX=NULL;
HRESULT hr=pI->QueryInterface(IID.IX,(void **) &pIX);
if(hr)
pIX->FX();
}
}
在上述代码中,将pIX初始化为NULL 是比较好的编程风格。
第四章 引用计数
1. 生命周期管理
COM组件通过AddRef 与Release实现其自身的生命周期管理。
引用计数是使组件能够自己将自己删除的最简单而又最为有效的方法。
2. AddRef与Release
AddRef告诉组建我们想使用某个接口;
Release告诉组件我们已经用完了某个接口。
第五章 DLL
1. 随便说说
这部分内容讨论如何将COM组件放到动态链接库中。但是并不意味着将一个组件变成DLL,一个组件实际上并不是一个DLL,将COM看作DLL是非常肤浅的。DLL只是一个组件服务器,组件实际上应该看作是在DLL中所实现的接口集。
DLL是形式,组件才是实质。
2. 为什么使用DLL来实现组件
之所以要使用DLL来实现组件,其原因在于DLL可以共享它们所链入的应用程序空间。
客户与组件通过接口进行交互。一个接口实际上是一个指向函数的指针列表vtbl,组件将为vtabl分配内存并用每个函数的地址来初始化该列表。为了使用vtabl,客户应该能够访问组建为其vtabl所分配的内存,同时还必须能够理解组建放到vtabl中的各个地址。在Windows中,由于DLL与客户所使用的是同一地址空间,因此客户访问vtabl不成问题。
在windows中,一个正在运行的程序是一个进程。每个应用程序都将以单独的进程运行,每个进程都有一个4GB的地址空间。进程间不能够不能进行指针传递。所幸的是,DLL将驻留在所连接的应用程序的地址空间,共享同一地址空间 ,因此DLL也叫做进程内服务器。与之相比较的是以EXE方式实现的进程外服务器。
第六章 HRESULT、GUID、注册表与其他
1. HRESULT
32位
组件使用HRESULT来向其用户报告各种情况(Here’s the RESULT)。HRESULT是一个可以分成三个部分的32位值。
大部分COM组件的接口函数返回HRESULT。HRESULT的比特位表示函数调用是否成功。
HRESULT的低16位包含函数的返回代码。HRESULT其余的15位包含有关类型和返回值起源的详细信息。这些信息是设备代码。我们可以用WIN32 API FormatMessage函数显示标准的COM错误信息。我们可以用MAKE_ HRESULT宏来自己定义一个HRESULT值。但是最好使用通用的COM成功和失败代码,避免自己定义。
2. GUID
GUID(Globally Unique Identifier(全局唯一标识符))是组件和接口的标识号。它不但可以用来唯一的标识接口,还可以用来唯一的标识组件。GUID在时间上和空间上都是唯一的。(目前生成GUID的算法可以保证3400年它仍然可以是唯一的)。我们可以用VC++带的GUIDGEN程序来生成一个GUID。为了和IID进行区别,类标识符对应的类型是CLSID。GUID一般采用引用传递。
3. 注册表
COM只使用了注册表的一个分支:HKEY_CLASSES_ROOT,在此键下可以看到一个CLSID键。CLSID键下有所有组件的CLSID。CLSID有一个默认值是DLL文件名称的InprocServer32的子键。
ProgID是程序员给CLSID指定的一个友好的名称。
可以用COM库提供的CLSIDFromProgID和ProgIDFromCLSID进行ProgID和CLSID之间的转换。
用REGSVR32程序来注册某个组件。用REGSVR32 /u卸载某个组件。但通常我们调用DllRegisterServer来进行安装程序时的自注册。
第七章 类厂
1. 随便说说
在介绍类厂之前,我们使用CoCreateInstance来创建组件。但是,CoCreateInstance的灵活性却不足以能够满足所有组件的要求。这也是引入类厂的原因。
因为COM对象可以位于客户进程之外 ,而且必须能够被不同的语言所访问,所以我们需要一种与语言无关的方法来实例化一个组件.在C++中,我们使用new运算符来创建一个对象的实例,但是COM则提供了一个标准接口IClassfactory,这个接口是一个特殊的、必须实现的构造器组件,使用它才能够让外部客户可以创建自己的组件实例。可以说,类厂的唯一功能就是简化其他COM对象的创建方法。
2. CoCreateInstance
在COM库的CoCreateInstance函数需要一个CLSID,在此基础上创建相应组件的一个实例,并返回组件实例的某个接口。
CoCreateInstance的声明如下:
HRESULT __stdcall CoCreateInstance(
const CLSID& clsid, ///待创建组件的CLSID
IUnknown* pIUnknownOuter, ///用于组件聚合(外部组件通过此参数向内部组件传递IUnknow)
DWORD dwClsContext, ///限定所创建组件的执行环境
const IID& iid, ///组件上待使用接口的IID
void ** ppv ///返回上一参数指定的接口指针
);
CoCreateInstance函数创建组件的过程是:传给CoCreateInstance函数一个CLSID,然后创建相应的组件,并返回指向所请求的接口的指针。其缺点在于利用CoCreateInstance客户无法灵活的控制创建组件的过程,CoCreateInstance完成后,组件实际上已经建立好了,在建立好一个组件之后,想要控制组件装载到内存中的何处或检查客户是否有权限来创建该组件已经不可能了。因此需要引出另一些创建组件的方法。
3. ClassFactory
类厂就是用来创建组件的组件,它唯一的功能就是创建其他组件。
实际CoCreateInstance创建的不是COM组件,而是创建了类厂。特定的类厂将创建特定的CLSID相对应的组件。
客户可以通过类厂支持的接口来对类厂创建组件的过程加以控制。创建组件的标准接口是IClassFactory.
用CoCreateInstance创建的组件实际上是通过IClassFactory.接口创建的。
用户端调用COM的库函数CoCreateInstance。 CoCreateInstance在COM架框中以CoGetClassObject实现。 CoCreateInstance会在视窗系统的Registry里搜寻所要的部件(在我们的例子中即CEmployee)。如果找到了这个部件,就会加载支持此部件的DLL。当此DLL加载成功后, CoGetClassObject就会调用DllGetClassObject。后者使用new操作符将工厂类CFactory实例化。然后DllGetClassObject会向工厂类CFactory搜询IClassFactory接口,返还给CoCreateInstance。 CoCreateInstance接下来利用IClassFactory接口调用CreateInstance函数。此时,IClassFactory::CreateInstance调用new操作符来创立所要的部件(CEmployee)。此外,它搜询IEmployee接口。在拿到接口的指针后, CoCreateInstance释放掉工厂类并把接口的指针返还给客户端。
4. CoGetClassObject(如何获得类厂的接口指针?)
CoGetClassObjectCoGetClassObject函数可以接收一个CLSID作为参数并返回相应类厂中某个接口指针。CoCreateClassObject与CoCreateInstance的区别在于后者传回的是指向所需组件的雷场而不是指向组件本身的一个接口。客户可以用它返回的指针来创建所需的组件。
CoGetClassObject函数的声明如下:
HRESULT __stdcall CoGetClassObject(
const CLSID& clsid, ///待创建组件的CLSID
DWORD dwClsContext, ///限定所创建组件的执行环境
COSERVERINFO * pServerInfo ///DCOM用来控制远程组件的
const IID& iid, ///组件上待使用接口的IID
void ** ppv ///返回指向所需组件的类厂的指针,通常是IClassFactory
);
5. IClassFactory
类厂所支持的用于创建组件的标准接口。大多数组件均可用这个接口来创建。
IClassFactory有两个成员函数,一个是CreateInstance一个是LockServer。
类厂的特性:类厂的一个实例只能创建同某一个CLSID相对应的组件;与某一个CLSID相对应的类厂是由组件开发人员实现的;
类厂可以知道并且具有关于它所创建的组件的一些特殊知识,在实现类厂时可以使用这些特殊的知识;类厂的目的是知道如何创建相对应的组件,并将这一过程封装。
CoGetClassObject需要DLL中的一个特定函数DllGetClassObject来创建类厂。客户通过调用CoGetClassObject来启动组件的创建过程。CoGetClassObject调用DLL的DllGetClassObject函数创建类厂。
类厂的创建方式由开发人员决定,类厂被创建后返回IClassFactory接口,通过此接口开发人员便可以控制创建组件了。
对于组件的注册COM库提供以下函数:DllRegisterServer (在注册表注册组件)、DllUnregisterServer(在注册表注销组件
)、DllMain (DLL的MAIN)。
DllCanUnloadNow():由COM库调用来检查是否服务器被从内存中卸载。
DllMain将模块的句柄保存在全局变量HANDLE中,供注册/注销函数DllRegisterServer/DllUnregisterServer使用。
DllRegisterServer/DllUnregisterServer用来在注册表中注册/注销某个组件。
DllGetClassObject用来创建类厂;它的声明为:
STDAPIDllGetClassObject(const CLSID& clsid, const IID& iid, void ** ppv);
将CLSID传给DllGetClassObject很有意义,使得我们可以可以在同一个DLL中实现多个组件,关键在于可以将待创建爱女的组件的CLSID传给DllClassObject。对于每一个CLSID ,DllGetClassObject可以方便地创建一个不同的类厂。这也从另外一个侧面说明了DLL不等同组件。
DLL服务器的一个与众不同的方面是控制它们被加载的时间。“标准的”DLLs被动的并且是在应用程序使用它们时被随机加载/或卸载。从技术上讲,DLL服务器也是被动的,因为不管怎样它们毕竟还是DLL,但COM库提供了一种机制,它允许某个服务器命令COM卸载它。这是通过输出函数DllCanUnloadNow()实现的。这个函数的原型如下: |
HRESULT DllCanUnloadNow(); |
当客户应用程序调用COM API CoFreeUnusedLibraries()时,通常出于其空闲处理期间,COM库遍历这个客户端应用已加载所有的DLL服务器并通过调用它的DllCanUnloadNow()函数查询每一个服务器。另一方面,如果某个服务器确定它不再需要驻留内存,它可以返回S_OK让COM将它卸载。服务器通过简单的引用计数来确定它是否能被卸载。 |
类厂实例和CLSID对应关系是一对一的。设计出色的类厂可以做到一个类厂实现完成所有的组件创建。
COM库实现一个CoFreeUnusedLibraries的函数来释放不在需要的库所占的内存。
在程序空闲时,客户周期性的调用CoFreeUnusedLibraries函数。用DllCanUnloadNow函数来确定DLL是否可以被卸载掉。LockServer可以防止当想在某个函数作用域外使用IClassFactory指针时DLL已被卸载掉了
调用LockServer(TRUE)可以锁住相应的服务器。LockServer(FALSE)可以对锁住的服务器解锁。 通过在内存中锁定服务器,即使当该服务器没有出现已经实现的组件时候,客户程序依然可以在今后使用它。进行锁定通常是基于性能的考虑。