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





5.00/5 (19投票s)
在 Windows 注册表中不注册服务器的情况下,
引言
在我之前的文章《通过 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()
函数的需要。这些宏的目的是定义稍后在代码中使用的智能指针类型 ICLRMetaHostPtr
、ICLRRuntimeInfoPtr
、ICorRuntimeHostPtr
、IAppDomainPtr
和 IObjectHandlePtr
。简要概述,这些接口定义了以下功能:
- 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 日:初始版本