Click here to Skip to main content
15,867,869 members
Articles / Programming Languages / VC++

Interoping .NET and C++ through Registration-free COM

Rate me:
Please Sign up or sign in to vote.
5.00/5 (19 votes)
2 Aug 2017CPOL5 min read 20.5K   163   44   2
Using managed COM objects in C++ without registering the server in Windows Registry

Introduction

In my previous article, Interoping .NET and C++ through COM, I have shown how to consume in C++ components written in .NET by exposing them through COM. The examples shown there relied on the COM server being registered in the Windows Registry so that components could be properly found and instantiated through the COM library. The very same COM interop mechanism for in-proc COM servers also works without registering the COM server by providing the server (i.e., the assembly) side-by-side with the client application. In this case, however, you need to either:

  • provide manifest files for the native COM application and the managed COM server, manifests that contain information about binding and activating the COM components.
  • explicitly write code for activating the managed COM components.

This article will focus on the second approach. If you are interested in the first, see the following articles:

Overview

In order to activate and consume managed COM objects from C++ in a registration-free manner, but without manifest files, we need to do the following:

  • Load and start the .NET runtime in the process (if not started already).
  • Instantiate objects in an app domain, by providing assembly and type name.
  • If explicitly started, stop the .NET runtime when no longer needed. In this article, we will start the runtime when the program starts and stop it when the program is about to exit.

Getting Started

In order to perform all these steps, we need to use several COM objects and interfaces. For this, we need to:

  • Import mscorlib.tlb type library.
  • Include the metahost.h header and link with the mscoree.lib static library.

To make the code easier to use (and reuse), all the utility code shown below will be put in a header called ManagedHost.h that contains a namespace called Managed and a class Host. The purpose of the Host class is to load and start the .NET runtime when an instance of the class is created and stop the runtime when the instance is destroyed (in an RTTI manner) and instantiate objects that implement dispatch COM interfaces. Because of the way the Managed::Host class handles the .NET runtime, you should only create one instance of it in your program. If it does not suit your application requirements, it should be fairly easy to modify the code (for instance, not to start the runtime if already started or stop it at some point).

C++
#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 is a macro that defines a _com_ptr_t COM smart pointer that hides the call to CoCreateInstance() for creating a COM object, encapsulates interface pointers and eliminates the need to call AddRef(), Release(), and QueryInterface() functions. The purpose of these macros is to define the smart pointer types ICLRMetaHostPtr, ICLRRuntimeInfoPtr, ICorRuntimeHostPtr, IAppDomainPtr, and IObjectHandlePtr used later in the code. As a short overview, these interfaces define the following functionalities:

  • ICLRMetaHost provides functionality for enumerating installed and loaded runtimes, get a specific runtime and other runtime operations.
  • ICLRRuntimeInfo provides functionality for retrieving information about a particular runtime version, directory, and load status, as well as some runtime-specific operations without initializing the runtime.
  • ICorRuntimeHost provides functionality for enabling the host to start and stop the common language runtime, to create and configure application domains, to access the default domain, and to enumerate all domains running in the process.
  • mscorlib::_AppDomain exposes the public members of the System.AppDomain class to unmanaged code.
  • mscorlib::_ObjectHandle exposes the public members of the System.Runtime.Remoting.ObjectHandle class to unmanaged code.

Hosting the CLR

In order to load and start the CLR in the process, we need to do the following:

  • Create an instance of the class implementing the ICLRMetaHost COM interface using the CLRCreateInstance() function.
  • Create an instance of the class implementing the ICLRRuntimeInfo COM interface using the meta host object and its GetRuntime() method and specifying a particular CLR version.
  • Create an instance of the class implementing the ICorRuntimeHost COM interface using the runtime info object and its GetInterface() method.
  • Start the CLR using the runtime host and its Start() method.

In order to stop the CLR from running in the current process, we need to use the ICorRuntimeHost object and call Stop(). This stops the execution of code in the runtime of the current process. This operation is not typically necessary when performed at the end of the process, since all code stops executing when the process exists.

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

Activating Objects

To create instances of COM classes that implement dispatch interfaces, we have to:

  • Get a reference to the current domain using the CurrentDomain() method of the ICorRuntimeHost interface.
  • Create an instance of the class specified by assembly and class name. The result is a pointer to an _ObjectHandle interface.
  • Get a pointer to the IDispatch interface of the actual underlying object using the Unwrap() method of the object handle.

The following methods are all public members of the Managed::Host class.

C++
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

To show how the code written above can be put to work, I will use the same code shown in the previous article that I wrote, Interoping .NET and C++ through COM. In that article, there was a sample that looked like this:

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

Using the Managed::Host class from above, this sample code would change to the following:

C++
#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;
}

Conclusion

Registration-free activation of .NET COM objects has the advantage that applications can be deployed just by copying files without Windows Registry involved (in regard to COM). The COM server assembly must be available locally to the application. Although it's possible to activate its components using manifest files, this article has shown how this can be done entirely programmatically without the need for additional configuration files.

History

  • 2nd August, 2017: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Visma Software
Romania Romania
Marius Bancila is the author of Modern C++ Programming Cookbook and The Modern C++ Challenge. He has been a Microsoft MVP since 2006, initially for VC++ and nowadays for Development technologies. He works as a system architect for Visma, a Norwegian-based company. He works with various technologies, both managed and unmanaged, for desktop, cloud, and mobile, mainly developing with VC++ and VC#. He keeps a blog at http://www.mariusbancila.ro/blog, focused on Windows programming. You can follow Marius on Twitter at @mariusbancila.

Comments and Discussions

 
PraiseMy 5 Pin
Igor Ladnik15-Sep-17 18:51
professionalIgor Ladnik15-Sep-17 18:51 
Very nice, thanks for sharing.
GeneralMy vote of 5 Pin
Raul Iloc8-Sep-17 7:06
Raul Iloc8-Sep-17 7:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.