文章目录
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_ROOTCLSID键值下正确的注册。
- 它必须导出一个名为DllGetClassObject()的函数。
这是让进程内服务器工作所需要做的最少的事情。必须在HKEY_CLASSES_ROOTCLSID键下创建一个名称为服务器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_ROOTCLSID下查找服务器的CLSID。这个key保存服务器的注册信息。
- COM库读取服务器DLL的完整路径,并将DLL加载到客户机的进程空间中。
- COM库调用服务器中的DllGetClassObject()函数来请求所请求的coclass的类工厂。
- 服务器创建一个类工厂,并从函数DllGetClassObject()返回它。
- COM库调用类工厂中的CreateInstance()方法来创建客户端程序请求的COM对象。
- CoCreateInstance()为客户端程序返回接口的指针。
7. 注册COM服务器
要使任何其他东西工作,COM服务器必须在Windows注册表中正确注册。如果您查看HKEY_CLASSES_ROOTCLSID键,您将看到大量的子键。HKCRCLSID保存计算机上可用的每个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; |
当客户端想要创建一个SimpleMsgBoxCOM对象时,它会使用如下代码:
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. |
完成转换后,我们将显示消息框,然后返回。
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的路径将根据您提取源代码的位置而有所不同
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
相关推荐: Kubernetes:kube-apiserver 之启动流程(一)
0. 前言 前面两篇文章 Kubernetes:kube-apiserver 之 scheme(一) 和 Kubernetes:kube-apiserver 之 scheme(二) 重点介绍了 kube-apiserver 中的资源注册表 scheme。这里进…