65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (147投票s)

2001年1月12日

viewsIcon

875104

downloadIcon

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库可以使用进程内服务器之前,它必须满足两个条件:

  1. 它必须在HKEY_CLASSES_ROOT\CLSID键下正确注册。
  2. 它必须导出一个名为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()中有三件事需要做:

  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()时我们的服务器是如何被使用的。

CoCreateInstance()内部

在第一篇COM入门文章中,我们看到了CoCreateInstance() API,它在客户端请求时创建一个COM对象。从客户端的角度来看,它是一个黑盒子。只需用正确的参数调用CoCreateInstance(),然后 BAM! 你就得到了一个COM对象。当然,没有什么魔法,而是一个定义明确的过程:COM服务器被加载,创建请求的COM对象,并返回请求的接口。

以下是该过程的快速概述。这里有一些不熟悉的术语,但别担心;我将在后面的章节中全部介绍。

  1. 客户端程序调用CoCreateInstance(),传递coclass的CLSID和它想要的接口的IID。
  2. COM库在HKEY_CLASSES_ROOT\CLSID下查找服务器的CLSID。此键包含服务器的注册信息。
  3. COM库读取服务器DLL的完整路径,并将DLL加载到客户端的进程空间中。
  4. COM库调用服务器中的DllGetClassObject()函数,以请求所请求coclass的类工厂。
  5. 服务器创建一个类工厂并从DllGetClassObject()返回它。
  6. COM库调用类工厂中的CreateInstance()方法来创建客户端程序请求的COM对象。
  7. 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对象,即将包含新对象的对象。聚合完全超出了本文的范围,我们的示例对象将不支持聚合。

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;
}

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运算符检索之前通过__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,看看它是否是我们类工厂实现的接口之一: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;
}

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.

转换完成后,我们显示消息框,然后返回。

    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 client screen shot - 12K]

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_()有对应的宏用于实现方法——STDMETHODIMPSTDMETHODIMP_()。例如,这是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在开始调试客户端时加载服务器的符号。

项目依赖项对话框应如下所示:

 [Project dependencies - 7K]

要加载服务器的符号,打开TestClient项目设置,转到Debug选项卡,并在Category组合框中选择Additional DLLs。单击列表框添加一个新条目,然后输入服务器DLL的完整路径。例如:

 [Debug settings - 15K]

当然,DLL的路径将根据你解压源代码的位置而有所不同。

© . All rights reserved.