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

通过无注册 COM 实现 .NET 和 C++ 的互操作

starIconstarIconstarIconstarIconstarIcon

5.00/5 (19投票s)

2017 年 8 月 2 日

CPOL

5分钟阅读

viewsIcon

22583

downloadIcon

192

在 Windows 注册表中不注册服务器的情况下, 在 C++ 中使用托管 COM 对象

引言

在我之前的文章《通过 COM 实现 .NET 和 C++ 的互操作》中,我展示了如何通过 COM 公开 .NET 组件,以便在 C++ 中使用它们。其中展示的示例依赖于 COM 服务器在 Windows 注册表中进行了注册,以便 COM 库能够正确找到并实例化组件。对于进程内 COM 服务器,相同的 COM 互操作机制在不注册 COM 服务器的情况下也能工作,只需将服务器(即程序集)与客户端应用程序并排放置即可。在这种情况下,您需要执行以下任一操作:

  • 为原生 COM 应用程序和托管 COM 服务器提供清单文件,这些文件包含有关绑定和激活 COM 组件的信息。
  • 显式编写代码来激活托管 COM 组件。

本文将重点介绍第二种方法。如果您对第一种方法感兴趣,请参阅以下文章:

概述

为了以无注册方式,但不需要清单文件的方式,从 C++ 激活和使用托管 COM 对象,我们需要执行以下操作:

  • 加载并启动进程中的 .NET 运行时(如果尚未启动)。
  • 通过提供程序集和类型名称,在应用程序域中实例化对象。
  • 如果显式启动了 .NET 运行时,则在不再需要时停止它。在本文中,我们将在程序启动时启动运行时,并在程序即将退出时停止它。

入门

为了执行所有这些步骤,我们需要使用几个 COM 对象和接口。为此,我们需要:

  • 导入 mscorlib.tlb 类型库。
  • 包含 metahost.h 头文件并链接 mscoree.lib 静态库。

为了使代码更易于使用(和重用),下面展示的所有实用工具代码都将放在一个名为 ManagedHost.h 的头文件中,其中包含一个名为 Managed 的命名空间和一个名为 Host 的类。Host 类的目的是在创建类的实例时加载和启动 .NET 运行时,并在实例销毁时停止运行时(以 RTTI 的方式),并实例化实现 dispatch COM 接口的对象。由于 Managed::Host 类处理 .NET 运行时的方式,您应该只在程序中创建它的一个实例。如果它不适合您的应用程序需求,修改代码(例如,如果运行时已启动则不启动,或在某个时候停止它)应该相当容易。

#pragma once

#import <mscorlib.tlb> raw_interfaces_only high_property_prefixes
    ("_get","_put","_putref") rename( "value", "value2" )
     rename( "ReportEvent", "InteropServices_ReportEvent" )

#include <comdef.h>
#include <metahost.h>
#pragma comment( lib, "Mscoree" )

namespace Managed
{
   _COM_SMARTPTR_TYPEDEF(ICLRMetaHost, IID_ICLRMetaHost);
   _COM_SMARTPTR_TYPEDEF(ICLRRuntimeInfo, IID_ICLRRuntimeInfo);
   _COM_SMARTPTR_TYPEDEF(ICorRuntimeHost, IID_ICorRuntimeHost);
   typedef mscorlib::_AppDomain IAppDomain;
   _COM_SMARTPTR_TYPEDEF(IAppDomain, __uuidof(IAppDomain));
   typedef mscorlib::_ObjectHandle IObjectHandle;
   _COM_SMARTPTR_TYPEDEF(IObjectHandle, __uuidof(IObjectHandle));
}

_COM_SMARTPTR_TYPEDEF 是一个宏,它定义了一个 _com_ptr_t COM 智能指针,它隐藏了调用 CoCreateInstance() 来创建 COM 对象,封装了接口指针,并消除了调用 AddRef()Release()QueryInterface() 函数的需要。这些宏的目的是定义稍后在代码中使用的智能指针类型 ICLRMetaHostPtrICLRRuntimeInfoPtrICorRuntimeHostPtrIAppDomainPtrIObjectHandlePtr。简要概述,这些接口定义了以下功能:

  • ICLRMetaHost 提供了枚举已安装和已加载的运行时、获取特定运行时以及其他运行时操作的功能。
  • ICLRRuntimeInfo 提供了检索有关特定运行时版本、目录和加载状态的信息,以及一些不初始化运行时即可执行的运行时特定操作的功能。
  • ICorRuntimeHost 提供了使宿主能够启动和停止公共语言运行时、创建和配置应用程序域、访问默认域以及枚举进程中所有正在运行的域的功能。
  • mscorlib::_AppDomain 向非托管代码公开了 System.AppDomain 类的 public 成员。
  • mscorlib::_ObjectHandle 向非托管代码公开了 System.Runtime.Remoting.ObjectHandle 类的 public 成员。

宿主 CLR

为了在进程中加载和启动 CLR,我们需要执行以下操作:

  • 使用 CLRCreateInstance() 函数创建一个实现 ICLRMetaHost COM 接口的类的实例。
  • 使用元宿主对象及其 GetRuntime() 方法创建一个实现 ICLRRuntimeInfo COM 接口的类的实例,并指定一个特定的 CLR 版本。
  • 使用运行时信息对象及其 GetInterface() 方法创建一个实现 ICorRuntimeHost COM 接口的类的实例。
  • 使用运行时宿主及其 Start() 方法启动 CLR。

为了停止 CLR 在当前进程中运行,我们需要使用 ICorRuntimeHost 对象并调用 Stop()。这会停止当前进程运行时中代码的执行。当在进程结束时执行此操作时,通常不需要,因为当进程退出时,所有代码都会停止执行。

class Host
{
public:
  Host()
  {
     ICLRMetaHostPtr pMetaHost{ nullptr };
     HRESULT hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
     if (FAILED(hr))
        return;

     ICLRRuntimeInfoPtr pRuntimeInfo{ nullptr };
     hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&pRuntimeInfo));
     if (FAILED(hr))
        return;

     hr = pRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_PPV_ARGS(&m_pCorHost));
     if (FAILED(hr))
     {
        m_pCorHost = nullptr;
        return;
     }

     hr = m_pCorHost->Start();
     if (FAILED(hr))
     {
        m_pCorHost = nullptr;
        return;
     }
  }

  ~Host()
  {
     if (m_pCorHost != nullptr)
     {
        m_pCorHost->Stop();
        m_pCorHost = nullptr;
     }
  }

private:
  ICorRuntimeHostPtr m_pCorHost{ nullptr };
};

激活对象

要创建实现 dispatch 接口的 COM 类的实例,我们必须:

  • 使用 ICorRuntimeHost 接口的 CurrentDomain() 方法获取当前域的引用。
  • 创建由程序集和类名称指定的类的实例。结果是指向 _ObjectHandle 接口的指针。
  • 使用对象句柄的 Unwrap() 方法获取实际底层对象的 IDispatch 接口的指针。

以下方法都是 Managed::Host 类的 public 成员。

HRESULT GetComObject(LPCTSTR assembly, LPCTSTR className, IDispatchPtr& result)
{
   IAppDomainPtr pAppDomain{ nullptr };
   HRESULT hr = GetCurrentAppDomain(pAppDomain);
   if (FAILED(hr))
      return hr;

   IObjectHandlePtr pObjHandle{ nullptr };
   hr = pAppDomain->CreateInstance(
      _bstr_t(assembly), 
      _bstr_t(className), 
      &pObjHandle);
   if (FAILED(hr))
      return hr;

   _variant_t vObj;
   hr = pObjHandle->Unwrap(&vObj);
   if(SUCCEEDED(hr))
      result = static_cast<IDispatch*>(vObj.pdispVal);

   return hr;
}

IDispatchPtr GetComObject(LPCTSTR assembly, LPCTSTR className)
{
   IDispatchPtr ptr{ nullptr };
   HRESULT hr = GetComObject(assembly, className, ptr);
   if (SUCCEEDED(hr))
      return ptr;

   return nullptr;
}

HRESULT GetCurrentAppDomain(IAppDomainPtr& pAppDomain)
{
   if (m_pCorHost == nullptr)
      return E_FAIL;

   IUnknownPtr pUnk{ nullptr };
   HRESULT hr = m_pCorHost->CurrentDomain(&pUnk);

   if (FAILED(hr))
      return hr;

   pAppDomain = pUnk;

   return S_OK;
}

Using the Code

为了展示上述代码如何发挥作用,我将使用我在上一篇文章《通过 COM 实现 .NET 和 C++ 的互操作》中展示的相同代码。在那篇文章中,有一个示例如下:

#include <iostream>
#import "ManagedLib.tlb"

struct COMRuntime
{
   COMRuntime() { CoInitialize(NULL); }
   ~COMRuntime() { CoUninitialize(); }
};

int main()
{
   COMRuntime runtime;
   ManagedLib::ITestPtr ptr;
   ptr.CreateInstance(L"ManagedLib.Test");
   if (ptr != nullptr)
   {
      try
      {   
         ptr->TestBool(true);
         ptr->TestSignedInteger(CHAR_MAX, SHRT_MAX, INT_MAX, MAXLONGLONG);
      }
      catch (_com_error const & e)
      {
         std::wcout << (wchar_t*)e.ErrorMessage() << std::endl;
      }      
   }
   
   return 0;
}

使用上面的 Managed::Host 类,此示例代码将更改为以下内容:

#include <iostream>
#import "ManagedLib.tlb"
#include "ManagedHost.h"

int main()
{
   Managed::Host host;
   ManagedLib::ITestPtr ptr = host.GetComObject(L"ManagedLib", L"ManagedLib.Test");

   if (ptr != nullptr)
   {
      try
      {   
         ptr->TestBool(true);
         ptr->TestSignedInteger(CHAR_MAX, SHRT_MAX, INT_MAX, MAXLONGLONG);
      }
      catch (_com_error const & e)
      {
         std::wcout << (wchar_t*)e.ErrorMessage() << std::endl;
      }      
   }
   
   return 0;
}

结论

对 .NET COM 对象进行无注册激活的好处是,应用程序只需复制文件即可部署,而无需涉及 Windows 注册表(就 COM 而言)。COM 服务器程序集必须在应用程序本地可用。尽管可以使用清单文件激活其组件,但本文已展示了如何完全以编程方式完成此操作,而无需额外的配置文件。

历史

  • 2017 年 8 月 2 日:初始版本
© . All rights reserved.