引用计数
AddRef 和 Release 函数
当客户从组 件取 得一 个接口 时 , 此引 用计 数值 将 增 1。 当 客户 使用 完某个接口后 , 组件的引用计数值将减 1。 当引用计数值 为 0 时 , 组件 即可将自 己从内存 中删除
三个简单的原则:
- 返回之前调用AddRef,对于那些返回接口指针的函数,在返回之前应用相应的指针调用AddRef 。这些函数包括QueryInterface 及 CreateInstance。这样当客户从这种函数得到一个接口后,它将无需调用AddRef。
- 使用完接口之后调用Release,在使用完某个接口之后应调用此接口的Release 函数
- 在赋值之后调用AddRef,在将一个接口指针赋给另外一个接口指针时,应调用AddRef。在建立接口的另外一个引用之后应增加相应组件的引用计数。
肯定是,每个接口维护一个引用计数更加的清晰,更加易于调试,但同时也有一个问题,组件的生命周期任何控制。
ULONG __stdcall CA::AddRef()
{
cout << "CA: AddRef = " << m_cRef+1 << '.' << endl ;
return InterlockedIncrement(&m_cRef) ;
}
ULONG __stdcall CA::Release()
{
cout << "CA: Release = " << m_cRef-1 << '.' << endl ;
if (InterlockedDecrement(&m_cRef) == 0)
{
delete this ;
return 0 ;
}
return m_cRef ;
}
加锁的操作,主要是为了,多线程情况下的适应性。
何时进行引用计数
引用计数的优化
对引用计数进行优化,关键是找出那些生命期嵌套在引用同一接口的指针生命期内的接口指针。
引用计数规则
1. 输出参数规则
任何在输出参数中或作为返回值返回一个新的接口指针的函数必须对此接口指针调用AddRef
2.输入参数规则
对输入函数的接口指针,无需调用AddRef 和 Release
- 输入-输出参数规则
同时具有输入参数以及输出参数的功能。在函数体中可以使用输入-输出参数的值,然后可以对这些值进行修改并将其返回给调用者。
必须在给它赋另外一个接口指针值之前调用其Release。函数返回之前,必须对输出参数中所保存的接口指针调用AddRef。
- 局部变量规则
无需调用AddRef 和 Release
- 全局变量规则
对于保存在全局变量中的接口指针, 在将其传递给另外一个函数之前, 必须调用其AddRef
- 不能确定时的规则
使用AddRef 和 Release 对,需要做优化的时候,应该在优化的地方打上注释。
动态链接
DLL 只是一个组件服务器,或者说是一种发行组件的方式。组件实际上应看成是在DLL 中所实现的接口集。DLL只是一种形式,组件才是实质。
组件的创建
从DLL 中输出函数
//
// Creation function
//
extern "C" IUnknown* CreateInstance()
{
IUnknown* pI = static_cast<IX*>(new CA) ;
pI->AddRef() ;
return pI ;
}
客户和组件的划分
关于HRESULT、GUID、注册表及其他细节
HRESULT
S_OK | 成功,某些情况下,它还表示函数返回了一个布尔真值。S_OK 被定义为0 |
NOERROR | 0 |
S_FALSE | 函数成功并返回一个布尔假值,S_FALSE 被定义为1 |
E_UNEXPECTED | 无法预知的失败 |
E_NOIMPLE | 成员函数未被实现 |
E_NOINTERFACE | 组件不支持所请求的接口 |
E_OUTOFMEMORY | 组件无法分配所需的内存 |
E_FAIL | 没有指定的失败 |
设备代码 | Value | Description |
---|---|---|
FACILITY_DISPATCH | 2 | For late-binding IDispatch interface errors. |
FACILITY_ITF | 4 | 大部分的接口方法返回的状态码,其实际意义是由接口定义的。不同的接口之间的值意义可能不同 |
FACILITY_NULL | 0 | For broadly applicable common status codes such as S_OK. |
FACILITY_RPC | 1 | For status codes returned from remote procedure calls. |
FACILITY_STORAGE | 3 | For status codes returned from IStorage or IStream method calls relating to structured storage. Status codes whose code (lower 16 bits) value is in the range of MS-DOS error codes (that is, less than 256) have the same meaning as the corresponding MS-DOS error. |
FACILITY_WIN32 | 7 | Used to provide a means of handling error codes from functions in the Windows API as an HRESULT. Error codes in 16-bit OLE that duplicated system error codes have also been changed to FACILITY_WIN32. |
FACILITY_WINDOWS | 8 | Used for additional error codes from Microsoft-defined interfaces. |
如果某个具有FACILITY_WIN32 设备代码的HRESULT 值,它不在HRESULT 值列表中,它被映射为一个win32错误代码。可查找其低16 位与之相同的Win32错误码。
HRESULT 值得使用
- 多状态代码
不能直接将HRESULT 值同某个成功代码(S_OK)进行比较以决定某个函数是否成功,也不能直接将其同某个失败代码(E_FAIL)进行比较以决定函数调用是否失败。
使用SUCCEEDED 及 FAILED 宏
- 错误可能会发生变化
客户使用的组件可能会发生变化,组件返回的错误代码可能发生变化。但客户并不需要处理所有可能的错误代码,因为它不需要在遇到某个无法预料的错误代码之后继续执行。
用户自己代码的定义
当某个接口的调用者本身是一个组件并试图将所得到的成功或失败返回值传播给其客户时, 就可能会出现问题了。这是由于调用者的客户无法理解这个返回值, 因它最初是由客户并不知道的那个客户返回的。----------此时,组件应该必须将所有被本组件调用的组件返回的具有FACILITY_ITF 设备代码的值转换成客户能够识别的HRESULT 值。
定义自己的HRESULT 的一般性原则:
- 不将0x0000 及 0x01FF 范围内的值作为返回代码。这些值是为COM 所定义的FACILITY_ITF 代码所保留
- 不传播FACILITY_ITF错误代码
- 尽可能使用通用的COM 成功及失败代码
- 避免定义自己的HRESULT ,而可以在函数中使用一个输出参数。
在我们明白了HRESULT 值的一些含义:可以使用MAKE_HRESULT 宏定义一个HRESULT 值:
//
// Create an HRESULT value from component pieces
//
#define MAKE_HRESULT(sev,fac,code) \
((HRESULT) (((unsigned long)(sev)<<31) | ((unsigned long)(fac)<<16) | ((unsigned long)(code))) )
#define MAKE_SCODE(sev,fac,code) \
((SCODE) (((unsigned long)(sev)<<31) | ((unsigned long)(fac)<<16) | ((unsigned long)(code))) )
GIUD
为什么要使用GUID
extern″C″const IID IID- IX = { 0x32bb8320, 0xb41b, 0x11cf,
{ 0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82 } };
实际上I ID 是一个128 比特( 16 字节) 的一个GUID 结构。GUID 是英文Globally Unique
Identifier (全局唯一标识符) 的首字母缩写
若两个接口具有相同的标识符, 那么客户在调用QueryInterface 时将可能会得到错误的接口指针。
GUID 的声明和定义
GUID 比较
Inline BOOL operator == (const GUID& guid1,const GUID& guid2)
{
return ! memcmp(&guid1,&guid2,sizeof(GUID));
}
将GUID 作为组件标识符
COM 中用以标识组件的GUID 被称作是类标识符,为将类标识符同IID 区别,与类标识符相应的类型为CLSID。
通过引用传递GUID 值
windows 注册表
COM 库函数CoCreateInstance 取代CallCreateInstance,此函数将不再使用包含组件的DLL 名称作为参数,而是使用了一个CLSID。组件可以用CLSID 作为索引在windows 的注册表中发布包含它们的DLL 文件名称。CoCreateInstance 将用CLSID 作为关键字在注册表中查找所需的文件名称。
CLSID 关键字结构
关于注册表的其他细节
InprocServer32 子关键字包含实现组件的DLL 名称。
ProgID
ProgID 指的是程序员给某个CLSID 指定的一个程序员容易记的名称。某个计算机语言,如VB ,将使用ProgID 而不是CLSID 标识组件,但没有办法保证ProgID 的唯一性,因此名字冲突将是一个潜在的问题。但对ProgID 处理起来比较容易。(由于某些编程语言并不能处理结构,因此它们需要使用字符串格式的CLSID。)
一:ProgID 命名约定
<Program>.<Component>.<Version>
但只是约定,不是规则。
很多情况,客户不关心所连接的组件的版本。组件经常会有一个与版本无关的ProgID。此ProgID 被映射成所安装的最新版本的组件。
二:ProgID 注册表格式
HKEY_CLASSES_ROOT 下面将直接列出ProgID。由于ProgID 并不是针对最终用户而定义的,因此ProgID 关键字的缺省值为用户容易记住的名称。在ProgID 下有一个名为CLSID 的关键字,其默认值为组件的CLSID。与版本号无关的ProgID 也直接被列在HKEY_CLASSES_ROOT,它们还将有另外一个关键字CurVer,其默认值为组件当前版本的ProgID。
ProgID 到 CLSID 的转换:
COM 库中的两个函数:
CLSIDFromProgID 和 ProgIDFromCLSID。
自注册
DLL 知道它所包含的组件,因此DLL 可以完成这些信息的注册。DLL 本身并不能完成任何事情,因此在DLL 中一定要输出如下两个函数。
#define STDAPI EXTERN_C HRESULT STDAPICALLTYPE
STDAPI DllRegisterServer();
STDAPI DllUnregisterServer();
用户可以使用程序REGSVR32.EXE 来注册某个组件,它实际上是通过调用上述那些函数来完成组件的注册的。
- DllRegisterServer 的实现----其实现就是直接操作注册表
组件类别
一个组件类别实际上就是一个接口集和。该集和将被分配给一个GUID,此GUID 此时将被称作是CATID。对于某个组件而言,若它实现了某个组件类别的所有接口,那么它就可以将其注册成该组件类别的一个成员。这样,客户就能通过从注册表中选择只属于某个特定组件类别的组件而准确找到它所需要的组件。
组件类别的实现
ICatRegerster 可以完成新组件类别的登记或取消注册,它也可以将某个组件加入到某个组件类别中,可将其冲组件类别中删除。ICatInformation 则可用于获取系统中某个组件类别的数据。如:
- 系统中注册的所有类别
- 属于某个特定类别的所有组件
- 某个特定组件所属的所有类别
COM 库函数
所有的COM 组件和客户都需要完成一些相同的操作。为保证这些操作是按照标准并且兼容的方法完成的,COM定义了一个函数库以实现所有这些操作。OLE32.DLL 中。
6.4.1 COM 库的初始化
使用COM 库中的函数之前,进程必须先调用CoInitialize 来初始化COM 库函数。当进程不再需要使用COM 库函数时,必须调用CoUninitialize。这些函数的原型定义如下:
HRESULT CoInitialize(void* reserved);
void CoUnitialize();
对于每个进程,COM 库函数只需初始化一次,可以多次调用CoInitialize函数,但需要保证每一个CoInitialize 都有一个相应的CoUninitialize 调用。当进程已经调用过CoInitialize 后,再次调用此函数所得到的返回值将是S_FAULSE 而不再是S_OK。
- OIeInitialize 的使用
OLE 是建立在COM 基础上的,它增加了对类型库、剪贴板、ActiveX 文档、自动化以及ActiveX 控件的支持。在OLE 库中包含对这些特性的额外的支持。在需要使用这些特性时,应调用OleInitialize 及 OleUninitialize ,而不是CoInitialize 和CoUninitialize。最简单的办法是调用Ole* 函数然后就不再理会其他的问题了。
CoInitializeEx
在支持分布式COM(DCOM)的windows 操作系统中,可以使用CoInitializeEx 将某个组件标记为所有线程均可访问的。
2 内存管理
组件和客户的内存管理方式可能不同,此时内存的申请和释放可能导致问题。COM 提供了一个任务内存分配器。使用此分配器,组件可以给客户提供一块可以由客户删除的内存。并且COM 的任务内存分配器的设计考虑到了线程之间的同步问题,因此可以在多线程应用程序中使用。
CoGetMalloc 返回 IMalloc 接口,IMalloc::Alloc 申请内存,IMalloc::Free 释放,将IMalloc 获取的接口释放。
更加方便的函数:
Void* CoTaskMemAlloc( ULONG cb);
Void* CoTaskMemFree(void* pv);
将字符串转换成GUID
类厂
1 CoCreateInstance 的声明
/* helper for creating instances */
_Check_return_ WINOLEAPI
CoCreateInstance(
_In_ REFCLSID rclsid,
_In_opt_ LPUNKNOWN pUnkOuter,
_In_ DWORD dwClsContext,
_In_ REFIID riid,
_COM_Outptr_ _At_(*ppv, _Post_readable_size_(_Inexpressible_(varies))) LPVOID FAR * ppv
);
CoCreateInstance的使用
IX* pIX = NULL;
HRESULT hr = ::CoCreateInstance(CLSID_Component1,
NULL,
CLSCIX_INPROC_SERVER,//只装载包含进程中服务器或DLL 中的组件
IDD_IX,
(void**)&pIX);
if (SUCCEEDED(hr))
{
pIX->Fx();
pIX->Release();
}
类上下文
dwClsContext 可以控制所创建的组件是在与客户相同的进程中运行,还是在不同的进程中裕兴,或是在另一台机器上运行。
CLSCTX_INPROC_SERVER = 0x1,
| 创建在同一进程中运行的组件。为能够同客户在同一进程中运行。组件必须得是在DLL 中实现的。 |
CLSCTX_INPROC_HANDLER = 0x2,
| 创建进程中处理器。一个进程中处理器实际上是一个只实现了某个组件一部分的进程中组件。该组件的其他部分将由本地或远程服务器上的某个进程外组件实现 |
CLSCTX_LOCAL_SERVER = 0x4, | 同一机器,另外进程中运行的组件。 |
CLSCTX_REMOTE_SERVER = 0x10, | 客户希望创建一个在远程机器上运行的组件,此标志需要分布式COM正常工作 |
#define CLSCTX_ALL (CLSCTX_INPROC_SERVER| \
CLSCTX_INPROC_HANDLER| \
CLSCTX_LOCAL_SERVER| \
CLSCTX_REMOTE_SERVER)
#define CLSCTX_INPROC (CLSCTX_INPROC_SERVER|CLSCTX_INPROC_HANDLER)
#defin CLSCTX_SERVER (CLSCTX_INPROC_SERVER|CLSCTX_LOCAL_SERVER|CLSCTX_REMOTE_SERVER)
如果客户对组件所处理的执行上下文不关心,可使用上面的三个宏
客户程序清单
CoCreateInstance 的不灵活性
CoCreateInstance 不可能控制组件的创建过程,CoCreateInstance 返回后,组件实际上已经建立好了。类厂可以控制这个过程。
类厂
CoCreateInstance 创建了一个称作为类厂的组件,所需组件正是由此类厂创建的。类厂组件的唯一功能就是创建其他的组件。某个特定的类厂将创建只同某个特定的CLSID 相应的组件。客户可以通过类厂所支持的接口来对类厂创建组件的过程加以控制。创建组件的标准接口是IClassFactory ,用CoCreateInstance 创建的组件实际上是通过IClassFactory 创建的。
通过类厂创建所需的组件的步骤:1. 创建类厂本身2. 使用一个接口加IClassFactory 创建所需的组件。
CoGetClassObject
_Check_return_ WINOLEAPI
CoGetClassObject(
_In_ REFCLSID rclsid,
_In_ DWORD dwClsContext,
_In_opt_ LPVOID pvReserved,
_In_ REFIID riid,
_Outptr_ LPVOID FAR * ppv
);
与CoCreateInstance类似的参数接口,但是,CoGetClassObject 返回的是指向类厂中某个接口的指针。另外,COSERVERINFO 指针将被DCOM 用于控制对远程组件的访问。
IClassFactory
IClassFactory : public IUnknown
{
public:
virtual /* [local] */ HRESULT STDMETHODCALLTYPE CreateInstance(
/* [annotation][unique][in] */
_In_opt_ IUnknown *pUnkOuter,
/* [annotation][in] */
_In_ REFIID riid,
/* [annotation][iid_is][out] */
_COM_Outptr_ void **ppvObject) = 0;
virtual /* [local] */ HRESULT STDMETHODCALLTYPE LockServer(
/* [in] */ BOOL fLock) = 0;
};
CreateInstance 没有接受CLSID,因此只能针对一个组件进行操作。
IClassFactory2 ,在IClassFactory 的基础上增加了许可或权限功能。此时,为使类厂能够创建所需的组件,客户必须通过IClassFactory2 给类厂提供正确的关键字或许可。
CoCreateInstance 与 CoGetClassObject 的比较
1. 为什么要使用CoGetClassObject
如果想用不同于IClassFactory 的某个创建接口来创建组件,需要使用CoGetClassObject。
如果需要创建同一个组件的多个实例,使用CoGetClassObject 可以获得更高的效率。
类厂的若干特性
类厂的一个实例将只能创建同某个CLSID 相应的组件。与某个特定CLSID 相应的类厂将是由实现组件的开发人员实现的。大部分情况下,类厂组件包含在与它所创建的组件相同的DLL中。
类厂就是需要知道如何创建相应的组件并将这一点封装起来,以便客户能够尽可能同组件所具有的特殊需求分开。
类厂的实现
7.3.1 DllGetClassObject的使用
类似地, CoGetClassObject 需要DLL 中的一个特定的函数来创建组件的类厂。此函数的名称为DllGetClassObject. 。CoGetClassObject 将调用此函数完成类厂的创建.
STDAPI DllGetClassObject( const CLSID& clsid ,
const IID& iid ,
void * * ppv
);
第一个参数为类厂将要创建的组件的CLSID; 第二
个参数为类厂中客户希望得到的接口ID。而接口的指针将被返回在最后一个参数中。
组件的创建过程
组件代码清单
流程控制
5 组件的注册
DllRegisterServer 和 DllUnregisterServer 函数实现
同一DLL 中的多个组件
类厂实现的复用
CFactory 依然是与组件关联的。因为其CreateInstance 方法没有CLSID 参数。
DllGetClassObject 函数可以统一实现,实现技巧各抒己见。
DLL 的卸载
COM 库实现了一个名为CoFreeUnusedLibraries 函数,释放不再需要的库所占用的内存空间。
DllCanUnloadNow 的使用
COM 的DllCanUnloadNow 函数将告诉COM 它是否仍然在提供对任意对象的支持。若DLL 不再提供任何组件了,那些CoFreeUnusedLibraries 就可以将此DLL 卸载掉。
2 LockServer
使用上面的办法,统计的将只是DLL 提供的组件,而不包括DLL 提供的那些类厂。但这些类厂可能一直是被用着的。因此在统计组件时, 统计类厂将更为合理。对于进程中的组件服务器, 对类厂进行统计是可以的。但在第10 章中我们将引入在EXE而非DLL 中实现的本地服务器。从内部实现上讲, 进程外服务器的启动与关闭与进程中服务器的启动与关闭是不同的。由于进程外服务器的启动方式, 在不使进程外服务器永远不能释放其自身的情况下, 我们将无法对类厂进行计数。要说明的是一个正在运行的类厂并不足以保证将某个服务器装载在内存中。
。客户需要一种手段, 以防止当它想在某个函数的作用域范围之外使用一个IClassFactory 指针时DLL 被从内存中卸载掉。IClassFactory∷ LockServer 的作用正在于此。它给客户提供了一种将服务器保存在内存中、直至使用完毕的方法。此时, 客户只需调用LockServer( TRUE) 以锁住相应的服务器, 并在使用完毕之后调用LockServer ( FALSE)将其解锁。
LockServer 的实现将是非常简单的, 只需相应地增大或减小g - cComponents 计数值即可。当然许多人, 包括作者在内, 喜欢对组件和加锁使用不同的计数值。此时, DllCanUnloadNow应同时检测这两个计数值。