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

简单 yet 可调试的 COM 骨架代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (50投票s)

2002 年 11 月 10 日

15分钟阅读

viewsIcon

256364

downloadIcon

2493

教程,演示如何从头开始构建 COM 组件(DLL、EXE、自动化)

引言

这篇文章和主题可能会引起代码专家的不适。但是,即使在 7 年后的今天,在当今以 Windows 为中心的世界中大量使用 COM 的情况下,我仍然经常看到人们对 COM 提出最简单的问题。

微软几年前推出了 ATL 库,希望现成的宏能够简化 COM 开发过程。但在实践中,宏往往会混淆代码的实际作用和预期作用。此外,本文的一个明显基础是 ATL 宏会生成不可调试的代码。宏由 C/C++ 预处理器展开,这意味着几乎不可能找出任何基于 ATL 的代码可能出现的问题。

这就是为什么我将在本文中展示如何从头开始编写工作的 COM 组件,而且不使用一个宏。希望您觉得有用。

本文的其余部分提供了三个(有望可重用)示例 COM 实现

  • 一个简单的 COM DLL
  • 一个支持自动化的 COM DLL
  • 一个支持自动化的 COM EXE

再加上几个工作的测试环境(C/C++ 和 VB)。

一个简单的 COM DLL

您可以在上面找到一个工作的 simplecomserver.zip 包,这是以下步骤的结果代码。

启动 VC6/7,创建一个名为 simplecomserver 的新项目。选择 WIN32 动态库项目向导,并选择简单的 DLL 项目选项。

到目前为止,您应该有一个包含 stdafx.h/cpp 预编译头文件的项目,以及带有以下代码的 simplecomserver.cpp

// simplecomserver.cpp : Defines the entry point for the DLL application.
//

#include "stdafx.h"

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
                     )
{
    return TRUE;
}

现在我们要声明我们的 COM 接口。让我们创建一个新的 .idl 文件(simplecomserver.idl),然后将以下内容粘贴进去

import "wtypes.idl";

[
    uuid(6F818C55-E6AD-488b-9EB6-511C0CCC0612),
    version(1.0)
]
library LibCOMServer
{
    importlib("stdole32.tlb");
    importlib("stdole.tlb"); 

    [ uuid(7F24AABF-C822-4c18-9432-21433208F4DC), 
      oleautomation 
    ]
    interface ICOMServer : IUnknown
    {
        HRESULT Name([out] BSTR* objectname);
    }


    [ uuid(6AE24C34-1466-482e-9407-90B98798A712),
      helpstring("COMServer object") 
    ]
    coclass CoCOMServer
    {
        [default] interface ICOMServer;
    }
}

我们有一个对象内的接口(coclass 关键字),它本身属于一组对象(这里只有一个)的集合,这些对象属于一个类型库(library 关键字)。接口、对象和类型库都有唯一的标识符:它是由一个工具生成的,例如 guidgen.exe(可从 MSDEV / Tools 菜单获得)。

simplecomserver.idl 添加到您的项目文件中,右键单击它并编译它。这会在 Debug 文件夹中生成类型库。 simplecomserver.tlb 是一个二进制编译的 IDL 文件,仅此而已。类型库是应用程序和语言将读取和解析的内容,以便在运行时提取接口名称、方法名称、参数限定符等。这就是为什么类型库如此重要的原因。

由于我们为这个 COM 服务器提供了 C/C++ 实现,因此我们还将要求类型库编译器生成一个接口头文件以供派生。右键单击 simplecomserver.idl,选择设置,然后在 MIDL 选项卡中,将“输出头文件名”字段设置为 simplecomserver_i.h。再次编译 IDL 文件。

这个自动生成的头文件有点复杂,因为它包含了 COM 库无法隐藏的许多实现细节。但我们并不真正关心它。我们只需要记住,头文件是一个类声明,我们将实现它。到目前为止这很简单,所以让我们创建一个新的头文件 simplecomserverImpl.h,并提供以下声明

#pragma once

// ICOMServer interface declaration ///////////////////////
//
//

class CoCOMServer : public ICOMServer
{
    // Construction
public:
    CoCOMServer();
    ~CoCOMServer();

    // IUnknown implementation
    //
    virtual HRESULT __stdcall QueryInterface
                        (const IID& iid, void** ppv) ;
    virtual ULONG __stdcall AddRef() ;
    virtual ULONG __stdcall Release() ;

    // ICOMServer implementation
    //
    virtual HRESULT __stdcall Name(/*out*/BSTR* objectname);

private:
    // Reference count
    long m_cRef ;
};

如果您将其与我们几分钟前在 .idl 文件中声明的内容进行比较,您会发现它看起来非常相似,并且完全用 C/C++ 语法表达。我们还可以注意到来自 IUnknown 接口的三个方法。这些方法通过提供路由功能(QueryInterface)和安全的引用计数(AddRefRelease)来帮助管理 COM 服务器的生命周期。但没有任何神秘之处或隐藏的技巧,我们将立即实现这些方法。让我们创建一个实现文件 simplecomserverImpl.cpp,并将以下代码粘贴进去

#include "stdafx.h"

#include <objbase.h> // 
#include "simplecomserver_i.h"

#include <atlbase.h> // CComBSTR

#include "simplecomserverImpl.h"

static long g_cComponents = 0 ; // Count of active components
static long g_cServerLocks = 0 ; // Count of locks

//
// Constructor
//
CoCOMServer::CoCOMServer() : m_cRef(1)
{ 
    InterlockedIncrement(&g_cComponents) ; 
}

//
// Destructor
//
CoCOMServer::~CoCOMServer() 
{ 
    InterlockedDecrement(&g_cComponents) ; 
}

//
// IUnknown implementation
//
HRESULT __stdcall CoCOMServer::QueryInterface
                      (const IID& iid, void** ppv)
{    
    if (iid == IID_IUnknown || iid == IID_ICOMServer)
    {
        *ppv = static_cast<ICOMServer*>(this) ; 
    }
    else
    {
        *ppv = NULL ;
        return E_NOINTERFACE ;
    }
    reinterpret_cast<IUnknown*>(*ppv)->AddRef() ;
    return S_OK ;
}

ULONG __stdcall CoCOMServer::AddRef()
{
    return InterlockedIncrement(&m_cRef) ;
}

ULONG __stdcall CoCOMServer::Release() 
{
    if (InterlockedDecrement(&m_cRef) == 0)
    {
        delete this ;
        return 0 ;
    }
    return m_cRef ;
}

这个代码骨架足以实现您在将来遇到的所有接口。当然,现在我们将添加我们自己的自定义实现

//
// ICOMServer implementation
//
HRESULT __stdcall CoCOMServer::Name(/*out*/BSTR* objectname)
{
    if (objectname==NULL)
        return ERROR_INVALID_PARAMETER;

    CComBSTR dummy;
    dummy.Append("hello world!");

    // Detach() returns an allocated BSTR string
    *objectname = dummy.Detach(); 

    return S_OK;
}

到目前为止,我们的 COM 服务器已经实现。当然,任何听说过 COM 的人都会知道 COM 服务器在使用前需要注册。确实,客户端应用程序,无论是什么应用程序,都将间接使用 COM 库,而 COM 库又会在注册表中查找已知 COM 服务器的字典(HKEY_CLASSES_ROOT\CLSID 键)。

因此,我们要做的就是确保我们的构建过程也能自动注册我们的 COM 服务器。这将需要几个步骤,您最好系好安全带!当然,好处是我们永远不需要手动创建许多注册表项,我的意思是,通过原始的、愚蠢的注册表相关代码。这将需要一些工作,因为我们需要注册 COM 对象本身、类型库和接口。

让我们开始添加一些导出函数。这些导出函数可以从外部访问,并允许注册工具(听说过 regsvr32.exe 吗?)请求注册。这就是 DllRegisterServer 函数。让我们添加一种新类型的文件:simplecomserver.def 并将以下内容粘贴进去

LIBRARY      "simplecomserver"

DESCRIPTION  'Proxy/Stub DLL'

EXPORTS
    DllCanUnloadNow         @1  PRIVATE
    DllGetClassObject       @2  PRIVATE
    DllRegisterServer       @3  PRIVATE
    DllUnregisterServer     @4  PRIVATE

.def 文件只是告诉链接器允许列表中的函数从外部访问。例如,您可以使用 dumpbin /exports 命令行(MSDEV 工具,需要在路径中包含 MSDEVDIR),甚至可以使用 Dependency Walker(另一个 MSDEV 工具)在将此文件添加到项目文件并构建项目后立即查看它们。

让我们为四个导出函数提供实现

///////////////////////////////////////////////////////////
//
// Exported functions
//

//
// Can DLL unload now?
//
STDAPI DllCanUnloadNow()
{
    if ((g_cComponents == 0) && (g_cServerLocks == 0))
    {
        return S_OK ;
    }
    else
    {
        return S_FALSE ;
    }
}

//
// Get class factory
//
STDAPI DllGetClassObject(const CLSID& clsid,
                         const IID& iid,
                         void** ppv)
{
    // Can we create this component?
    if (clsid != CLSID_CoCOMServer)
    {
        return CLASS_E_CLASSNOTAVAILABLE ;
    }

    // Create class factory.
    CFactory* pFactory = new CFactory ;  // Reference count set to 1
                                         // in constructor
    if (pFactory == NULL)
    {
        return E_OUTOFMEMORY ;
    }

    // Get requested interface.
    HRESULT hr = pFactory->QueryInterface(iid, ppv) ;
    pFactory->Release() ;

    return hr ;
}

//
// Server registration
//
STDAPI DllRegisterServer()
{
    HRESULT hr= RegisterServer(g_hModule, 
                               CLSID_CoCOMServer,
                               g_szFriendlyName,
                               g_szVerIndProgID,
                               g_szProgID,
                               LIBID_LibCOMServer) ;
    if (SUCCEEDED(hr))
    {
        RegisterTypeLib( g_hModule, NULL);
    }
    return hr;
}

//
// Server unregistration
//
STDAPI DllUnregisterServer()
{
    HRESULT hr= UnregisterServer(CLSID_CoCOMServer,
                                 g_szVerIndProgID,
                                 g_szProgID,
                                 LIBID_LibCOMServer) ;
    if (SUCCEEDED(hr))
    {
        UnRegisterTypeLib( g_hModule, NULL);
    }
    return hr;
}

///////////////////////////////////////////////////////////
//
// DLL module information
//
BOOL APIENTRY DllMain(HANDLE hModule,
                      DWORD dwReason,
                      void* lpReserved)
{
  if (dwReason == DLL_PROCESS_ATTACH)
  {
    g_hModule = (HMODULE)hModule ;
  }
  return TRUE ;
}

这里有几点说明:首先,这四个函数都带有 STDAPI 前缀,这只是告诉链接器导出它们而不进行任何 C++ 符号修饰(否则函数名周围会出现@和数字,目前来说这是不必要的)。我们还提供了一个新的 DllMain 实现,替换了类向导几分钟前提供的默认代码。现在您需要删除类向导在 simplecomserver.cpp 中为我们生成的代码(否则链接器会因重复的 DllMain() 实现而报错)。

DllRegisterServer()DllUnregisteServer() 是注册的入口点。此时,我们不深入细节,只记住类型库知道 COM 服务器的内部工作原理,并将执行大部分实际注册工作。

然后是 DllGetClassObject()。这是外部创建我们的 COM 服务器实例所使用的入口点。出于奇怪的原因,COM 的发明者希望有一些中间对象可以进行交互。所以,有了类工厂。这个对象本身的行为很像一个 COM 对象,尽管它没有关联的 IDL 文件。DllGetClassObject() 由 COM 管道代表外部应用程序调用,并期望返回一个有效的 IClassFactory 接口指针。这个接口(又是接口,一个简单的类)实现了 CreateInstance() 构造方法,而后者实际上会实例化我们的 COM 服务器。正如您所见,到目前为止,类工厂没什么可担心的。我们将这段代码复制/粘贴到 simplecomserverImpl.cpp

///////////////////////////////////////////////////////////
//
// Class factory
//
class CFactory : public IClassFactory
{
public:
  // IUnknown
  virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ;
  virtual ULONG   __stdcall AddRef() ;
  virtual ULONG   __stdcall Release() ;

  // Interface IClassFactory
  virtual HRESULT __stdcall CreateInstance(IUnknown* pUnknownOuter,
                                           const IID& iid,
                                           void** ppv) ;
  virtual HRESULT __stdcall LockServer(BOOL bLock) ; 

  // Constructor
  CFactory() : m_cRef(1) {}

  // Destructor
  ~CFactory() {;}

private:
  long m_cRef ;
} ;

//
// Class factory IUnknown implementation
//
HRESULT __stdcall CFactory::QueryInterface(const IID& iid, void** ppv)
{    
  if ((iid == IID_IUnknown) || (iid == IID_IClassFactory))
  {
    *ppv = static_cast<IClassFactory*>(this) ; 
  }
  else
  {
    *ppv = NULL ;
    return E_NOINTERFACE ;
  }
  reinterpret_cast<IUnknown*>(*ppv)->AddRef() ;
  return S_OK ;
}

ULONG __stdcall CFactory::AddRef()
{
  return InterlockedIncrement(&m_cRef) ;
}

ULONG __stdcall CFactory::Release() 
{
  if (InterlockedDecrement(&m_cRef) == 0)
  {
    delete this ;
    return 0 ;
  }
  return m_cRef ;
}

//
// IClassFactory implementation
//
HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnknownOuter,
                                           const IID& iid,
                                           void** ppv) 
{
  // Cannot aggregate.
  if (pUnknownOuter != NULL)
  {
    return CLASS_E_NOAGGREGATION ;
  }

  // Create component.
  CoCOMServer* pA = new CoCOMServer ;
  if (pA == NULL)
  {
    return E_OUTOFMEMORY ;
  }

  // Get the requested interface.
  HRESULT hr = pA->QueryInterface(iid, ppv) ;

  // Release the IUnknown pointer.
  // (If QueryInterface failed, component will delete itself.)
  pA->Release() ;
  return hr ;
}

// LockServer
HRESULT __stdcall CFactory::LockServer(BOOL bLock) 
{
  if (bLock)
  {
    InterlockedIncrement(&g_cServerLocks) ; 
  }
  else
  {
    InterlockedDecrement(&g_cServerLocks) ;
  }
  return S_OK ;
}

猜猜怎么着,我们完成了!

注册可以通过显式的regsvr32 <path>\Debug\simplecomserver.dll shell 命令执行,或者通过在项目中添加一个自定义构建步骤来执行。

一旦注册成功,您就可以在注册表中查找新添加的键

+ HKEY_CLASSES_ROOT

  +  COMServer.object
    +  CLSID = {6AE24C34-1466-482e-9407-90B98798A712}

  +  CLSID
    +  {6AE24C34-1466-482e-9407-90B98798A712} = "COMServer object"
      +  InProcServer32 = <path>\simplecomserver.dll

  +  Interface
    +  {7F24AABF-C822-4c18-9432-21433208F4DC} = "ICOMServer"
      + TypeLib = {6F818C55-E6AD-488B-9EB6-511C0CCC0612}

  +  TypeLib
    + {6F818C55-E6AD-488b-9EB6-511C0CCC0612}
      + 1.0
        + 0
          + win32 = <path>\simplecomserver.tlb

我希望所有这些注册表的混乱现在对您来说都更清楚了。基本上,您可以看到注册 COM 组件只是在正确的位置添加接口、对象和类型库的 ID 的问题。再次强调,这里没有隐藏的技巧。

出于任何原因,在执行注册时可能会出现问题,您可能不会收到成功的“DllRegisterServer in simplecomserver.dll succeeded.”消息。但不用担心,以下是调试注册的步骤

  • DllMain() 实现中,添加对 DllRegisterServer() 的调用。这样做,每次加载 DLL 时,DllRegisterServer() 都会被调用。请记住,这是一个临时更改,如果您在注册似乎工作正常时没有删除此调用,它将会在每次加载 DLL 时进行自我注册。
  • DllRegisterServer() 中设置一个断点。
  • 开始调试,并浏览任何启动进程。实际上,我们正在告诉调试器启动一个进程并自动加载我们的 DLL。
  • 调试注册代码。

如果您想注销您的组件,只需在 regsvr32 shell 命令中添加 -u 选项。结果,将调用 DllUnregisterServer()

任何有兴趣使用 C/C++ 客户端应用程序测试 COM 服务器的人,这是代码(请参阅 zip 包中的 TestSimplecomserver.dsp 项目)。

#include <atlbase.h>
#include "..\simplecomserver\simplecomserver_i.h" // interface declaration
#include "..\simplecomserver\simplecomserver_i.c" // IID, CLSID

::CoInitialize(NULL);

ICOMServer *p = NULL;

// create an instance
HRESULT hr = CoCreateInstance(    CLSID_CoCOMServer, NULL, CLSCTX_ALL,
                                IID_ICOMServer, 
                                (void **)&p);
if (SUCCEEDED(hr))
{
    // call the (only) method exposed by the main interface
    //
    BSTR message;
    p->Name(&message);

    // if everything went well, message holds the returned name
    //
    // ...
    CString szMessage;
    AfxBSTR2CString(message, szMessage);
    AfxMessageBox(szMessage);

    // don't forget to release the stuff
    ::SysFreeString(message);
}

p->Release();

::CoUninitialize();

我们的 COM 服务器的局限性?

  • 具有内置自动化语言(VB、Perl、Python 等)的应用程序尚无法与其配合使用。实际上,我们必须将接口从 IUnknown 派生到 IDispatch,并为四个方法提供默认实现。我们将在下一节中看到这一点。
  • 这是一个 DLL。要使 COM 服务器运行在一个单独的.EXE进程之外,我们需要一些额外的代码。我们将在下一节中看到这一点。

一个自动化 COM DLL

我们不会从头开始构建支持自动化的 COM DLL,因为它只是对简单 COM 服务器 DLL 的一个小改动。但是,您可以尝试自己做。

结果代码包含在 automcomserver.zip 包中。

我们要做的就是将 IUnknown 接口支持替换为 IDispatch 支持(它本身继承自 IUnknown,这就是为什么三个方法 QueryInterface()AddRef()Release() 不会消失的原因)。但为什么呢?基本上,IDispatch 接口提供了一种便捷的编程方法来解析类型库,并列出匿名 IDispatch 派生接口公开的方法名称。这旨在提供所谓的后期绑定。换句话说,借助后期绑定,客户端应用程序不再需要静态链接到它使用的接口。在简单的 COM 服务器 DLL 中,当我们包含 simplecomserver_i.h 接口声明时,我们实际上是在为编译器提供一个static函数vtable。这并不总是好的。后期绑定允许通过给出名称来检索接口函数的位置。从长远来看,这为软件开发人员提供了一个灵活的绑定系统。这项服务由 IDispatch 提供,它是一种指向类型库的指针。

但这还不够,参数呢?自动化语言应该能够在设计时甚至只在运行时即时猜测方法参数。在设计时,问题很容易解决。实际上,当您注册 COM 服务器时,您也注册了类型库,因此任何支持自动化的语言都可以代表您读取它,提取所有对象、接口、方法和参数,然后通过 intellisense(例如)公开它们。如果您使用的是 VB,通常会通过工具 \ 引用菜单添加类型库。然后,对象浏览器会列出所有提到的内容。因此,我们能够调用在设计时即时发现的方法,但运行时呢?在运行时,自动化引擎需要执行类型绑定,这就是 IDispatch 暴露的其他方法的作用。它们提供指向类型库的入口点以及操作系统公开的底层 API,允许提取参数的所有微小细节:它们是[in]吗?它们是BSTR吗?……

虽然所有这些听起来有点吓人,但我们不会纠缠于细节。事实上,大部分 IDispatch 实现将是 COM 库之一提供的默认实现。开始了

所以,让我们重新查看 .idl 接口,并对其进行以下更改

  • IUnknown 替换为 IDispatch,以反映 ICOMServer 接口现在支持自动化
  • Name 方法签名左侧添加 [id(1)] 前缀。此 id 将方法映射到 vtable 中的索引,并用于发现方法。

IDL 接口现在应该如下所示

import "wtypes.idl";

[
    uuid(6F818C55-E6AD-488b-9EB6-511C0CCC0612),
    version(1.0)
]
library LibCOMServer
{
    importlib("stdole32.tlb");
    importlib("stdole.tlb"); 

    [ uuid(7F24AABF-C822-4c18-9432-21433208F4DC),
         dual,
      oleautomation 
    ]
    interface ICOMServer : IDispatch
    {
        [id(1)] HRESULT Name([out, retval] BSTR* objectname);
    }


    [ uuid(6AE24C34-1466-482e-9407-90B98798A712),
      helpstring("COMServer object") 
    ]
    coclass CoCOMServer
    {
        [default] interface ICOMServer;
    }
}

如果您现在尝试构建项目,您会遇到错误,因为尽管我们从 IDispatch 派生,但我们尚未提供其实现。就这样吧

#pragma once

// ICOMServer interface declaration ///////////////////////
//
//

class CoCOMServer : public ICOMServer
{
  // Construction
public:
  CoCOMServer();
  ~CoCOMServer();

  // IUnknown implementation
  //
  virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) ;
  virtual ULONG __stdcall AddRef() ;
  virtual ULONG __stdcall Release() ;

  //IDispatch implementation
  virtual HRESULT __stdcall GetTypeInfoCount(UINT* pctinfo);
  virtual HRESULT __stdcall GetTypeInfo(UINT itinfo, 
                         LCID lcid, ITypeInfo** pptinfo);
  virtual HRESULT __stdcall GetIDsOfNames(REFIID riid, 
            LPOLESTR* rgszNames, UINT cNames,
            LCID lcid, DISPID* rgdispid);
  virtual HRESULT __stdcall Invoke(DISPID dispidMember, REFIID riid,
            LCID lcid, WORD wFlags, 
            DISPPARAMS* pdispparams, VARIANT* pvarResult,
            EXCEPINFO* pexcepinfo, UINT* puArgErr);

  // ICOMServer implementation
  //
  virtual HRESULT __stdcall Name(/*out*/BSTR* objectname);

private:

  HRESULT LoadTypeInfo(ITypeInfo ** pptinfo, 
       const CLSID& libid, const CLSID& iid, LCID lcid);

  // Reference count
  long          m_cRef ;
  LPTYPEINFO    m_ptinfo; // pointer to type-library
};

现在是实现部分。首先,让我们加载类型库(实际上我们加载 .tlb 文件)

//
// Constructor
//
CoCOMServer::CoCOMServer() : m_cRef(1)
{ 
  InterlockedIncrement(&g_cComponents) ; 

  m_ptinfo = NULL;
  LoadTypeInfo(&m_ptinfo, LIBID_LibCOMServer, IID_ICOMServer, 0);
}

HRESULT CoCOMServer::LoadTypeInfo(ITypeInfo ** pptinfo, 
                                  const CLSID &libid, 
                                  const CLSID &iid, 
                                  LCID lcid)
{
  HRESULT hr;
  LPTYPELIB ptlib = NULL;
  LPTYPEINFO ptinfo = NULL;

  *pptinfo = NULL;

  // Load type library.
  hr = ::LoadRegTypeLib(libid, 1, 0, lcid, &ptlib);
  if (FAILED(hr))
    return hr;

  // Get type information for interface of the object.
  hr = ptlib->GetTypeInfoOfGuid(iid, &ptinfo);
  if (FAILED(hr))
  {
    ptlib->Release();
    return hr;
  }

  ptlib->Release();
  *pptinfo = ptinfo;
  return NOERROR;
}

我们还允许外部请求 IDispatch 接口,因为我们现在完全支持它

HRESULT __stdcall CoCOMServer::QueryInterface(const IID& iid, void** ppv)
{    
  if (iid == IID_IUnknown || iid == IID_ICOMServer || iid == IID_IDispatch)
  {
    *ppv = static_cast<ICOMServer*>(this) ; 
  }
  else
  {
    *ppv = NULL ;
    return E_NOINTERFACE ;
  }
  reinterpret_cast<IUnknown*>(*ppv)->AddRef() ;
  return S_OK ;
}

我们还为 IDispatch 接口本身提供了默认实现

HRESULT __stdcall CoCOMServer::GetTypeInfoCount(UINT* pctinfo)
{
  *pctinfo = 1;
  return S_OK;
}

HRESULT __stdcall CoCOMServer::GetTypeInfo
    (UINT itinfo, LCID lcid, ITypeInfo** pptinfo)
{
  *pptinfo = NULL;

  if(itinfo != 0)
    return ResultFromScode(DISP_E_BADINDEX);

  m_ptinfo->AddRef(); // AddRef and return pointer to cached
                         // typeinfo for this object.
  *pptinfo = m_ptinfo;

  return NOERROR;
}

HRESULT __stdcall CoCOMServer::GetIDsOfNames
       (REFIID riid, LPOLESTR* rgszNames, UINT cNames,
        LCID lcid, DISPID* rgdispid)
{
  return DispGetIDsOfNames(m_ptinfo, rgszNames, cNames, rgdispid);
}

HRESULT __stdcall CoCOMServer::Invoke(DISPID dispidMember, REFIID riid,
    LCID lcid, WORD wFlags, DISPPARAMS* pdispparams, VARIANT* pvarResult,
    EXCEPINFO* pexcepinfo, UINT* puArgErr)
{
  return DispInvoke(
               this, m_ptinfo,
               dispidMember, wFlags, pdispparams,
               pvarResult, pexcepinfo, puArgErr); 
}

构建项目,并注册组件。

又完成了!此 COM 组件支持自动化语言、早期绑定和后期绑定。

让我们玩玩。运行 MS Word,然后进入 Visual Basic 编辑器,并复制/粘贴此代码片段

Sub Macro1()

  Dim obj As ICOMServer
  Set obj = CreateObject("COMServer.object")
  Dim szname As String
  szname = obj.Name ' explicit Name([out,retval] BSTR*) method call
  MsgBox szname

End Sub

在运行它之前,不要忘记进入工具 \ 引用并为 automcomserver.tlb 类型库添加一个引用。

我们不传递 string 作为输入参数的原因(与 .idl 接口建议的相反)是,当一个参数明确标记为 [out,retval] 时,它实际上是一个结果值,因此赋值给 szname 变量。

一个自动化 COM EXE

出于多种原因,最好在一个单独的进程中运行 COM 组件。出于安全和性能原因的进程隔离只是举几个例子。但这就是事情开始变得有点棘手的地方。事实上,对于最终用户来说,.exe COM 服务器的使用方式与 .dll 几乎相同:CoCreateInstanceQueryInterface、方法调用、Release。但开发人员必须处理所有实现细节。

我们将重用我们的代码,添加一个消息循环,并将类对象注册到 COM 库知道的另一个表,该表仅用于进程。

结果代码包含在 automexeserver.zip 包中。

让我们创建一个新项目,这次是 Win32 应用程序而不是 Win32 动态库,并将其命名为 automexeserver。确保 automexeserver.cpp 包含以下内容

// automexeserver.cpp : Defines the entry point for the application.
//

#include "stdafx.h"

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
     // TODO: Place code here.

    return 0;
}

现在复制、重命名并将所有这些文件添加到此项目中

  • automcomserver.idl ==> automexeserver.idl
  • automcomserverImpl.h ==> automexeserverImpl.h
  • automcomserverImpl.cpp ==> automexeserverImpl.cpp
  • Registry.h .cpp ==> Registry.h .cpp(此处无需更改)

当然,您需要在文件中进行一些内部更改以反映新的 automexeserver 名称:您需要替换引用 automcomserver 头文件的 #include 语句,并且需要进入项目设置并更改 automexeserver.idl 的 MIDL 选项:将“输出头文件名”字段填写为 automexeserver_i.h

正如您可能已经猜到的,我们不再需要 automcomserver.def,它用于声明导出函数。事实上,进程不需要导出任何函数,因为将使用的方法调用是 LPC/RPC,而不是我们到目前为止看到的简单的进程内方法调用。无需惊慌,我们不会深入研究封送处理的狂热。所以放松吧。

构建项目是可以的,但它还不能如预期那样工作。

首先,让我们给应用程序入口点添加一些功能。在这里,我们将针对 COM 进程的内部表进行初始化,然后通过可中断的消息循环。 automexeserver.cpp 应该完全反映这一点

// automexeserver.cpp : Defines the entry point for the application.
//

#include "stdafx.h"
#include <objbase.h> // 
#include "automexeserver_i.h"
#include "automexeserverImpl.h"

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{

  // register/unregister server on demand
  //
  char szUpperCommandLine[MAX_PATH];
  // copy command line and work with it.
  strcpy (szUpperCommandLine, lpCmdLine); 
  strupr (szUpperCommandLine);
  if (strstr (szUpperCommandLine, "UNREGSERVER"))
  {
    DllUnregisterServer();
    return 0;
  }
  else if (strstr (szUpperCommandLine, "REGSERVER"))
  {
    DllRegisterServer();
    return 0;
  }

  // initialize the COM library
  ::CoInitialize(NULL);

  // register ourselves as a class object against the internal COM table
  // (this has nothing to do with the registry)
  DWORD nToken = CoEXEInitialize();

  // -- the message poooommmp ----------------
  //
  // (loop ends if WM_QUIT message is received)
  //
  MSG msg;
  while (GetMessage(&msg, 0, 0, 0) > 0) 
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  // unregister from the known table of class objects
  CoEXEUninitialize(nToken);

  // 
  ::CoUninitialize();

  return 0;
}

需要一些说明

  • 正如您所见,用于启动进程的命令行被检查是否可能带有 /regserver/unregserver 参数。确实,COM 进程不是使用标准的 regsvr32 命令行注册的。我们拥有 DllRegisterServer()DllUnregisterServer() 的事实不应被误解。我们拥有一直使用的相当方便的注册助手。所以再次使用它们是明智的。但是 Dll 前缀并不意味着我们正在注册一个 DLL。事实上,注册的几乎唯一的区别是我们最终有一个 LocalServer32 注册表项而不是 InProcServer32
  • 然后我们将自己注册到内部 COM 类对象表中(由 R.O.T. 使用)。
  • 然后是消息循环,等待 WM_QUIT 消息,同时处理沿途的所有其他消息。需要注意的是,当您手动杀死进程时,操作系统会发送此 WM_QUIT 消息。我们将模仿这一点,以便在客户端应用程序不再需要我们时自我销毁。

为了完成实现,我们在 automexeserverImpl.cpp 中提供了这四个函数:DllRegisterServer()DllUnregisterServer()CoEXEInitialize()CoEXEUninitialize()

CFactory gClassFactory;

DWORD CoEXEInitialize()
{
  DWORD nReturn;

  HRESULT hr=::CoRegisterClassObject(CLSID_CoCOMServer,
                                     &gClassFactory,
                                     CLSCTX_SERVER, 
                                     REGCLS_MULTIPLEUSE, 
                                     &nReturn);

  return nReturn;
}

void CoEXEUninitialize(DWORD nToken)
{
  ::CoRevokeClassObject(nToken);
}

//
// Server registration
//
STDAPI DllRegisterServer()
{
  g_hModule = ::GetModuleHandle(NULL);

  HRESULT hr= RegisterServer(g_hModule, 
                             CLSID_CoCOMServer,
                             g_szFriendlyName,
                             g_szVerIndProgID,
                             g_szProgID,
                             LIBID_LibCOMServer) ;
  if (SUCCEEDED(hr))
  {
    RegisterTypeLib( g_hModule, NULL);
  }
  return hr;
}

//
// Server unregistration
//
STDAPI DllUnregisterServer()
{
  g_hModule = ::GetModuleHandle(NULL);

  HRESULT hr= UnregisterServer(CLSID_CoCOMServer,
                               g_szVerIndProgID,
                               g_szProgID,
                               LIBID_LibCOMServer) ;
  if (SUCCEEDED(hr))
  {
    UnRegisterTypeLib( g_hModule, NULL);
  }
  return hr;
}

别忘了,当客户端应用程序告诉我们时,我们需要销毁自己。让我们更新 CoCOMServer::Release() 的实现

ULONG __stdcall CoCOMServer::Release() 
{
  if (InterlockedDecrement(&m_cRef) == 0)
  {
    delete this ;
    ::PostMessage(NULL,WM_QUIT,0,0);
    return 0 ;
  }
  return m_cRef ;
}

现在您可以构建此项目了。在使用 COM 服务器之前,不要忘记命令行:automexeserver.exe /regserver。您可以使用 VB 代码片段来测试它(不要忘记引用类型库,现在是 automexeserver.tlb)。

如果您想使用 C/C++ 客户端应用程序测试组件,可以使用 TestAutomexeserver.dsp 项目(与 TestSimplecomserver.dsp 的唯一区别是包含了正确的接口头文件。经验表明,如果包含错误的文件,运行时几乎肯定会出现 GPF!)。

终于完成了!这很难吗?

本文为您提供了三个实际的代码示例,反映了您在接近 COM 边界时需要的三种代码。

希望您觉得它们有用。

历史

  • 2002 年 11 月 10 日:初始版本

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

一个简单但可调试的 COM 骨架代码 - CodeProject - 代码之家
© . All rights reserved.