本文
为刚刚
接触
COM
的程序
员
提
供
编程
指南,解
释
COM
服务
器内幕以及如何用
C++
编
写自己的接口。
继 上一篇 COM 编程 入 门 之后,本文将 讨论 有 关 COM 服务 器的内容,解 释编 写自己的 COM 接口和 COM 服务 器所需要的 步骤 和知 识 ,以及 详细讨论 当 COM 库对 COM 服务 器 进 行 调 用 时 , COM 服务 器运行的内部机制。
如果你 读过 上一篇文章。 应该 很熟悉 COM 客 户 端是怎 么 会事了。本文将 讨论 COM 的另一端 ——COM 服务 器。内容包括如何用 C++ 编 写一个 简单 的不 涉 及 类库 的 COM 服务 器。深入到 创 建 COM 服务 器的内部 过 程,毫无遮掩地研究那些 库 代 码 是充分理解 COM 服务 器内部机制的最好方法。
本文假 设 你精通 C++ 并掌握了上一篇文章所 讨论 的概念和 术语 。在 这 一部分将包括如下内容:
走 马观 花看 COM 服务 器 —— 描述 COM 服务 器的基本要求。
服务 器生命其管理 —— 描述 COM 服务 器如何控制加 载时间 。
实现 接口,从 IUnknown 开 始 —— 展示如何用 C++ 类编 写一个接口 实现 并描述 IUnknown 之方法的目的。
深入 CoCreateInstance()—— 探究 CoCreateInstance() 的 调 用机理。
COM 服务 器的注册 —— 描述完成 服务 器注册所需要的注册表入口。
创 建 COM 对 象 —— 类 工厂 —— 描述 创 建客 户 端要使用的 COM 对 象的 过 程。
一个定制接口的例子 —— 例子代 码 示范了上述概念。
一个使用 服务 器的客 户 端 —— 举 例 说 明一个 简单 的客 户 端 应 用程序,用它来 测试 COM 服务 器。
其它内容 —— 有 关 源代 码 和 调试 的注 释 。
走 马观 花看 COM 服务 器
本文我 们 将 讨论 最 简单 的一 种 COM 服务 器, 进 程 内 服务 器( in-process )。 “ 进 程内 ” 意思是 服务 器被加 载 到客 户 端程序的 进 程空 间 。 进 程 内 服务 器都是 DLLs ,并且与客 户 端程序同在一台 计 算机上。
进 程 内 服务 器在被 COM 库 使用之前必 须满 足两个条件或 标 准:
1 、 必 须 正确在注册表的 HKEY_CLASSES_ROOT/CLSID 键值 下注册。
2 、 必 须输 出 DllGetClassObject() 函数。
这 是 进 程 内 服务 器运行的最小需求。在注册表的 HKEY_CLASSES_ROOT/CLSID 键值 下必 须创 建一个 键值 , 用 服务 器的 GUID 作 为键 名字, 这 个 键值 必 须 包含两个 键值 清 单 ,一 是 服务 器的位置,而是 服务 器的 线 程模型。 COM 库对 DllGetClassObject() 函数 进 行 调 用是在 CoCreateInstance() API 中完成的。
还 有三个函数通常也要 输 出:
o DllCanUnloadNow(): 由 COM 库调 用来 检查 是 否 服务 器被从内存中卸 载 。
o DllRegisterServer(): 由 类 似 RegSvr32 的安装 实 用程序 调 用来注 册 服务 器。
o DllUnregisterServer(): 由卸 载实 用程序 调 用来 删 除由 DllRegisterServer() 创 建的注册表入口。
另外,只 输 出正确的函数是不 够 的 —— 还 必 须 遵循 COM 规 范, 这样 COM 库 和客 户 端程序才能使 用 服务 器。
服务 器生命其管理
DLL 服务 器的一个与众不同的方面是控制它 们 被加 载 的 时间 。 “ 标 准的 ”DLLs 被 动 的并且是在 应 用程序使用它 们时 被随机加 载 / 或卸 载 。 从 技术 上 讲 , DLL 服务 器也是被 动 的,因 为 不管怎 样 它 们毕 尽 还 是 DLL ,但 COM 库 提供了一 种 机制,它允 许 某 个 服务 器命令 COM 卸 载 它。 这 是通 过输 出函数 DllCanUnloadNow() 实现 的。 这 个函数的原型如下:
HRESULT DllCanUnloadNow();
当客 户应 用程序 调 用 COM API CoFreeUnusedLibraries() 时 ,通常出于其空 闲处 理期 间 , COM 库 遍 历这 个客 户 端 应 用已加 载 所有的 DLL 服务 器并通 过调 用它的 DllCanUnloadNow() 函数 查询每 一 个 服务 器。另一方面,如果某个 服务 器确定它不再需要 驻 留内存,它可以返回 S_OK 让 COM 将它卸 载 。
服务 器通 过简单 的引用 计 数来确定它是否能被卸 载 。下面是 DllCanUnloadNow() 的 实现 :
extern UINT g_uDllRefCount; // 服务 器的引用 计 数
HRESULT DllCanUnloadNow()
{
return (g_uDllRefCount > 0) ? S_FALSE : S_OK;
}
如何 处 理引用 计 数将在下一 节涉 及到具体代 码时讨论 。
实现 接口,从 IUnknown 开 始
有必要回想一下 IUnknown 派生的 每 一个接口。因 为 IUnknown 包含了两个 COM 对 象的基本特性 —— 引用 计 数和接口 查询 。当你 编 写 组 件 对 象 类时 ( coclass ), 还 要写一个 满 足自己需要的 IUnknown 实现 。以 实现 IUnknown 接口的 组 件 对 象 类为 例 —— 下面 这 个例子可能是你 编 写的最 简单 的一个 组 件 对 象 类 。我 们 将在一个叫做 CUnknownImpl 的 C++ 类 中 实现 IUnknown 。下面是 这 个 类 的声明:
class CUnknownImpl : public IUnknown
{
public:
// 构造函数和析构器
CUnknownImpl();
virtual ~CUnknownImpl();
// IUnknown 方法
ULONG AddRef();
ULONG Release)();
HRESULT QueryInterface( REFIID riid, void** ppv );
protected:
UINT m_uRefCount; // 对 象的引用 计 数
};
构造器和析构器
构造器和析构器管理 服务 器的引用 计 数:
CUnknownImpl::CUnknownImpl()
{
m_uRefCount = 0;
g_uDllRefCount++;
}
CUnknownImpl::~CUnknownImpl()
{
g_uDllRefCount--;
}
当 创 建新的 COM 对 象 时 ,构造器被 调 用,它增 加 服务 器的引用 计 数以保持 这 个 服务 器 驻 留内存。同 时 它 还 将 对 象的引用 计 数初始化 为 零。当 这 个 COM 对 象被摧 毁时 ,它减 少 服务 器的引用 计 数。
AddRef() 和 Release()
这 两个方法控制 COM 对 象的生命期。 AddRef() 很 简单 :
ULONG CUnknownImpl::AddRef()
{
return ++m_uRefCount;
}
AddRef() 只增加 对 象的引用 计 数并返回更新的 计 数。
Release() 更 简单 :
ULONG CUnknownImpl::Release()
{
ULONG uRet = --m_uRefCount;
if ( 0 == m_uRefCount ) // 是否 释 放了最后的引用 ?
delete this;
return uRet;
}
除了减少 对 象的引用 计 数外,如果没有另外的明确引用, Release() 将摧 毁对 象。 Release() 也返回更新的引用 计 数。注意 Release() 的 实现 假 设 COM 对 象在堆中 创 建。如果你在全局粘上 创 建某个 对 象,当 对 象 试图删 除自己 时 就会出 问题 。
现 在 应该 明白了 为 什 么 在客 户 端 应 用程序中正确 调 用 AddRef() 和 Release() 是如此重要!如果在 这 了做得不 对 ,你使用的 对 象会被很快摧 毁 , 这样 的 话 在整 个 服务 器中内存会很快溢出 导 致 应 用程序下次存 取 服务 器代 码时 崩 溃 。
如果你 编 写多 线 程 应 用,可能会想到使用 ++& 替代 InterlockedIncrement() 和 InterlockedDecrement() 的 线 程安全 问题 。 ++&—— 用于 单线 程 服务 器很保 险 ,因 为 即使客 户 端 应 用是多 线 程的并从不同的 线 程中 进 行方法 调 用, COM 库 都会按 顺 序 进 行 服务 器的方法 调 用。也就是 说 ,一旦一个方法 调 用 开 始,所有其它 试图调 用方法的 线 程都将阻塞,直到第一个方法返回。 COM 库 本身确 保 服务 器一次不会被一个以上的 线 程 闯 入。
QueryInterface()
QueryInterface() 简 称 QI() ,由客 户 端程序 调 用 这 个函数从 COM 对 象 请 求不同的接口。我 们 在例子代 码 中因 为 只 实现 一个接口, QI() 会很容易使用。 QI() 有两个参数:一个是所 请 求的接口 IID ,一个是指 针 的 缓 冲大小,如果 查询 成功, QI() 将接口指 针 地址存 储 在 这 个 缓 冲指 针 中。
HRESULT CUnknownImpl::QueryInterface ( REFIID riid, void** ppv )
{
HRESULT hrRet = S_OK;
// 标 准 QI() 初始化 – 置 *ppv 为 NULL.
*ppv = NULL;
// 如果客 户 端 请 求提供的接口, 给 *ppv. 赋值
if ( IsEqualIID ( riid, IID_IUnknown ))
{
*ppv = (IUnknown*) this;
}
else
{
// 不提供客 户 端 请 求的接口
hrRet = E_NOINTERFACE;
}
// 如果返回一个接口指 针 。 调 用 AddRef() 增加引用 计 数 .
if ( S_OK == hrRet )
{
((IUnknown*) *ppv)->AddRef();
}
return hrRet;
}
在 QI() 中做了三件不同的事情:
1 、初始化 传 入的指 针为 NULL[*ppv = NULL;] 。
2 、 检查 riid ,确定 组 件 对 象 类 ( coclass ) 实现 了客 户 端所 请 求接口 .
[if ( IsEqualIID ( riid, IID_IUnknown ))]
3 、如果确 实实现 勒索 请 求的接口, 则 增加 COM 对 象的引用 计 数。
[((IUnknown*) *ppv)->AddRef();]
AddRef() 调 用很 关键 。
*ppv = (IUnknown*) this;
要 创 建新的 COM 对 象引用,就必 须调 用 这 个函数通知 COM 对 象 这 个新引用成立。在 AddRef() 调 用中的 强 制 转换 IUnknown* 看起来好像多余,但是在 QI() 中初始化的 *ppv 有可能不是 IUnknown* 类 型,所以最好是 养 成 习惯对 之 进 行 强 行 转换 。。
上面我 们 已 经讨论 了一些 DLL 服务 器的内部 细节 ,接下来 让 我 们 回 头 看一看当客 户 端 调 用 CoCreateInstance() 时 是如何 处 理 服务 器的。
深入 CoCreateInstance()
在本文的第一部分中,我 们见过 CoCreateInstance()API ,其作用是当客 户 端 请 求 对 象 时 ,用它来 创 建 对 象。从客 户 端的立 场 看,它是一个黑盒子。只要用正确的参数 调 用它即可得到一个 COM 对 象。它并没有什 么 魔法,只是在一个定 义 良好的 过 程中加 载 COM 服务 器, 创 建 请 求的 COM 对 象并返回所要的指 针 。就 这 些。
下面 让 我 们 来 浏览 一下 这 个 过 程。 这 里要 涉 及到几个不太熟悉的 术语 ,但不用着急,后面会 对 它 们 作 详细讨论 。
1 、客 户 端程序 调 用 CoCreateInstance() , 传递组 件 对 象 类 的 CLSID 以及所要接口的 IID 。
2 、 COM 库 在 HKEY_CLASSES_ROOT/CLSID. 键值 下 查 找 服务 器的 CLSID 键值 , 这 个 键值 包 含 服务 器的注册信息。
3 、 COM 库读 取 服务 器 DLL 的全路径并将 DLL 加 载 到客 户 端的 进 程空 间 。
4 、 COM 库调 用 在 服务 器中 DllGetClassObject() 函数 为 所 请 求的 组 件 对 象 类请 求 类 工厂。
5 、 服务 器 创 建一个 类 工厂并将它从 DllGetClassObject() 返回。
6 、 COM 库 在 类 工厂中 调 用 CreateInstance() 方法 创 建客 户 端程序 请 求的 COM 对 象。
7 、 CreateInstance() 返回一个接口指 针 到客 户 端程序。
COM 服 务 器注册
继 上一篇 COM 编程 入 门 之后,本文将 讨论 有 关 COM 服务 器的内容,解 释编 写自己的 COM 接口和 COM 服务 器所需要的 步骤 和知 识 ,以及 详细讨论 当 COM 库对 COM 服务 器 进 行 调 用 时 , COM 服务 器运行的内部机制。
如果你 读过 上一篇文章。 应该 很熟悉 COM 客 户 端是怎 么 会事了。本文将 讨论 COM 的另一端 ——COM 服务 器。内容包括如何用 C++ 编 写一个 简单 的不 涉 及 类库 的 COM 服务 器。深入到 创 建 COM 服务 器的内部 过 程,毫无遮掩地研究那些 库 代 码 是充分理解 COM 服务 器内部机制的最好方法。
本文假 设 你精通 C++ 并掌握了上一篇文章所 讨论 的概念和 术语 。在 这 一部分将包括如下内容:
走 马观 花看 COM 服务 器 —— 描述 COM 服务 器的基本要求。
服务 器生命其管理 —— 描述 COM 服务 器如何控制加 载时间 。
实现 接口,从 IUnknown 开 始 —— 展示如何用 C++ 类编 写一个接口 实现 并描述 IUnknown 之方法的目的。
深入 CoCreateInstance()—— 探究 CoCreateInstance() 的 调 用机理。
COM 服务 器的注册 —— 描述完成 服务 器注册所需要的注册表入口。
创 建 COM 对 象 —— 类 工厂 —— 描述 创 建客 户 端要使用的 COM 对 象的 过 程。
一个定制接口的例子 —— 例子代 码 示范了上述概念。
一个使用 服务 器的客 户 端 —— 举 例 说 明一个 简单 的客 户 端 应 用程序,用它来 测试 COM 服务 器。
其它内容 —— 有 关 源代 码 和 调试 的注 释 。
走 马观 花看 COM 服务 器
本文我 们 将 讨论 最 简单 的一 种 COM 服务 器, 进 程 内 服务 器( in-process )。 “ 进 程内 ” 意思是 服务 器被加 载 到客 户 端程序的 进 程空 间 。 进 程 内 服务 器都是 DLLs ,并且与客 户 端程序同在一台 计 算机上。
进 程 内 服务 器在被 COM 库 使用之前必 须满 足两个条件或 标 准:
1 、 必 须 正确在注册表的 HKEY_CLASSES_ROOT/CLSID 键值 下注册。
2 、 必 须输 出 DllGetClassObject() 函数。
这 是 进 程 内 服务 器运行的最小需求。在注册表的 HKEY_CLASSES_ROOT/CLSID 键值 下必 须创 建一个 键值 , 用 服务 器的 GUID 作 为键 名字, 这 个 键值 必 须 包含两个 键值 清 单 ,一 是 服务 器的位置,而是 服务 器的 线 程模型。 COM 库对 DllGetClassObject() 函数 进 行 调 用是在 CoCreateInstance() API 中完成的。
还 有三个函数通常也要 输 出:
o DllCanUnloadNow(): 由 COM 库调 用来 检查 是 否 服务 器被从内存中卸 载 。
o DllRegisterServer(): 由 类 似 RegSvr32 的安装 实 用程序 调 用来注 册 服务 器。
o DllUnregisterServer(): 由卸 载实 用程序 调 用来 删 除由 DllRegisterServer() 创 建的注册表入口。
另外,只 输 出正确的函数是不 够 的 —— 还 必 须 遵循 COM 规 范, 这样 COM 库 和客 户 端程序才能使 用 服务 器。
服务 器生命其管理
DLL 服务 器的一个与众不同的方面是控制它 们 被加 载 的 时间 。 “ 标 准的 ”DLLs 被 动 的并且是在 应 用程序使用它 们时 被随机加 载 / 或卸 载 。 从 技术 上 讲 , DLL 服务 器也是被 动 的,因 为 不管怎 样 它 们毕 尽 还 是 DLL ,但 COM 库 提供了一 种 机制,它允 许 某 个 服务 器命令 COM 卸 载 它。 这 是通 过输 出函数 DllCanUnloadNow() 实现 的。 这 个函数的原型如下:
HRESULT DllCanUnloadNow();
当客 户应 用程序 调 用 COM API CoFreeUnusedLibraries() 时 ,通常出于其空 闲处 理期 间 , COM 库 遍 历这 个客 户 端 应 用已加 载 所有的 DLL 服务 器并通 过调 用它的 DllCanUnloadNow() 函数 查询每 一 个 服务 器。另一方面,如果某个 服务 器确定它不再需要 驻 留内存,它可以返回 S_OK 让 COM 将它卸 载 。
服务 器通 过简单 的引用 计 数来确定它是否能被卸 载 。下面是 DllCanUnloadNow() 的 实现 :
extern UINT g_uDllRefCount; // 服务 器的引用 计 数
HRESULT DllCanUnloadNow()
{
return (g_uDllRefCount > 0) ? S_FALSE : S_OK;
}
如何 处 理引用 计 数将在下一 节涉 及到具体代 码时讨论 。
实现 接口,从 IUnknown 开 始
有必要回想一下 IUnknown 派生的 每 一个接口。因 为 IUnknown 包含了两个 COM 对 象的基本特性 —— 引用 计 数和接口 查询 。当你 编 写 组 件 对 象 类时 ( coclass ), 还 要写一个 满 足自己需要的 IUnknown 实现 。以 实现 IUnknown 接口的 组 件 对 象 类为 例 —— 下面 这 个例子可能是你 编 写的最 简单 的一个 组 件 对 象 类 。我 们 将在一个叫做 CUnknownImpl 的 C++ 类 中 实现 IUnknown 。下面是 这 个 类 的声明:
class CUnknownImpl : public IUnknown
{
public:
// 构造函数和析构器
CUnknownImpl();
virtual ~CUnknownImpl();
// IUnknown 方法
ULONG AddRef();
ULONG Release)();
HRESULT QueryInterface( REFIID riid, void** ppv );
protected:
UINT m_uRefCount; // 对 象的引用 计 数
};
构造器和析构器
构造器和析构器管理 服务 器的引用 计 数:
CUnknownImpl::CUnknownImpl()
{
m_uRefCount = 0;
g_uDllRefCount++;
}
CUnknownImpl::~CUnknownImpl()
{
g_uDllRefCount--;
}
当 创 建新的 COM 对 象 时 ,构造器被 调 用,它增 加 服务 器的引用 计 数以保持 这 个 服务 器 驻 留内存。同 时 它 还 将 对 象的引用 计 数初始化 为 零。当 这 个 COM 对 象被摧 毁时 ,它减 少 服务 器的引用 计 数。
AddRef() 和 Release()
这 两个方法控制 COM 对 象的生命期。 AddRef() 很 简单 :
ULONG CUnknownImpl::AddRef()
{
return ++m_uRefCount;
}
AddRef() 只增加 对 象的引用 计 数并返回更新的 计 数。
Release() 更 简单 :
ULONG CUnknownImpl::Release()
{
ULONG uRet = --m_uRefCount;
if ( 0 == m_uRefCount ) // 是否 释 放了最后的引用 ?
delete this;
return uRet;
}
除了减少 对 象的引用 计 数外,如果没有另外的明确引用, Release() 将摧 毁对 象。 Release() 也返回更新的引用 计 数。注意 Release() 的 实现 假 设 COM 对 象在堆中 创 建。如果你在全局粘上 创 建某个 对 象,当 对 象 试图删 除自己 时 就会出 问题 。
现 在 应该 明白了 为 什 么 在客 户 端 应 用程序中正确 调 用 AddRef() 和 Release() 是如此重要!如果在 这 了做得不 对 ,你使用的 对 象会被很快摧 毁 , 这样 的 话 在整 个 服务 器中内存会很快溢出 导 致 应 用程序下次存 取 服务 器代 码时 崩 溃 。
如果你 编 写多 线 程 应 用,可能会想到使用 ++& 替代 InterlockedIncrement() 和 InterlockedDecrement() 的 线 程安全 问题 。 ++&—— 用于 单线 程 服务 器很保 险 ,因 为 即使客 户 端 应 用是多 线 程的并从不同的 线 程中 进 行方法 调 用, COM 库 都会按 顺 序 进 行 服务 器的方法 调 用。也就是 说 ,一旦一个方法 调 用 开 始,所有其它 试图调 用方法的 线 程都将阻塞,直到第一个方法返回。 COM 库 本身确 保 服务 器一次不会被一个以上的 线 程 闯 入。
QueryInterface()
QueryInterface() 简 称 QI() ,由客 户 端程序 调 用 这 个函数从 COM 对 象 请 求不同的接口。我 们 在例子代 码 中因 为 只 实现 一个接口, QI() 会很容易使用。 QI() 有两个参数:一个是所 请 求的接口 IID ,一个是指 针 的 缓 冲大小,如果 查询 成功, QI() 将接口指 针 地址存 储 在 这 个 缓 冲指 针 中。
HRESULT CUnknownImpl::QueryInterface ( REFIID riid, void** ppv )
{
HRESULT hrRet = S_OK;
// 标 准 QI() 初始化 – 置 *ppv 为 NULL.
*ppv = NULL;
// 如果客 户 端 请 求提供的接口, 给 *ppv. 赋值
if ( IsEqualIID ( riid, IID_IUnknown ))
{
*ppv = (IUnknown*) this;
}
else
{
// 不提供客 户 端 请 求的接口
hrRet = E_NOINTERFACE;
}
// 如果返回一个接口指 针 。 调 用 AddRef() 增加引用 计 数 .
if ( S_OK == hrRet )
{
((IUnknown*) *ppv)->AddRef();
}
return hrRet;
}
在 QI() 中做了三件不同的事情:
1 、初始化 传 入的指 针为 NULL[*ppv = NULL;] 。
2 、 检查 riid ,确定 组 件 对 象 类 ( coclass ) 实现 了客 户 端所 请 求接口 .
[if ( IsEqualIID ( riid, IID_IUnknown ))]
3 、如果确 实实现 勒索 请 求的接口, 则 增加 COM 对 象的引用 计 数。
[((IUnknown*) *ppv)->AddRef();]
AddRef() 调 用很 关键 。
*ppv = (IUnknown*) this;
要 创 建新的 COM 对 象引用,就必 须调 用 这 个函数通知 COM 对 象 这 个新引用成立。在 AddRef() 调 用中的 强 制 转换 IUnknown* 看起来好像多余,但是在 QI() 中初始化的 *ppv 有可能不是 IUnknown* 类 型,所以最好是 养 成 习惯对 之 进 行 强 行 转换 。。
上面我 们 已 经讨论 了一些 DLL 服务 器的内部 细节 ,接下来 让 我 们 回 头 看一看当客 户 端 调 用 CoCreateInstance() 时 是如何 处 理 服务 器的。
深入 CoCreateInstance()
在本文的第一部分中,我 们见过 CoCreateInstance()API ,其作用是当客 户 端 请 求 对 象 时 ,用它来 创 建 对 象。从客 户 端的立 场 看,它是一个黑盒子。只要用正确的参数 调 用它即可得到一个 COM 对 象。它并没有什 么 魔法,只是在一个定 义 良好的 过 程中加 载 COM 服务 器, 创 建 请 求的 COM 对 象并返回所要的指 针 。就 这 些。
下面 让 我 们 来 浏览 一下 这 个 过 程。 这 里要 涉 及到几个不太熟悉的 术语 ,但不用着急,后面会 对 它 们 作 详细讨论 。
1 、客 户 端程序 调 用 CoCreateInstance() , 传递组 件 对 象 类 的 CLSID 以及所要接口的 IID 。
2 、 COM 库 在 HKEY_CLASSES_ROOT/CLSID. 键值 下 查 找 服务 器的 CLSID 键值 , 这 个 键值 包 含 服务 器的注册信息。
3 、 COM 库读 取 服务 器 DLL 的全路径并将 DLL 加 载 到客 户 端的 进 程空 间 。
4 、 COM 库调 用 在 服务 器中 DllGetClassObject() 函数 为 所 请 求的 组 件 对 象 类请 求 类 工厂。
5 、 服务 器 创 建一个 类 工厂并将它从 DllGetClassObject() 返回。
6 、 COM 库 在 类 工厂中 调 用 CreateInstance() 方法 创 建客 户 端程序 请 求的 COM 对 象。
7 、 CreateInstance() 返回一个接口指 针 到客 户 端程序。
COM 服 务 器注册
COM
服
务
器必
须
在
Windows
注册表中正确注册以后才能正常工作。如果你看一下注册表中的
HKEY_CLASSES_ROOT/CLSID
键
,就会
发现
大把大把子
键
,它
们
就是在
这
个
计
算机上注册的
COM
服
务
器。当某个
COM
服
务
器注册后(通常是用
DllRegisterServer()
进
行注册),就会以
标
准的注册表格式在
CLSID
键
下
创
建一个
键
,它名字
为
服
务
器的
GUID
。下面是一个
这样
的例子:
{067DF822-EAB6-11cf-B56E-00A0244D5087}
大括弧和
连
字符是必不可少的,字母大小写均可。
这
个
键
的默
认值
是人可
值别
的
组
件
对
象
类
名,使用
VC
所
带
的
OLE/COM
对
象
浏览
器可以察看到它
们
。
在
GUID
键
的子
键
中
还
可以存
储
其它信息。需要
创
建什
么
子
键
依
赖
于
COM
服
务
器的
类
型以及
COM
服
务
器的使用方法。
对
于本文例子中
这
个
简单
的
进
程内服
务
器,我
们值
需要一个子
键
:
InProcServer32
。
InProcServer32
键
包含两个串:
这
两个串的缺省
值
是服
务
器
DLL
的全路径和
线
程模型
值
(
ThreadingModel
)。
线
程模型超出了本文所
涉
及的范
围
,我
们
先接受
这
个概念,
这
里我
们
指的是
单线
程服
务
器,用的模式
为
Apartment
(即
单线
程公寓)。
创
建
COM
对
象
——
类
工厂
回首看一看客
户
端的
COM
,它是如何以自己独立于
语
言的方式
创
建和
销毁
COM
对
象。客
户
端
调
用
CoCreateInstance()
创
建新的
COM
对
象。
现
在我
们
来看看它在服
务
器端是如何工作的。
你
每
次
实现组
件
对
象
类
的
时
候,都要写一个旁
类负责创
建第一个
组
件
对
象
类
的
实
例。
这
个旁
类
就叫
这
个
组
件
对
象
类
的
类
工厂(
class factory
),其唯一目的是
创
建
COM
对
象。之所以要一个
类
工厂,是因
为语
言无
关
的
缘
故。
COM
本身并不
创
建
对
象,因
为
它不是独立于
语
言的也不是独立于
实现
的。
当某个客
户
端想要
创
建一个
COM
对
象
时
,
COM
库
就从
COM
服
务
器
请
求
类
工厂。然后
类
工厂
创
建
COM
对
象并将它返回客
户
端。它
们
的通
讯
机制由函数
DllGetClassObject()
来提供。
术语
“
类
工厂
”
和
“
类对
象
”
实际
上是一回事。没有那个
单词
能精确描述
类
工厂的作用和
义
,但正是
这
个工厂
创
建了
COM
对
象,而不是
COM
类
所
为
。将
“
类
工厂
”
理解成
“
对
象工厂
”
可能会更有助于理解(
实际
上
MFC
就是
这样
理解的
——
它的
类
工厂
实现
就叫做
COleObjectFactory
)。但
“
类
工厂
”
是正式
术语
,所以本文也
这样
用。
当
COM
库调
用
DllGetClassObject()
时
,它
传递
客
户
端
请
求的
CLSID
。服
务
器
负责为
所
请
求的
CLSID
创
建者各
类
工厂并将它返回。
类
工厂本身就是一个
组
件
对
象
类
,并且
实现
IClassFactory
接口。如果
DllGetClassObject()
调
用成功,它返回一个
IClassFactory
指
针给
COM
库
,然后
COM
库
用
IClassFactory
接口方法
创
建客
户
端所
请
求的
COM
对
象
实
例。
一下是IClassFactory接口:
struct IClassFactory : public IUnknown
{
HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppvObject );
HRESULT LockServer( BOOL fLock );
};
其中,
CreateInstance()
是
创
建
COM
对
象的方法。
LockServer()
在必要
时让
COM
库
增加或减少服
务
器的引用
计
数。
一个定制接口的例子
这
个工程是一个能运行的
DLL
服
务
器例子,
对
象由
类
工厂
创
建,此
DLL
服
务
器在
CSimpleMsgBoxImpl
组
件
对
象
类
中
实现
了一个接口:
ISimpleMsgBox
。
接口定
义
我
们
的新接口是
ISimpleMsgBox
。所有的接口多必
须
从
IUnknown
派生。
这
个接口只有一个方法
:
DoSimpleMsgBox()
。注意它返回
标
准
类
型
HRESULT
。所有的方法都
应该
返回
HRESULT
类
型,并且所有返回到
调
用者的其它数据都
应该
通
过
指
针
参数操作。
struct ISimpleMsgBox : public IUnknown
{
// IUnknown
方法
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface( REFIID riid, void** ppv );
// ISimpleMsgBox
方法
HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );
};
struct __declspec(uuid("{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}")) ISimpleMsgBox;
有
__declspec
的一行将一个
GUID
赋值给
ISimpleMsgBox
,并且以后可以用
__uuidof
操作符来
获
取
GUID
。
这
两个
东
西都是微
软
的
C++
的
扩
展。
DoSimpleMsgBox()
的第二个参数是
BSTR
类
型。意思是二
进
制串
——
即定
长
序列位的
COM
表示。
BSTRs
主要用于
Visual Basic
和
Windows Scripting Host
之
类
的脚本客
户
端。
接下来
这
个接口由
CSimpleMsgBoxImpl C++
类
来
实现
。其定
义
如下:
class CSimpleMsgBoxImpl : public ISimpleMsgBox
{
public:
CSimpleMsgBoxImpl();
virtual ~CSimpleMsgBoxImpl();
// IUnknown
方法
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface( REFIID riid, void** ppv );
// ISimpleMsgBox
方法
HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );
protected:
ULONG m_uRefCount;
};
class __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}")) CSimpleMsgBoxImpl;
当某一客
户
端想要
创
建一个
SimpleMsgBox COM
对
象
时
,它
应该
用下面
这样
的代
码
:
ISimpleMsgBox* pIMsgBox;
HRESULT hr;
//
组
件
对
象
类
的
CLSID
hr = CoCreateInstance ( __uuidof(CSimpleMsgBoxImpl), NULL, //
非聚合
CLSCTX_INPROC_SERVER, //
进
程内服
务
器
__uuidof(ISimpleMsgBox), //
所
请
求接口的
IID
(void**) &pIMsgBox ); //
返回的接口指
针
的地址
类
工厂
实现
我
们
的
类
工厂
SimpleMsgBox
是在一个叫做
CSimpleMsgBoxClassFactory
的
C++
类
中
实现
的:
class CSimpleMsgBoxClassFactory : public IClassFactory
{
public:
CSimpleMsgBoxClassFactory();
virtual ~CSimpleMsgBoxClassFactory();
// IUnknown
方法
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface( REFIID riid, void** ppv );
// IClassFactory
方法
HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, void** ppv );
HRESULT LockServer( BOOL fLock );
protected:
ULONG m_uRefCount;
};
构造函数、析构函数和
IUnknown
方法都和前面例子中的一
样
,不同的只有
IClassFactory
的方法,
LockServer()
,看起来相当更
简单
:
HRESULT CSimpleMsgBoxClassFactory::LockServer ( BOOL fLock )
{
fLock ? g_uDllLockCount++ : g_uDllLockCount--;
return S_OK;
}
CreateInstance()
是重点。我
们说过这
个方法
负责创
建新的
CSimpleMsgBoxImpl
对
象。
让
我
们进
一
步
探
讨
一下它的原型和参数:
HRESULT CSimpleMsgBoxClassFactory::CreateInstance ( IUnknown* pUnkOuter,
REFIID riid,
void** ppv );
第一个参数
pUnkOuter
只用于聚合的新
对
象,指向
“
外部的
”COM
对
象,也就是
说
,
这
个
“
外部
”
对
象将包含此新
对
象。
对
象的聚合超出了
本文的
讨论
范
围
,本文的例子
对
象也不支持聚合。
riid
和
ppv
与在
QueryInterface()
中的用法一
样
——
它
们
是客
户
端所
请
求的接口
IID
和存
储
接口指
针
的指
针缓
冲。
下面是
CreateInstance()
的
实现
。它从参数的有效性
检查
和参数的初始化
开
始。
HRESULT CSimpleMsgBoxClassFactory::CreateInstance ( IUnknown* pUnkOuter,
REFIID riid,
void** ppv )
{
//
因
为
不支持聚合,所以
这
个参数
pUnkOuter
必
须为
NULL.
if ( NULL != pUnkOuter )
return CLASS_E_NOAGGREGATION;
//
检查
指
针
ppv
是不是
void*
类
型
if ( IsBadWritePtr ( ppv, sizeof(void*) ))
return E_POINTER;
*ppv = NULL;
检查
完参数的有效性后,就可以
创
建一个新的
对
象了。
CSimpleMsgBoxImpl* pMsgbox;
//
创
建一个新的
COM
对
象
pMsgbox = new CSimpleMsgBoxImpl;
if ( NULL == pMsgbox )
return E_OUTOFMEMORY;
最后,用
QI()
来
查询
客
户
端所
请
求的新
对
象的接口。如果
QI()
失
败
,
则这
个
对
象不可用,必
须删
除它。
HRESULT hrRet;
//
用
QI
查询
客
户
端所
请
求的
对
象接口
hrRet = pMsgbox->QueryInterface ( riid, ppv );
//
如果
QI
失
败
,
则删
除
这
个
COM
对
象,因
为
客
户
端不能使用它(客
户
端没有
//
这
个
对
象的任何接口)
if ( FAILED(hrRet) )
delete pMsgbox;
return hrRet;
}
深入DllGetClassObject()
现
在
让
我
们
深入
DllGetClassObject()
内部。它的原型是:
HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv );
rclsid
是客
户
端所
请
求的
组
件
对
象
类
的
CLSID
。
这
个函数必
须
返回指定
组
件
对
象
类
的
类
工厂。
这
里的两个参数:
riid
和
ppv
类
似
QI()
的参数。不
过
在
这
个函数中,
riid
指的是
COM
库
所
请
求的
类
工厂
接口的
IID
。通常就是
IID_IClassFactory
。
因
为
DllGetClassObject()
也
创
建一个新的
COM
对
象(
类
工厂),所以代
码
与
IClassFactory::CreateInstance()
十分相似。
开
始也是
进
行一些有效性
检查
以及初始化。
HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv )
{
//
检查
客
户
端所要的
CSimpleMsgBoxImpl
类
工厂
if ( !InlineIsEqualGUID ( rclsid, __uuidof(CSimpleMsgBoxImpl) ))
return CLASS_E_CLASSNOTAVAILABLE;
//
检查
指
针
ppv
是不是
void*
类
型
if ( IsBadWritePtr ( ppv, sizeof(void*) ))
return E_POINTER;
*ppv = NULL;
第一个
if
语
句
检查
rclsid
参数。我
们
的服
务
器只有一个
组
件
对
象
类
,所以
rclsid
必
须
是
CSimpleMsgBoxImpl
类
的
CLSID
。
__uuidof
操作符
获
取先前在
__declspec(uuid())
声明中指定的
CsimpleMsgBoxImpl
类
的
GUID
。
下一
步
是
创
建一个
类
工厂
对
象。
CSimpleMsgBoxClassFactory* pFactory;
//
构造一个新的
类
工厂
对
象
pFactory = new CSimpleMsgBoxClassFactory;
if ( NULL == pFactory )
return E_OUTOFMEMORY;
这
里的
处
理与
CreateInstance()
中所做的有所不同。在
CreateInstance()
中是
调
用了
QI()
,并且如果
调
用失
败
,
则删
除
COM
对
象。
我
们
可以把自己假
设
成一个所
创
建的
COM
对
象的客
户
端,
调
用
AddRef()
进
行一次引用
计
数(
COUNT = 1
)。然后
调
用
QI()
。如果
QI()
调
用成功,它将再一次用
AddRef()
进
行引用
计
数(
COUNT = 2
)。如果
QI()
调
用失
败
。引用
计
数将保持
为
原来的
值
(
COUNT = 1
)。
在
QI()
调
用之后,
类
工厂
对
象就使用完了,因此要
调
用
Release()
来
释
放它。如
果
QI()
调
用失
败
,
这
个
对
象将自我
删
除(因
为
引用
计
数将
为
零),所以最
终结
果是一
样
的。
//
调
用
AddRef()
增加一个
类
工厂引用
计
数,因
为
我
们
正在使用它
pFactory->AddRef();
HRESULT hrRet;
//
调
用
QI()
查询
客
户
端所要的
类
工厂接口
hrRet = pFactory->QueryInterface ( riid, ppv );
//
使用完
类
工厂后
调
用
Release()
释
放它
pFactory->Release();
return hrRet;
}
再
谈
QueryInterface()
前面
讨论过
QI()
的
实现
,但
还
是有必要再看一看
类
工厂的
QI()
,因
为
它是一个很
现实
的例子,其中
COM
对
象
实现
的不光是
IUnknown
。首先
进
行的是
对
ppv
缓
冲的有效性
检查
以及初始化。
HRESULT CSimpleMsgBoxClassFactory::QueryInterface( REFIID riid, void** ppv )
{
HRESULT hrRet = S_OK;
//
检查
指
针
ppv
是不是
void*
类
型
if ( IsBadWritePtr ( ppv, sizeof(void*) ))
return E_POINTER;
//
标
准的
QI
初始化,将
赋值为
NULL.
*ppv = NULL;
接下来
检查
riid
,看看它是不是
类
工厂
实现
的接口之一:
IUnknown
或
IclassFactory
。
//
如果客
户
端
请
求一个有效接口,
则
扶植
给
*ppv.
if ( InlineIsEqualGUID ( riid, IID_IUnknown ))
{
*ppv = (IUnknown*) this;
}
else if ( InlineIsEqualGUID ( riid, IID_IClassFactory ))
{
*ppv = (IClassFactory*) this;
}
else
{
hrRet = E_NOINTERFACE;
}
最后,如果
riid
是有效接口,
则调
用接口的
AddRef()
,然后返回。
//
如果返回有效接口指
针
,
则调
用
AddRef()
if ( S_OK == hrRet )
{
((IUnknown*) *ppv)->AddRef();
}
return hrRet;
}
ISimpleMsgBox
实现
最后的也是必不可少的一
关
是
ISimpleMsgBox
实现
,我
们
的代
码
只
实现
ISimpleMsgBox
的方法
DoSimpleMsgBox()
。首先用微
软
的
扩
展
类
_bstr_t
将
bsMessageText
转换
成
TCHAR
串。
HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent, BSTR bsMessageText )
{
_bstr_t bsMsg = bsMessageText;
LPCTSTR szMsg = (TCHAR*) bsMsg; //
如果需要的
话
,用
_bstr_t
将串
转换为
ANSI
做完
转换
的工作后,
显
示信息框,然后返回。
MessageBox ( hwndParent, szMsg, _T("Simple Message Box"), MB_OK );
return S_OK;
}
使用服
务
器的客
户
端
我
们
已
经
完成了一个超
级
棒的
COM
服
务
器,如何使用它呢
?
我
们
的接口一个定制接口,也就是
说
它只能被
C
或
C++
客
户
端使用。(如果在
组
件
对
象
类
中同
时实现
IDispatch
接口,那我
们
几乎就可以在任何客
户
端
环
境中
——Visual Basic
,
Windows Scripting Host
,
Web
页
面,
PerlScript
等使用
COM
对
象。有
关这
方面的内容我
们
留待另外的文章
讨论
)。本文提供了一个使用
ISimpleMsgBox
的例子程序。
这
个程序基于用
Win32
应
用程序向
导
建立的
Hello World
例子。文件菜
单
包含两个
测试
服
务
器的命令:
如
图
所示:
Test MsgBox COM Server
菜
单
命令
创
建
CSimpleMsgBoxImpl
对
象并
调
用
DoSimpleMsgBox()
。因
为这
是个
简单
的方法,要写的代
码
不
长
。
我
们
先用
CoCreateInstance()
创
建一个
COM
对
象。
void DoMsgBoxTest(HWND hMainWnd)
{
ISimpleMsgBox* pIMsgBox;
HRESULT hr;
hr = CoCreateInstance ( __uuidof(CSimpleMsgBoxImpl), //
组
件
对
象
类
的
CLSID
NULL, //
非聚合
CLSCTX_INPROC_SERVER, //
只使用
进
程内服
务
器
__uuidof(ISimpleMsgBox), //
所
请
求接口的
IID
(void**) &pIMsgBox ); //
容
纳
接口指
针
的
缓
冲
if ( FAILED(hr) )
return;
然后
调
用
DoSimpleMsgBox()
方法并
释
放接口。
pIMsgBox->DoSimpleMsgBox ( hMainWnd, _bstr_t("Hello COM!") );
pIMsgBox->Release();
}
就
这么简单
。代
码
中从
头
到尾都有
TRACE
语
句,
这样
在
调试
器中运行
测试
程序就可以看到服
务
器的
每
一个方法
是如何被
调
用的。
另外一个菜
单
命令是
调
用
CoFreeUnusedLibraries()
函数,从中你能看到服
务
器
DllCanUnloadNow()
函数的运行。
其它
细节
-COM
宏
COM
代
码
中有些宏
隐
藏了
实现细节
,并允
许
在
C
和
C++
客
户
端使用相同的声明。本文中没有使用宏,但在例子代
码
中用到了
这
些宏,所以必
须
掌握它
们
的用法。下面是
ISimpleMsgBox
的声明
struct ISimpleMsgBox : public IUnknown
{
// IUnknown
方法
STDMETHOD_(ULONG, AddRef)() PURE;
STDMETHOD_(ULONG, Release)() PURE;
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;
// ISimpleMsgBox
方法
STDMETHOD(DoSimpleMsgBox)(HWND hwndParent, BSTR bsMessageText) PURE;
};
STDMETHOD()
包含
virtual
关键
字,返回
类
型和
调
用
规
范。
STDMETHOD_()
也一
样
,除非你指定不
同的返回
类
型。
PURE
扩
展了
C++
的
“=0”
,使此函数成
为
一个
纯
虚
拟
函数。
STDMETHOD()
和
STDMETHOD_()
有
对应
的宏用于方法
实现
——STDMETHODIMP
和
STDMETHODIMP_()
。
例如
DoSimpleMsgBox()
的
实现
:
STDMETHODIMP CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent, BSTR bsMessageText )
{
...
}
最后,
标
准的
输
出函数用
STDAPI
宏声明,如:
STDAPI DllRegisterServer()
STDAPI
包括返回
类
型和
调
用
规
范。要注意
STDAPI
不能和
__declspec(dllexport)
一起使用,
因
为
STDAPI
的
扩
展。
输
出必
须
使用
.DEF
文件。
服
务
器注册以及反注册
前面
讲过
服
务
器
实现
了
DllRegisterServer()
和
DllUnregisterServer()
两个函数。它
们
的工作是
创
建和
删
除
关
于
COM
服
务
器的注册表入口。其代
码
都是
对
注册表的
处
理,所以在此不必
赘
言,只是列出
DllRegisterServer()
创
建的注册表入口:
键
名
键值
HKEY_CLASSES_ROOT
CLSID
{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}
Default="SimpleMsgBox class"
InProcServer32
Default=[path to DLL]; ThreadingModel="Apartment"
关
于例子代
码
的注
释
本文的例子代
码
在一个
WORKSPACE
(工作
间
)文件中(
SimpleComSvr.dsw
)同
时
包含了服
务
器的源代
码
和
测试
服
务
器所用的客
户
端源代
码
。在
VC
的
IDE
环
境中可以同
时
加
载
它
们进
行
处
理。在工作
间
的同
级层
次有两个工程都要
用到的
头
文件,但
每
个工程都有自己的子目
录
。
同
级
的公共
头
文件是:
ISimpleMsgBox.h——
定
义
ISimpleMsgBox
的
头
文件。
SimpleMsgBoxComDef.h——
包含
__declspec(uuid())
的声明。
这
些声明都在
单
独的文件中,因
为
客
户
端需要
CSimpleMsgBoxImpl
的
GUID
,不是它的定
义
。将
GUID
移到
单
独的文件中,使客
户
端在存取
GUID
时
不依
赖
CSimpleMsgBoxImpl
的内部
结
构
。它是接口,
ISimpleMsgBox
,
对
客
户
端很重要。
正如前面所
说
的,必
须
用
.DEF
文件来从服
务
器
输
出四个
标
准的
输
出函数。下面是例子工程的
.DEF
文件:
EXPORTS
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
每
一行都包含函数名和
PRIVATE
关键
字。
这
个
关键
字的意思是:此函数是
输
出函数,但不包含在
输
入
库
(
import lib
)中。也就是
说
客
户
端不能直接从代
码
中
调
用
这
个函数,即使是
链
接了
输
入
库
也不行。
这
个
关键
字
时
必
须
要用的,否
则链
接器会出
错
。
在服
务
器中
设
置断点
链
如果你想在服
务
器代
码
中
设
置断点,有两
种
方法:第一
种
是将服
务
器工程
(MsgBoxSvr)
设
置
为
活
动
工程,然后
开
始
调试
。
MSVC
将
问
你
调试
会
话
要运行的可
执
行程序。
输
入客
户
端
测试
程序的全路径,你必
须
事先建立好。第二
种
方法是将客
户
端工程
(TestClient)
设
置
为
活
动
工程,配置工程的从属(
dependencies
)属性,以便服
务
器工程从属于客
户
端工程。
这样
如果你改
变
了服
务
器的代
码
,那
么
在
编译
客
户
端工程
时
会自
动
重新
编译
服
务
器工程代
码
。最后
还
要做的是当你
开
始
调试
客
户
端
时
必
须
告
诉
MSVC
加
载
服
务
器符号(
symbols
)。
下面是
设
置工程属性的
对话
框:
Project->Dependencies
菜
单
为
了加
载
服
务
器符号,打
开
TestClient
的工程
设
置(
Project->Settings
菜
单
),
选择
Debug
标签
,并在
Category
组
合框中
选择
Additional DLLs
。在列表框中
单击
New
一个入口,然后
输
入服
务
器
DLL
的全路径名。
如下
图
所示:
这样设
置以后,根据
实际
源代
码
的所在位置,
DLL
的路径将会做自
动调
整
。