文章目录
1. 本文的目的
2. 介绍
3. 快速浏览COM服务器
4. 服务器生命周期管理
5. 实现接口,从IUnknown开始
6. CoCreateInstance()的内部
7. 注册COM服务器
8. 创建COM对象 - 类工厂
9. 示例自定义接口
10. 客户端使用我们的COM服务
11. 其他细节
本篇文章为翻译文章,适合像我一样,之前从来没有接触过COM编程的人,如果翻译的有什么不足之处,希望大家多多指出。
原文链接:
Introduction to COM Part II - Behind the Scenes of a COM Server - CodeProject
本篇文章为译文的第二部分,第一部分链接:
COM编程入门Part Ⅰ- 什么是COM和如何使用COM [译]
源代码下载地址:
https://download.csdn.net/download/douzhq/13625106
下面为译文部分:
这是一个面向COM新手程序员的教程,解释了COM服务器的内部原理,以及如何用c++编写自己的接口。
1. 本文的目的
和我的第一篇介绍COM的文章一样,我写的这个教程是为那些刚开始使用COM并且需要一些帮助来理解基础知识的程序员编写的。本文从服务器端介绍了COM,解释了编写自己的COM接口和COM服务器所需的步骤,以及详细描述了COM库调用COM服务器时在COM服务器中具体发生了什么。
2. 介绍
如果你读过我的第一篇介绍COM的文章,你应该很熟悉使用COM作为客户端所涉及的内容。现在是时候从另一端——COM服务器——接近COM了。我将介绍如何在不涉及类库的普通c++中从头开始编写COM服务器。虽然这不是现在通常采用的方法,但是查看所有用于创建COM服务器的代码——没有任何东西隐藏在预先构建的库中——确实是完全理解服务器中发生的所有事情的最好方法。
本文假设您熟练使用C++,并理解第一篇介绍COM的文章中涉及的概念和术语。这篇文章的将介绍如下几个部分:
- 快速浏览COM服务器 - 描述COM服务器的基本要求
- 服务器生命周期管理 - 描述COM服务器如何控制它的加载时间。
- 实现接口,从IUnknown开始 - 演示如何在C++类中编写接口的实现,并描述 IUnknown 方法的用途。
- CoCreateInstance()的内部 - 概述调用 CoCreateInstance() 时会发生什么。
- 注册COM服务器 - 描述正确注册COM服务器所需的注册表项。
- 创建COM对象 - 类工厂 - 描述为客户端程序创建要使用的COM对象的过程。
- 示例自定义接口 - 一些示例代码,演示了前面几节中的概念。
- 客户端使用我们的服务器 - 演示一个简单的客户端应用程序,我们可以使用它来测试服务器。
- 其他说明 - 关于源代码和调试的说明。
3. 快速浏览COM服务器
在本文中,我们将介绍最简单的COM服务器类型,即进程内(in-process)服务器。“进程内”是指服务器被加载到客户端程序的进程空间中。进程内服务器总是dll,并且必须与客户端程序在同一台计算机上。
一个程序内的服务器必须满足两个条件,它才能被作为COM库使用:
- 必须在注册表 HKEY_CLASSES_ROOT\CLSID 键值下正确的注册。
- 它必须导出一个名为 DllGetClassObject() 的函数。
这是让进程内服务器工作所需要做的最少的事情。必须在 HKEY_CLASSES_ROOT\CLSID 键下创建一个名称为服务器GUID的键,该键必须包含一对值的列表, 包括COM服务器位置和它的线程模式。 DllGetClassObject() 函数由COM库调用,作为 CoCreateInstance() API所做工作的一部分。
通常也会导出其他三个函数:
- DllCanUnloadNow(): 由COM库调用,以查看服务器是否可以从内存中卸载。
- DllRegisterServer(): 由安装程序(比如RegSvr32)调用,让服务器注册自己。
- DllUnregisterServer() 由卸载程序调用,删除通过 DllRegisterServer() 创建的注册表入口。
当然,仅仅导出正确的函数是不够的——它们必须符合COM规范,这样COM库和客户端程序才能使用服务器。
4. 服务器生命周期管理
DLL服务器的一个与众不同之处在于,它们控制加载时间。“普通”dll是被动的,使用它们的应用程序可以随意加载/卸载它们。从技术上讲,DLL服务器也是被动的,因为它们毕竟是DLL, 但是COM库提供了一种机制,允许服务器指示COM卸载它。这是通过导出的函数 DllCanUnloadNow() 完成的。该函数的原型为:
服务器告诉它是否可以卸载的方式是简单的引用计数。 DllCanUnloadNow() 的一个实现可能是这样的:
extern UINT g_uDllRefCount; // server's reference count | |
HRESULT DllCanUnloadNow() | |
{ | |
return (g_uDllRefCount > 0) ? S_FALSE : S_OK; | |
} |
在下一节中,当我们看到一些示例代码时,我将介绍如何维护引用计数。
5. 实现接口,从IUnknown开始
回想一下,每个接口都源自 IUnknown 。这是因为 IUnknown 包含了COM对象的两个基本特性——引用计数和接口查询。 当你写一个coclass时,你也写了一个满足你需要的 IUnknown的实现。让我们以一个刚刚实现 IUnknown 的coclass为例——这是您可以编写的 最简单的coclass。我们将在一个名为 CUnknownImpl 的C++类中实现 IUnknown 。类声明是这样的:
class CUnknownImpl : public IUnknown | |
{ | |
public: | |
// Construction and destruction | |
CUnknownImpl(); | |
virtual ~CUnknownImpl(); | |
// IUnknown methods | |
ULONG AddRef(); | |
ULONG Release(); | |
HRESULT QueryInterface( REFIID riid, void** ppv ); | |
protected: | |
UINT m_uRefCount; // object's reference count | |
}; |
(1) 构造和析构
构造函数和析构函数管理服务器的引用计数:
CUnknownImpl::CUnknownImpl() | |
{ | |
m_uRefCount = 0; | |
g_uDllRefCount++; | |
} | |
CUnknownImpl::~CUnknownImpl() | |
{ | |
g_uDllRefCount--; | |
} |
在创建新的COM对象时调用构造函数,因此它增加服务器的引用计数,以将服务器保存在内存中。它还将对象的引用计数初始化为零。当COM对象被销毁时,它会减少服务器的引用计数。
(2) AddRef() and Release()
这两个方法控制COM对象的生存期。函数 AddRef() 的简单实现:
ULONG CUnknownImpl::AddRef() | |
{ | |
return ++m_uRefCount; | |
} |
函数 AddRef() 只是增加对象的引用计数,并返回更新的计数。
函数 Release() 则没有那么简单:
ULONG CUnknownImpl::Release() | |
{ | |
ULONG uRet = --m_uRefCount; | |
if ( 0 == m_uRefCount ) // releasing last reference? | |
delete this; | |
return uRet; | |
} |
除了减少对象的引用计数外,如果没有未完成引用, Release() 函数就会销毁它。 Release() 还返回更新后的引用计数。注意, Release() 的这个实现假设COM对象是在堆上创建的。如果您在栈上或在全局作用域上创建对象,那么当对象试图删除自己时,就会出错。
现在应该清楚为什么在客户端应用程序中正确调用 AddRef() 和 Release() 很重要了!如果你没有正确地调用它们,你正在使用的COM对象可能会很快被销毁,或者根本没有。如果COM对象被过早地销毁,这可能会导致整个COM服务器被拉出内存,导致你的应用程序在下一次试图访问该服务器中的代码时崩溃。
如果您做过多线程编程,那么您可能想要现成安全的去使用++和–,而不是 InterlockedIncrement() 和 InterlockedDecrement() 。 在单线程服务器中使用++和–是非常安全的,因为即使客户端应用程序是多线程的,并且从不同的线程调用方法,COM库也会将方法调用序列化到我们的服务器中。这意味着,一旦一个方法调用开始,所有试图调用方法的其他线程将阻塞,直到第一个方法返回。COM库本身可以确保服务器不会同时被多个线程进入。
(3) QueryInterface()
客户端使用 QueryInterface() 或简称 QI() 从一个COM对象请求不同的接口。由于我们的例子coobject仅仅实现了一个接口, 所以我们的 QI() 将很容易。QI() 接受两个参数:被请求接口的IID,以及一个指针大小的缓冲区,如果查询成功, QI() 将在该缓冲区中存储接口指针。
HRESULT CUnknownImpl::QueryInterface ( REFIID riid, void** ppv ) | |
{ | |
HRESULT hrRet = S_OK; | |
// Standard QI() initialization - set *ppv to NULL. | |
*ppv = NULL; | |
// If the client is requesting an interface we support, set *ppv. | |
if ( IsEqualIID ( riid, IID_IUnknown )) | |
{ | |
*ppv = (IUnknown*) this; | |
} | |
else | |
{ | |
// We don't support the interface the client is asking for. | |
hrRet = E_NOINTERFACE; | |
} | |
// If we're returning an interface pointer, AddRef() it. | |
if ( S_OK == hrRet ) | |
{ | |
((IUnknown*) *ppv)->AddRef(); | |
} | |
return hrRet; | |
} |
QI() 中主要做了如下三件事:
- 初始化参数传递过来的指针为NULL。[ *ppv = NULL; ]
- 测试riid,看看我们的coclass是否实现了客户端请求的接口。[ if ( IsEqualIID ( riid, IID_IUnknown )) ]
- 如果我们实现了请求的接口,则增加COM对象的引用计数。[ ((IUnknown*) *ppv)->AddRef(); ]
注意 AddRef() 是关键的一行。
*ppv = (IUnknown*) this;
创建一个对COM对象的新引用,因此我们必须调用 AddRef() 来告诉对象这个新引用存在。AddRef() 调用中对 IUnknown* 的转换可能看起来很奇怪,但是有些coclass的 QI() 中,*ppv可能不是 IUnknown* ,所以养成使用这种转换的习惯是一个好主意。
现在我们已经讨论了一些DLL服务器的内部细节,让我们回过头来看看客户端调用CoCreateInstance() 时我们的服务器是如何被使用。
6. CoCreateInstance()的内部
在第一个介绍COM的文章中,我们看到了 CoCreateInstance() API,它在客户端请求并创建了一个COM对象。从客户的角度来看,它一个黑盒子。只需使用正确的参数调用 CoCreateInstance() ,嘭!你得到一个COM对象。当然,这里面没有黑魔法;这里发生了一系列的过程,加载COM服务器、创建所请求的COM对象并返回所请求的接口。
下面是这个过程的一个概述。这里有一些不熟悉的术语,但不用担心;我将在下面几节中介绍所有内容。
- 客户端程序调用 CoCreateInstance() ,传递coclass的CLSID和它需要的接口的IID。
- COM库在 HKEY_CLASSES_ROOT\CLSID 下查找服务器的CLSID。这个key保存服务器的注册信息。
- COM库读取服务器DLL的完整路径,并将DLL加载到客户机的进程空间中。
- COM库调用服务器中的 DllGetClassObject() 函数来请求所请求的coclass的类工厂。
- 服务器创建一个类工厂,并从函数 DllGetClassObject() 返回它。
- COM库调用类工厂中的 CreateInstance() 方法来创建客户端程序请求的COM对象。
- CoCreateInstance() 为客户端程序返回接口的指针。
7. 注册COM服务器
要使任何其他东西工作,COM服务器必须在Windows注册表中正确注册。如果您查看 HKEY_CLASSES_ROOT\CLSID 键,您将看到大量的子键。 HKCR\CLSID 保存计算机上可用的每个COM服务器的列表。当注册COM服务器时(通常通过 DllRegisterServer()),它在CLSID键下创建一个key,该key的名称是标准注册表格式的服务器GUID。下面是注册表格式的一个例子:
{067DF822-EAB6-11cf-B56E-00A0244D5087}
括号和连字符是必需的,字母可以是大写或小写。
这个键的默认值是一个人类可读的coclass的名称,它应该适合,通过像OLE/COM对象查看器(VC内嵌的)这样的工具,在直接查看。
更多信息可以存储在GUID键下的子键中。您需要创建的子键在很大程度上取决于您拥有的COM服务器的类型以及如何使用它。对于我们简单的in-proc服务器,我们只需要一个子键: InProcServer32 。
InProcServer32 键包含两个字符串:默认值,它是服务器DLL的完整路径;和一个 ThreadingModel 值,它保存线程模型。线程模型超出了本文的范围,但是可以这样说,对于单线程服务器,使用的模型是Apartment。
8. 创建COM对象 - 类工厂
当我们研究COM的客户端时,我谈到了COM如何有自己的语言独立的过程来创建和销毁COM对象。客户端调用 CoCreateInstance() 来创建一个新的COM对象。现在,我们将看到它在服务器端是如何工作的。
每次实现一个coclass时,您还需要编写一个coclass的伙伴,它负责创建第一个coclass的实例。这个同伴称为coclass的类工厂,它的唯一目的是创建COM对象。拥有类工厂的原因是语言独立性。COM本身不创建COM对象,因为这不是独立于语言实现的。
当客户端想要创建COM对象时,COM库从COM服务器请求类工厂。类工厂然后创建返回给客户端的COM对象。这种通信的机制是导出的通过函数 DllGetClassObject() 。
术语“类工厂”和“类对象”实际上指的是同一件事。但是,这两个术语都不能准确地描述类工厂的目的,因为工厂创建的是COM对象,而不是COM类。它可以帮助您在思想上将“类工厂”替换为“对象工厂”。(事实上,MFC做到了这一点——它的类工厂实现称为COleObjectFactory。)但是,正式术语是“类工厂”,所以我将在本文中使用它。
当COM库调用 DllGetClassObject() 时,它传递客户端请求的CLSID。服务器负责为所请求的CLSID创建类工厂并返回它。类工厂本身就是一个coclass,它实现了 IClassFactory 接口。如果 DllGetClassObject() 成功,它将返回指向COM库的 IClassFactory 指针,然后使用 IClassFactory 方法创建客户端请求的COM对象的实例。
接口 IClassFactory 看起来似乎是这样的:
struct IClassFactory : public IUnknown | |
{ | |
HRESULT CreateInstance( IUnknown* pUnkOuter, REFIID riid, | |
void** ppvObject ); | |
HRESULT LockServer( BOOL fLock ); | |
}; |
CreateInstance() 是创建新的COM对象的方法。LockServer()会使COM库在必要时增加或减少服务器的引用计数。
9. 示例自定义接口
这是一个展示类工厂如何工作的示例,让我们先看一下本文的示例项目。它是一个DLL服务器,在一个名为 CSimpleMsgBoxImpl 的coclass中实现接口 ISimpleMsgBox 。
(1) 接口定义
我们的新接口称为 ISimpleMsgBox 。与所有接口一样,它必须继承自 IUnknown 。这里只有一个方法, DoSimpleMsgBox() 。注意,它返回标准类型 HRESULT 。您编写的所有方法都应该使用 HRESULT 作为返回类型,并且您需要返回给调用者的任何其他数据,都应该通过指针参数来完成。
struct ISimpleMsgBox : public IUnknown | |
{ | |
// IUnknown methods | |
ULONG AddRef(); | |
ULONG Release(); | |
HRESULT QueryInterface( REFIID riid, void** ppv ); | |
/ ISimpleMsgBox methods | |
HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText ); | |
}; | |
struct __declspec(uuid("{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}")) | |
ISimpleMsgBox; |
( __declspec 这一行分配了一个GUID给 ISimpleMsgBox ,之后可以使用 __uuidof 操作符获取该GUID。__declspec 和 __uuidof 这两个是由Microsoft C++扩展的。)
函数 DoSimpleMsgBox() 的第二个参数是 BSTR 类型。 BSTR 代表“binary string(二进制字符串)”——COM对固定长度的字节序列的表示。 BSTR 主要用于脚本客户端,如Visual Basic和Windows脚本主机。
然后这个接口由一个名为 CSimpleMsgBoxImpl 的c++类实现。它的定义是:
class CSimpleMsgBoxImpl : public ISimpleMsgBox | |
{ | |
public: | |
CSimpleMsgBoxImpl(); | |
virtual ~CSimpleMsgBoxImpl(); | |
// IUnknown methods | |
ULONG AddRef(); | |
ULONG Release(); | |
HRESULT QueryInterface( REFIID riid, void** ppv ); | |
// ISimpleMsgBox methods | |
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; | |
hr = CoCreateInstance( __uuidof(CSimpleMsgBoxImpl), // CLSID of the coclass | |
NULL, // no aggregation | |
CLSCTX_INPROC_SERVER, // the server is in-proc | |
__uuidof(ISimpleMsgBox), // IID of the interface | |
// we want | |
(void**) &pIMsgBox ); // address of our | |
// interface pointer |
(2) 类工厂
我们类工厂的实现
我们的 SimpleMsgBox 类工厂是在一个C++类中实现的,这个C++类叫做 CSimpleMsgBoxClassFactory :
class CSimpleMsgBoxClassFactory : public IClassFactory | |
{ | |
public: | |
CSimpleMsgBoxClassFactory(); | |
virtual ~CSimpleMsgBoxClassFactory(); | |
// IUnknown methods | |
ULONG AddRef(); | |
ULONG Release(); | |
HRESULT QueryInterface( REFIID riid, void** ppv ); | |
// IClassFactory methods | |
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 ) | |
{ | |
// We don't support aggregation, so pUnkOuter must be NULL. | |
if ( NULL != pUnkOuter ) | |
return CLASS_E_NOAGGREGATION; | |
// Check that ppv really points to a void*. | |
if ( IsBadWritePtr ( ppv, sizeof(void*) )) | |
return E_POINTER; | |
*ppv = NULL; |
我们已经检查了参数是否有效,因此现在可以创建一个新对象。
CSimpleMsgBoxImpl* pMsgbox; | |
// Create a new COM object! | |
pMsgbox = new CSimpleMsgBoxImpl; | |
if ( NULL == pMsgbox ) | |
return E_OUTOFMEMORY; |
最后,我们 QI() 客户端请求的接口的新对象。如果 QI() 失败,则对象不可用,因此我们删除它。
HRESULT hrRet; | |
// QI the object for the interface the client is requesting. | |
hrRet = pMsgbox->QueryInterface ( riid, ppv ); | |
// If the QI failed, delete the COM object since the client isn't able | |
// to use it (the client doesn't have any interface pointers on the | |
// object). | |
if ( FAILED(hrRet) ) | |
delete pMsgbox; | |
return hrRet; | |
} |
(3) DllGetClassObject()
让我们仔细看看 DllGetClassObject() 的内部结构。它的原型是:
HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv );
rclsid 是客户端需要的coclass的CLSID。该函数必须返回该coclass的类工厂。
riid 和 ppv 同样类似于 QI() 的参数。在本例中,riid是COM库在类工厂对象上请求的接口的IID。这通常是 IID_IClassFactory 。
因为 DllGetClassObject() 创建了一个新的COM对象(类工厂),所以代码看起来非常类似于 IClassFactory::CreateInstance() 。我们从一些验证和初始化开始。
HRESULT DllGetClassObject ( REFCLSID rclsid, REFIID riid, void** ppv ) | |
{ | |
// Check that the client is asking for the CSimpleMsgBoxImpl factory. | |
if ( !InlineIsEqualGUID ( rclsid, __uuidof(CSimpleMsgBoxImpl) )) | |
return CLASS_E_CLASSNOTAVAILABLE; | |
// Check that ppv really points to a void*. | |
if ( IsBadWritePtr ( ppv, sizeof(void*) )) | |
return E_POINTER; | |
*ppv = NULL; |
第一个if语句检查rclsid参数。我们的服务器只包含一个coclass,所以 rclsid 必须是我们的 CSimpleMsgBoxImpl 类的CLSID。 __uuidof 操作符通指派 CSimpleMsgBoxImpl 的GUID,比 __declspec(uuid()) 的声明更早一些。 InlineIsEqualGUID() 是一个内联函数,用于检查两个GUID是否相等。
下一步是创建一个类工厂对象。
CSimpleMsgBoxClassFactory* pFactory; | |
// Construct a new class factory object. | |
pFactory = new CSimpleMsgBoxClassFactory; | |
if ( NULL == pFactory ) | |
return E_OUTOFMEMORY; |
这里的内容与 CreateInstance() 稍有不同。在 CreateInstance() 中,我们仅仅调用了 QI() ,如果它失败了,我们就删除COM对象。这里有一种不同的做事方式。
我们可以将自己看作是刚刚创建的COM对象的客户端,因此我们对其调用 AddRef() 以使其引用计数为1。然后我们调用 QI() 。如果 QI() 成功,它将再次 AddRef() 该对象,使引用计数为2。如果 QI() 失败,引用计数将保持为1。
在调用 QI() 之后,我们就完成了对类工厂对象的使用,因此我们对它调用 Release() 。如果 QI() 失败,对象将删除自己(因为引用计数将为0),因此最终结果是相同的。
// AddRef() the factory since we're using it. | |
pFactory->AddRef(); | |
HRESULT hrRet; | |
// QI() the factory for the interface the client wants. | |
hrRet = pFactory->QueryInterface ( riid, ppv ); | |
// We're done with the factory, so Release() it. | |
pFactory->Release(); | |
return hrRet; | |
} |
(4) 再论QueryInterface()
我在前面展示了 QI() 实现,但是值得看看类工厂的 QI() ,因为它是一个实际的示例,因为COM对象实现的不仅仅是
IUnknown 。首先,我们验证 ppv 缓冲区并初始化它。
HRESULT CSimpleMsgBoxClassFactory::QueryInterface( REFIID riid, void** ppv ) | |
{ | |
HRESULT hrRet = S_OK; | |
// Check that ppv really points to a void*. | |
if ( IsBadWritePtr ( ppv, sizeof(void*) )) | |
return E_POINTER; | |
// Standard QI initialization - set *ppv to NULL. | |
*ppv = NULL; |
接下来,我们检查 riid ,看看它是否是类工厂实现的接口之一: IUnknown 或 IClassFactory 。
// If the client is requesting an interface we support, set *ppv. | |
if ( InlineIsEqualGUID ( riid, IID_IUnknown )) | |
{ | |
*ppv = (IUnknown*) this; | |
} | |
else if ( InlineIsEqualGUID ( riid, IID_IClassFactory )) | |
{ | |
*ppv = (IClassFactory*) this; | |
} | |
else | |
{ | |
hrRet = E_NOINTERFACE; | |
} |
最后,如果 riid 是被支持的接口,我们在接口指针上调用AddRef(),然后返回。
// If we're returning an interface pointer, AddRef() it. | |
if ( S_OK == hrRet ) | |
{ | |
((IUnknown*) *ppv)->AddRef(); | |
} | |
return hrRet; | |
} |
(5) ISimpleMsgBox实现
最后但并非最不重要的是,我们有 ISimpleMsgBox 唯一的方法 DoSimpleMsgBox() 的代码。我们首先使用Microsoft扩展类 _bstr_t 将 bsMessageText 转换为 TCHAR 字符串。
HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent, | |
BSTR bsMessageText ) | |
{ | |
_bstr_t bsMsg = bsMessageText; | |
LPCTSTR szMsg = (TCHAR*) bsMsg; // Use _bstr_t to convert the | |
// string to ANSI if necessary.</FONT> |
完成转换后,我们将显示消息框,然后返回。
MessageBox ( hwndParent, szMsg, _T("Simple Message Box"), MB_OK ); | |
return S_OK; | |
} |
10. 客户端使用我们的COM服务
现在我们有了这个超级漂亮的COM服务器,我们如何使用它呢?我们的接口是一个自定义接口,这意味着它只能由C或c++客户端使用。(如果我们的coclass也实现了 IDispatch , 那么我们就可以用几乎任何东西来编写客户机——Visual Basic、Windows脚本主机、web页面、PerlScript等等。但这个讨论最好留到另一篇文章中讨论。)我提供了一个使用 ISimpleMsgBox 的简单应用程序。
该应用程序基于由Win32应用程序AppWizard构建的Hello World示例。File菜单包含两个用于测试服务器的命令:
Test MsgBox COM Server 命令创建一个 CSimpleMsgBoxImpl 对象并调用 DoSimpleMsgBox() 。因为这是一个简单的方法,所以代码不是很长。我们首先使用 CoCreateInstance() 创建一个COM对象。
void DoMsgBoxTest(HWND hMainWnd) | |
{ | |
ISimpleMsgBox* pIMsgBox; | |
HRESULT hr; | |
hr = CoCreateInstance ( __uuidof(CSimpleMsgBoxImpl), // CLSID of coclass | |
NULL, // no aggregation | |
CLSCTX_INPROC_SERVER, // use only in-proc | |
// servers | |
__uuidof(ISimpleMsgBox), // IID of the interface | |
// we want | |
(void**) &pIMsgBox ); // buffer to hold the | |
// interface pointer | |
if ( FAILED(hr) ) | |
return; |
然后调用 DoSimpleMsgBox() 并释放接口。
pIMsgBox->DoSimpleMsgBox ( hMainWnd, _bstr_t("Hello COM!") ); | |
pIMsgBox->Release(); | |
} |
这就是它的全部。代码中有许多跟踪语句,因此如果您在调试器中运行测试应用程序,您可以看到服务器中每个方法被调用的位置。
另一个文件菜单命令调用 CoFreeUnusedLibraries() API,这样您就可以看到服务器的 DllCanUnloadNow() 函数在起作用。
11. 其他细节
(1) COM宏
COM代码中使用了一些宏来隐藏实现细节,并允许C和c++客户端使用相同的声明。在本文中我没有使用宏,但是示例项目使用了它们,因此您需要理解它们的含义。 ISimpleMsgBox 的正确声明如下:
struct ISimpleMsgBox : public IUnknown | |
{ | |
// IUnknown methods | |
STDMETHOD_(ULONG, AddRef)() PURE; | |
STDMETHOD_(ULONG, Release)() PURE; | |
STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE; | |
// ISimpleMsgBox methods | |
STDMETHOD(DoSimpleMsgBox)(HWND hwndParent, BSTR bsMessageText) PURE; | |
}; |
STDMETHOD() 包括 virtual 关键字、 HRESULT 的返回类型和 __stdcall 调用约定。 STDMETHOD_() 与此相同,只是可以指定不同的返回类型。 PURE 在C++中扩展为“=0”,使函数成为纯虚函数。
STDMETHOD() 和 STDMETHOD_() 在方法 STDMETHODIMP 和 STDMETHODIMP_() 的实现中有相应的宏。例如,下面是 DoSimpleMsgBox() 的实现:
STDMETHODIMP CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent, | |
BSTR bsMessageText ) | |
{ | |
... | |
} |
最后用 STDAPI 宏声明标准导出的函数,如:
STDAPI DllRegisterServer()
STDAPI 包括返回类型和调用约定。使用 STDAPI 的一个缺点是,由于 STDAPI 的扩展方式,您不能使用 __declspec(dllexport) 。相反,您必须使用. def文件导出该函数。
(2) 服务器注册和注销
服务器实现了我前面提到的 DllRegisterServer() 和 DllUnregisterServer() 函数。 他们的工作是创建和删除那些告诉COM关于我们的服务器的注册表项。代码都是无聊的注册表操作,所以我不在这里重复,但是这里有一个由 DllRegisterServer() 创建的注册表条目列表:
(3) 示例代码中的注意事项
所包含的示例代码包含COM服务器和测试客户端应用程序的源代码。项目文件 SimpleComSvr , dsw ,您可以在服务器和客户端应用程序在同时加载和工作。在与工作空间相同的级别上有两个由两个项目使用的头文件。然后,每个项目都在自己的子目录中。
共用的两个头文件:
- 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 关键字。这个关键字意味着函数被导出,但不包含在导入库中。这意味着客户端不能直接从代码中调用函数,即使它们链接到导入库中。 这是一个必要的步骤,如果你省略了 PRIVATE 关键字,链接器将会报错。
(4) 在服务器端设置断点
如果希望在服务器代码中设置断点,有两种方法。第一种方法是将服务器项目(MsgBoxSvr)设置为活动项目,然后开始调试。MSVC将要求您为调试会话运行可执行文件。输入测试客户端的完整路径,您必须已经构建了该路径。
另一种方法是使客户端项目(TestClient)成为活动项目,并配置项目依赖项,使服务器项目成为客户端项目的依赖项。这样,如果您在服务器中更改代码,它将在您构建客户机项目时自动重新生成。最后一个细节是告诉MSVC在开始调试客户端时加载服务器的符号。
项目依赖关系对话框应该像这样:
要加载服务器的符号,打开TestClient项目设置,转到Debug选项卡,并在类别组合框中选择其他dll。单击列表框添加一个新条目,然后输入服务器DLL的完整路径。这里有一个例子:
当然,到DLL的路径将根据您提取源代码的位置而有所不同