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


    文章目录

    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.

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

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

  • 相关阅读:
    快速入门Elasticsearch:安装、基本概念、分词器和文档基本操作详解
    [华为杯研究生创新赛 2023] 初赛 REV WP
    基于.Net开发的ChatGPT客户端,兼容Windows、IOS、安卓、MacOS、Linux
    【分布式系统】面向结构化数据的分布式存储
    [从零开始学习FPGA编程-41]:视野篇 - 摩尔时代与摩尔定律以及后摩尔时代的到来
    秋招大厂184道阿里、百度、腾讯、头条Java面试题合集
    【0104】查找PostgreSQL数据库和表的大小
    第五篇 《随机点名答题系统》——抽点答题详解(类抽奖系统、在线答题系统、线上答题系统、在线点名系统、线上点名系统、在线考试系统、线上考试系统)
    冰河世纪:AIOps的落地实践之路
    堆--完全二叉树
  • 原文地址:https://blog.csdn.net/lvwenshuai/article/details/133825746