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

本篇文章为翻译文章,适合像我一样,之前从来没有接触过COM编程的人,如果翻译的有什么不足之处,希望大家多多指出。

原文链接:
https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a
本篇文章为译文的第二部分,第一部分链接:
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对象,即将包含新对象的对象。聚合超出了本文的范围,我们的示例对象将不支持聚合。

riidppv 的使用就像 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的类工厂。

riidppv 同样类似于 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 ,看看它是否是类工厂实现的接口之一: IUnknownIClassFactory

 // 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_tbsMessageText 转换为 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_() 在方法 STDMETHODIMPSTDMETHODIMP_() 的实现中有相应的宏。例如,下面是 DoSimpleMsgBox() 的实现:

STDMETHODIMP CSimpleMsgBoxImpl::DoSimpleMsgBox ( HWND hwndParent,
                                                 BSTR bsMessageText )
{
  ...
}

最后用 STDAPI 宏声明标准导出的函数,如:

STDAPI DllRegisterServer()

STDAPI 包括返回类型和调用约定。使用 STDAPI 的一个缺点是,由于 STDAPI 的扩展方式,您不能使用 __declspec(dllexport) 。相反,您必须使用. def文件导出该函数。

(2) 服务器注册和注销

服务器实现了我前面提到的 DllRegisterServer()DllUnregisterServer() 函数。 他们的工作是创建和删除那些告诉COM关于我们的服务器的注册表项。代码都是无聊的注册表操作,所以我不在这里重复,但是这里有一个由 DllRegisterServer() 创建的注册表条目列表:

(3) 示例代码中的注意事项

所包含的示例代码包含COM服务器和测试客户端应用程序的源代码。项目文件 SimpleComSvrdsw ,您可以在服务器和客户端应用程序在同时加载和工作。在与工作空间相同的级别上有两个由两个项目使用的头文件。然后,每个项目都在自己的子目录中。

共用的两个头文件:

  • 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的路径将根据您提取源代码的位置而有所不同

不会飞的纸飞机
扫一扫二维码,了解我的更多动态。

下一篇文章:QSplitter QSS hover失效的解决办法