COM编程入门Part Ⅱ - 深入理解COM服务器[译]

news2024/11/24 5:05:11

文章目录

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() 中主要做了如下三件事:

  1. 初始化参数传递过来的指针为NULL。[ *ppv = NULL; ]
  2. 测试riid,看看我们的coclass是否实现了客户端请求的接口。[ if ( IsEqualIID ( riid, IID_IUnknown )) ]
  3. 如果我们实现了请求的接口,则增加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对象并返回所请求的接口。

下面是这个过程的一个概述。这里有一些不熟悉的术语,但不用担心;我将在下面几节中介绍所有内容。

  1. 客户端程序调用 CoCreateInstance() ,传递coclass的CLSID和它需要的接口的IID。
  2. COM库在 HKEY_CLASSES_ROOT\CLSID 下查找服务器的CLSID。这个key保存服务器的注册信息。
  3. COM库读取服务器DLL的完整路径,并将DLL加载到客户机的进程空间中。
  4. COM库调用服务器中的 DllGetClassObject() 函数来请求所请求的coclass的类工厂。
  5. 服务器创建一个类工厂,并从函数 DllGetClassObject() 返回它。
  6. COM库调用类工厂中的 CreateInstance() 方法来创建客户端程序请求的COM对象。
  7. 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的路径将根据您提取源代码的位置而有所不同

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1092030.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[入门一]C# webApi创建、与发布、部署、api调用

一.创建web api项目 1.1、项目创建 MVC架构的话&#xff0c;它会有view-model-control三层&#xff0c;在web api中它的前端和后端是分离的&#xff0c;所以只在项目中存在model-control两层 1.2、修改路由 打开App_Start文件夹下&#xff0c;WebApiConfig.cs ,修改路由&…

12.SpringBoot之RestTemplate的使用

SpringBoot之RestTemplate的使用 初识RestTemplate RestTemplate是Spring框架提供用于调用Rest接口的一个应用&#xff0c;它简化了与http服务通信方式。RestTemplate统一Restfull调用的标准&#xff0c;封装HTTP链接&#xff0c;只要需提供URL及返回值类型即可完成调用。相比…

小程序之基础入门(1)

⭐⭐ 小程序专栏&#xff1a;小程序开发专栏 ⭐⭐ 个人主页&#xff1a;个人主页 目录 一.前言 二.小程序简介 2.1 微信小程序的特点和优势&#xff1a; 三.小程序的安装与使用 五.小程序入门案例及目录结构 好啦&#xff01;&#xff01;今天的分享就到这啦&#xff01;…

多测师肖sir_高级金牌讲师_python之模块008

python之模块 一、模块的介绍 &#xff08;1&#xff09;python模块&#xff0c;是一个python文件&#xff0c;以一个.py文件&#xff0c;包含了python对象定义和pyhton语句 &#xff08;2&#xff09;python对象定义和python语句 &#xff08;3&#xff09;模块让你能够有逻辑…

微信浏览器大字体模式 按钮文字居中用line-height 显示异常

按钮文字居中用line-height 的css 在微信浏览器大字体模式&#xff0c;会导致显示错误。改成flex 居中就好了

R语言手动绘制NHANSE数据基线表并聊聊NHANSE数据制作亚组交互效应表的问题(P for interaction)

美国国家健康与营养调查&#xff08; NHANES, National Health and Nutrition Examination Survey&#xff09;是一项基于人群的横断面调查&#xff0c;旨在收集有关美国家庭人口健康和营养的信息。 地址为&#xff1a;https://wwwn.cdc.gov/nchs/nhanes/Default.aspx 在既往的…

精品Python基于django就业数据分析平台求职招聘应聘-爬虫可视化大屏

《[含文档PPT源码等]精品基于django就业数据分析平台-爬虫》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程等&#xff01; 软件开发环境及开发工具&#xff1a; 开发语言&#xff1a;python 使用框架&#xff1a;Django 前端技术&#xff1a;JavaS…

常见锁的分类

入职体验&#xff1a; 今天运维岗位刚入职&#xff0c;但是目前还没有办理入职手续&#xff0c;但是领导发了一堆资料&#xff01;看了一下&#xff0c;非常多的新东西&#xff0c;只能说努力一把&#xff01;&#xff01;&#xff01; 一、锁的分类 1.1 可重入锁、不可重入锁…

DAE转换GLB格式

1、DAE模型介绍 DAEA&#xff08;Deep Attentive and Ensemble Autoencoder&#xff09;模型是一种用于无监督学习的深度学习模型&#xff0c;由华为公司提出。DAEA模型结合了自编码器和深度注意力机制&#xff0c;能够对高维数据进行降维和特征提取&#xff0c;并且在处理大规…

求三维坐标绕坐标轴旋转后的坐标值

目的 旋转矩阵 应用 沿单个坐标轴旋转 沿多个坐标系旋转 目的 由于其他文章原理介绍比较详细&#xff0c;但应用方面较少。本文直接介绍旋转矩阵的应用&#xff0c;条件为坐标系不变&#xff0c;求旋转后的三维坐标。本文方法较傻瓜式&#xff0c;需要自己进行测试以确认是…

Java-使用sqlSessionTemplate实现批量更新-模拟mybatis 动态sql

环境准备&#xff08;非核心方法&#xff09; 创建表 创建表的sql(下表是基于Oracle创建的) CREATE TABLE "SYSTEM"."STUDENT" ("ID" NUMBER(10, 0),"NAME" VARCHAR2(20 BYTE),"ADDRES" CLOB,PRIMARY KEY ( …

Studio One6.5全新版本更新功能详情介绍

Studio One是一款专业的音乐制作软件&#xff0c;由美国PreSonus公司开发。该软件提供了全面的音频编辑和混音功能&#xff0c;包括录制、编曲、合成、采样等多种工具&#xff0c;可用于制作各种类型的音乐&#xff0c;如流行音乐、电子音乐、摇滚乐等。 Studio One的主要特点…

一文2000字从0到1手把手教你jmeter分布式压测

一、jmeter为什么要做分布式压测 jmeter本身的局限性 一台压力机的 Jmeter 支持的线程数受限于 Jmeter 其本身的机制和硬件配置&#xff08;内存、CPU等&#xff09;是有限的由于 Jmeter 是 Java 应用&#xff0c;对 CPU 和内存的消耗较大&#xff0c;在需要模拟大量并发用户…

论文中的算法/伪代码怎么写/理解

文章目录 第零步 基础知识第一步 创建latex文档&#xff0c;导入安装包第二步 写伪代码第三步 美化伪代码补充&#xff1a;latex无法显示中文 准备工作&#xff1a; overleaf&#xff1a;在线的latex文档编辑平台 algorithm2e&#xff1a;Latex的一个写算法的库 第零步 基础知识…

【树莓派 picamera】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言https://www.cnblogs.com/uestc-mm/p/7606855.html 一、picamera是什么&#xff1f;二、使用步骤1.引入库2.先要安装opencv 总结 前言 想用树莓派libcamera &a…

发货100虚拟商品自动发货系统存在SQL注入

漏洞描述 该系统存在SQL注入漏洞&#xff0c;可获取敏感信息及账号密码。 漏洞复现 构造SQL注入延时payload&#xff1a; M_id11%20AND%20(SELECT%208965%20FROM%20(SELECT(SLEEP(5)))sdhh)&typeproduct漏洞证明&#xff1a; 文笔生疏&#xff0c;措辞浅薄&#xff0c;…

C语言网络编程基础(linux)

目录 文件描述符与套接字网络编程的基本流程基础的函数和结构体&#xff08;持续更新&#xff09;socket函数sockaddr和sockaddr_in结构体bind函数listen函数accept函数recv函数writev函数readv函数connect函数fcntl函数 epoll相关函数epoll_createepoll_ctl函数epoll_wait函数…

竞赛 深度学习+python+opencv实现动物识别 - 图像识别

文章目录 0 前言1 课题背景2 实现效果3 卷积神经网络3.1卷积层3.2 池化层3.3 激活函数&#xff1a;3.4 全连接层3.5 使用tensorflow中keras模块实现卷积神经网络 4 inception_v3网络5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; *…

生物标志物发现中的无偏数据分析策略

目录 0. 导论基本概念 1. 生物标志物发现的注意事项2. 数据预处理2.1 高质量原始数据和缺失值处理2.2 数据过滤2.3 数据归一化 3. 数据质量评估3.1 混杂因素3.2 类别分离3.3 功效分析3.4 批次效应 4. 生物标志物发现4.1 策略4.2 数据分析工具4.3 模型优化策略 0. 导论 组学技术…

Pytorch之MobileViT图像分类

文章目录 前言一、Transformer存在的问题二、MobileViT1.MobileViT网络结构&#x1f353; Vision Transformer结构&#x1f349;MobileViT结构 2.MV2(MobileNet v2 block)3.MobileViT block&#x1f947;Local representations&#x1f948;Transformers as Convolutions (glob…