COM 第二部分简介 - COM 服务器的幕后






4.97/5 (147投票s)
2001年1月12日

875104

10931
一篇面向刚接触COM的程序员的教程,解释了COM服务器的内部机制,以及如何用C++编写自己的接口。
本文目的
与我的第一篇COM入门文章一样,我写这篇教程是面向刚开始接触COM并需要一些基本知识帮助的程序员。这篇文章从COM服务器的角度讲解COM,解释了编写自己的COM接口和COM服务器所需的步骤,并详细说明了当COM库调用它时,COM服务器中到底发生了什么。
引言
如果你已经阅读了我第一篇COM入门文章,你应该对作为客户端使用COM所涉及的内容有所了解。现在是时候从另一个角度来处理COM了——COM服务器。我将介绍如何用纯C++从头开始编写COM服务器,不涉及任何类库。虽然这不一定是当今常用的方法,但看到一个COM服务器的全部实现代码——没有任何隐藏在预构建库中的东西——是完全理解服务器中发生的一切的最佳方式。
本文假定你精通C++,并且理解第一篇COM入门文章中涵盖的概念和术语。文章的章节包括:
COM服务器快速浏览 - 描述COM服务器的基本要求。
服务器生命周期管理 - 描述COM服务器如何控制其加载时间。
实现接口,从IUnknown开始 - 展示如何在C++类中编写接口的实现,并描述IUnknown
方法的目的。
CoCreateInstance()内部 - 概述调用CoCreateInstance()
时发生的情况。
COM服务器注册 - 描述正确注册COM服务器所需的注册表项。
创建COM对象 - 类工厂 - 描述为客户端程序创建COM对象的过程。
一个自定义接口示例 - 一些示例代码,说明了前面章节的概念。
一个用于我们服务器的客户端 - 演示我们可以用来测试服务器的简单客户端应用程序。
其他细节 - 关于源代码和调试的说明。
COM服务器快速浏览
在本文中,我们将关注最简单的COM服务器类型——进程内服务器。“进程内”意味着服务器被加载到客户端程序的进程空间中。进程内(或“in-proc”)服务器始终是DLL,并且必须与客户端程序在同一台计算机上。
在COM库可以使用进程内服务器之前,它必须满足两个条件:
- 它必须在
HKEY_CLASSES_ROOT\CLSID
键下正确注册。 - 它必须导出一个名为
DllGetClassObject()
的函数。
这是让进程内服务器工作的最低要求。一个以服务器GUID命名的键必须在HKEY_CLASSES_ROOT\CLSID
键下创建,并且该键必须包含一些值,列出服务器的位置及其线程模型。DllGetClassObject()
函数由COM库在CoCreateInstance()
API工作的一部分中调用。
通常还会导出另外三个函数:
DllCanUnloadNow()
: 由COM库调用,以确定服务器是否可以从内存中卸载。DllRegisterServer()
: 由RegSvr32等安装实用程序调用,允许服务器自行注册。DllUnregisterServer()
: 由卸载实用程序调用,以删除DllRegisterServer()
创建的注册表项。
当然,仅仅导出正确的函数是不够的——它们必须符合COM规范,以便COM库和客户端程序能够使用服务器。
服务器生命周期管理
DLL服务器的一个不寻常方面是它们控制着自身的加载时间。“普通”DLL是被动的,由使用它们的应用程序决定加载/卸载。严格来说,DLL服务器也是被动的,毕竟它们是DLL,但是COM库提供了一种机制,允许服务器指示COM卸载它。这是通过导出的DllCanUnloadNow()
函数实现的。该函数的原型是:
HRESULT DllCanUnloadNow();
当客户端应用程序调用COM API CoFreeUnusedLibraries()
(通常在空闲处理期间)时,COM库会遍历应用程序加载的所有DLL服务器,并通过调用其DllCanUnloadNow()
函数来查询每个服务器。如果服务器需要保持加载,它返回S_FALSE
。另一方面,如果服务器确定不再需要在内存中,它可以返回S_OK
让COM将其卸载。
服务器判断是否可以卸载的方法是一个简单的引用计数。DllCanUnloadNow()
的实现可能如下所示:
extern UINT g_uDllRefCount; // server's reference count HRESULT DllCanUnloadNow() { return (g_uDllRefCount > 0) ? S_FALSE : S_OK; }
在下一节,当我们开始看一些示例代码时,我会讲解引用计数是如何维护的。
实现接口,从IUnknown开始
回想一下,每个接口都派生自IUnknown
。这是因为IUnknown
涵盖了COM对象的两个基本功能——引用计数和接口查询。当你编写一个coclass时,你还需要编写一个满足你需求的IUnknown
实现。让我们以一个只实现IUnknown
的coclass为例——这是你能编写的最简单的coclass。我们将IUnknown
实现在一个名为CUnknownImpl
的C++类中。类声明如下:
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 };
构造函数和析构函数
构造函数和析构函数管理服务器的引用计数。
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 ) // releasing last reference? delete this; return uRet; }
除了减少对象的引用计数外,Release()
会在没有更多未决引用时销毁对象。Release()
还返回更新后的引用计数。请注意,此Release()
实现假定COM对象是在堆上创建的。如果你在栈上或全局范围内创建对象,当对象尝试自我删除时,情况会变得一团糟。
现在应该清楚为什么在客户端应用程序中正确调用AddRef()
和Release()
很重要了!如果你不正确调用它们,你使用的COM对象可能会过早销毁,或者根本不销毁。如果COM对象过早销毁,可能会导致整个COM服务器从内存中被移除,当你下次尝试访问属于该服务器的代码时,你的应用程序可能会崩溃。
如果你做过多线程编程,你可能会想知道使用++
和--
而不是InterlockedIncrement()
和InterlockedDecrement()
的线程安全性。对于单线程服务器,使用++
和--
是完全安全的,因为即使客户端应用程序是多线程的并且从不同线程调用方法,COM库也会将方法调用序列化到我们的服务器中。这意味着一旦一个方法调用开始,所有试图调用方法的其他线程都将阻塞,直到第一个方法返回。COM库本身确保我们的服务器永远不会被多个线程同时进入。
QueryInterface()
QueryInterface()
,或简称为QI()
,用于客户端从一个COM对象请求不同的接口。由于我们的示例coclass只实现了一个接口,我们的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()
时我们的服务器是如何被使用的。
CoCreateInstance()内部
在第一篇COM入门文章中,我们看到了CoCreateInstance()
API,它在客户端请求时创建一个COM对象。从客户端的角度来看,它是一个黑盒子。只需用正确的参数调用CoCreateInstance()
,然后 BAM! 你就得到了一个COM对象。当然,没有什么魔法,而是一个定义明确的过程:COM服务器被加载,创建请求的COM对象,并返回请求的接口。
以下是该过程的快速概述。这里有一些不熟悉的术语,但别担心;我将在后面的章节中全部介绍。
- 客户端程序调用
CoCreateInstance()
,传递coclass的CLSID和它想要的接口的IID。 - COM库在
HKEY_CLASSES_ROOT\CLSID
下查找服务器的CLSID。此键包含服务器的注册信息。 - COM库读取服务器DLL的完整路径,并将DLL加载到客户端的进程空间中。
- COM库调用服务器中的
DllGetClassObject()
函数,以请求所请求coclass的类工厂。 - 服务器创建一个类工厂并从
DllGetClassObject()
返回它。 - COM库调用类工厂中的
CreateInstance()
方法来创建客户端程序请求的COM对象。 CoCreateInstance()
将接口指针返回给客户端程序。
COM服务器注册
为了让其他一切正常工作,COM服务器必须在Windows注册表中正确注册。如果你查看HKEY_CLASSES_ROOT\CLSID
键,你会看到大量的子键。HKCR\CLSID
保存了计算机上所有可用COM服务器的列表。当COM服务器注册时(通常通过DllRegisterServer()
),它会在CLSID
键下创建一个以服务器GUID的标准注册表格式命名的键。注册表格式示例为:
{067DF822-EAB6-11cf-B56E-00A0244D5087}
花括号和连字符是必需的,字母可以是大小写。
此键的默认值是coclass的可读名称,适合在OLE/COM Object Viewer等工具(随VC一起提供)的UI中显示。
可以在GUID键下的子键中存储更多信息。需要创建哪些子键很大程度上取决于你的COM服务器类型及其使用方式。对于我们简单的进程内服务器,我们只需要一个子键:InProcServer32
。
InProcServer32
键包含两个字符串:默认值,即服务器DLL的完整路径;以及一个ThreadingModel
值,该值保存(还能是什么?)线程模型。线程模型超出了本文的范围,但足以说明,对于单线程服务器,应使用的模型是Apartment
。
创建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()
成功,它将返回一个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服务器,在一个名为CSimpleMsgBoxImpl
的coclass中实现了一个ISimpleMsgBox
接口。
接口定义
我们的新接口称为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
符号,并且该GUID之后可以通过__uuidof
运算符检索。__declspec
和__uuidof
都是Microsoft C++扩展。)
DoSimpleMsgBox()
的第二个参数是BSTR
类型。BSTR
代表“二进制字符串”——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
类工厂
我们的类工厂实现
我们的SimpleMsgBox
类工厂由一个名为(顾名思义)CSimpleMsgBoxClassFactory
的C++类实现。
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; }
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
运算符检索之前通过__declspec(uuid())
声明分配给CSimpleMsgBoxImpl
的GUID。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; }
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; }
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.
转换完成后,我们显示消息框,然后返回。
MessageBox ( hwndParent, szMsg, _T("Simple Message Box"), MB_OK ); return S_OK; }
一个用于我们服务器的客户端
那么,我们有了这个超酷的COM服务器,该如何使用它呢?我们的接口是自定义接口,这意味着它只能被C或C++客户端使用。(如果我们的coclass还实现了IDispatch
,那么我们可以用几乎任何语言编写客户端——Visual Basic、Windows脚本宿主、网页、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();
}
就这样。代码中有很多TRACE
语句,所以如果你在调试器中运行测试应用程序,你可以看到服务器中的每个方法何时被调用。
File
菜单中的另一个命令调用CoFreeUnusedLibraries()
API,这样你就可以看到服务器的DllCanUnloadNow()
函数是如何工作的。
其他细节
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
的一个缺点是不能用它来使用__declspec(dllexport)
,因为STDAPI
的展开方式。你必须使用.DEF文件导出函数。
服务器注册和注销
服务器实现了我之前提到的DllRegisterServer()
和DllUnregisterServer()
函数。它们的工作是创建和删除告知COM服务器的注册表项。代码都是无聊的注册表操作,所以我不会在这里重复,但这是一份DllRegisterServer()
创建的注册表项列表:
键名 |
键中的值 |
---|---|
HKEY_CLASSES_ROOT |
|
CLSID |
|
{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4} |
Default="SimpleMsgBox class" |
InProcServer32 |
Default=[DLL的路径]; ThreadingModel="Apartment" |
关于示例代码的说明
包含的示例代码包含COM服务器和测试客户端应用程序的源文件。有一个工作区文件SimpleComSvr.dsw
,您可以加载它以同时处理服务器和客户端应用程序。工作区同一级别的有两个头文件,供两个项目使用。每个项目都在自己的子目录中。
公共头文件是:
ISimpleMsgBox.h
-ISimpleMsgBox
定义。SimpleMsgBoxComDef.h
- 包含__declspec(uuid())
声明。这些声明在一个单独的文件中,因为客户端需要CSimpleMsgBoxImpl
的GUID,但不需要它的定义。将GUID移到单独的文件中可以让客户端在不依赖CSimpleMsgBoxImpl
内部结构的情况下访问GUID。对客户端来说重要的是接口ISimpleMsgBox
。
如前所述,你需要一个.DEF文件来从服务器导出四个标准的导出函数。示例项目的.DEF文件如下所示:
EXPORTS DllRegisterServer PRIVATE DllUnregisterServer PRIVATE DllGetClassObject PRIVATE DllCanUnloadNow PRIVATE
每一行包含函数名和PRIVATE
关键字。这个关键字表示函数被导出,但不包含在导入库中。这意味着客户端不能直接从代码中调用函数,即使它们链接了导入库。这是必需的步骤,如果你省略PRIVATE
关键字,链接器会报错。
在服务器中设置断点
如果你想在服务器代码中设置断点,有两种方法。第一种方法是将服务器项目(MsgBoxSvr)设为活动项目,然后开始调试。MSVC会询问你用于调试会话的可执行文件。输入测试客户端的完整路径,你必须已经构建了它。
另一种方法是将客户端项目(TestClient)设为活动项目,并配置项目依赖项,以便服务器项目是客户端项目的依赖项。这样,如果你更改了服务器中的代码,在构建客户端项目时它将被自动重新构建。最后的细节是告诉MSVC在开始调试客户端时加载服务器的符号。
项目依赖项对话框应如下所示:
要加载服务器的符号,打开TestClient项目设置,转到Debug选项卡,并在Category组合框中选择Additional DLLs。单击列表框添加一个新条目,然后输入服务器DLL的完整路径。例如:
当然,DLL的路径将根据你解压源代码的位置而有所不同。