Click here to Skip to main content
13,259,976 members (50,148 online)
Click here to Skip to main content
Add your own
alternative version

Stats

5.9K views
5 downloads
30 bookmarked
Posted 2 Aug 2017

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

, 2 Aug 2017
Rate this:
Please Sign up or sign in to vote.
Using managed COM objects in C++ without registering the server in Windows Registry

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).

#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 exist.

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.

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:

#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:

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

Conclusions

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 shown how this can be done entirely programmatically without the need for additional configuration files.

License

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

Share

About the Author

Marius Bancila
Architect Visma Software
Romania Romania
Marius Bancila is the author of Modern C++ Programming Cookbook. He has been a Microsoft MVP for VC++ and later Visual Studio and Development Technologies for 11 years. He works as a system architect for Visma, a Norwegian-based company. He is mainly focused on building desktop applications with VC++ and VC#. He keeps a blog at http://www.mariusbancila.ro/blog, focused on Windows programming. He is the co-founder of codexpert.ro, a community for Romanian C++ programmers.

You may also be interested in...

Comments and Discussions

 
PraiseMy 5 Pin
Igor Ladnik15-Sep-17 19:51
professionalIgor Ladnik15-Sep-17 19:51 
GeneralMy vote of 5 Pin
Raul Iloc8-Sep-17 8:06
professionalRaul Iloc8-Sep-17 8: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.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.171114.1 | Last Updated 2 Aug 2017
Article Copyright 2017 by Marius Bancila
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid