Click here to Skip to main content
15,860,859 members
Articles / Programming Languages / C++/CLI

CLR Hosting - Customizing the CLR

Rate me:
Please Sign up or sign in to vote.
4.89/5 (35 votes)
10 Jul 2012Ms-PL7 min read 76.1K   2.6K   76   18
We go through the basics and create a simple AppDomainManager.

Image 1

Introduction

Are you interested in what makes the .NET runtime tick or in need to change the behavior of the .NET runtime to your needs? Then this article is for you. This is part 1, where we go through the basics and create a simple AppDomainManager. In part 2, we will implement AppDomainManagers with functionality for sandboxing, exception handling, and alternative assembly loading.

Background

This article is a result of an educational endeavor from my side. I have a very strong interest in debugging and testing so most of my articles are related to that, either directly as how to write debugger extensions or indirectly such as increasing logging capabilities or exploring APIs. My last article was about Building a mixed mode sampling profiler which was something I needed. When I started exploring the API related to this article, I didn't have a special need for it, I did it out of curiosity. At this point, I can see several interesting points.

How does a .NET application start?

It is too soon to talk about this API. Let's step back a little.

How does Windows know that a binary is a .NET application? Actually the answer varies depending on which version of Windows you run.

Normally .exe files are executed by Windows by looking at the PE-Header. This PE-Header says how it should be loaded into memory, what dependencies it has, and where the entry point is.

Where is the entry-point of a .NET application? Well, your application is in some IL-code. Executing that directly will clearly lead to a crash. It is not the IL-code that should start executing, but the .NET runtime, which eventually should load the IL-code and execute it.

In newer versions of Windows, .NET comes preinstalled, and Windows has built-in support for recognizing a .NET application. This can be done by simply looking in the PE-Header present in all executables and DLLs. In older versions of Windows, execution is passed to an entry point where boot-strapper code is located. The boot-strapper, which is native code, uses an unmanaged CLR Hosting API, to start the .NET runtime inside the current process and launch the real program which is the IL-code.

The CLR Hosting API

Hosting the CLR in an unmanaged app

When you start the .NET runtime inside a native process, that native application becomes a host for the runtime. This lets you add .NET capabilities to your native applications.

MC++
#include <metahost.h>
#include <mscoree.h>
#pragma comment(lib, "mscoree.lib")

ICLRMetaHost    *pMetaHost     = nullptr;
ICLRRuntimeHost *pRuntimeHost  = nullptr;
ICLRRuntimeInfo *pRuntimeInfo  = nullptr;
HRESULT hr;

hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&pMetaHost);
hr = pMetaHost->GetRuntime(runtimeVersion, IID_PPV_ARGS(&pRuntimeInfo));
hr = pRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost,IID_PPV_ARGS(&pRuntimeHost));
hr = pRuntimeHost->Start();

Now the runtime is running, but it hasn't got any loaded user code yet. Some internal thread scheduler and garbage collector are surely running, because they are part of the CLR runtime.

Running managed code

To start a managed app from our host, what we would like to do is something like this:

C#
AppDomain.CurrentDomain.ExecuteAssembly(assemblyName);

Old API

Well, it is was possible in an early version of the CLR hosting interface, through an API called GetDefaultDomain, which returned an AppDomain.

MC++
HRESULT hr = CorBindToRuntimeEx(..., IID_ICorRuntimeHost, (void**)&pRuntimeHost);
hr = pRuntimeHost->Start();
_AppDomain* pCurrentDomain = nullptr;
hr = pRuntimeHost->GetDefaultDomain(&pCurrentDomain);
pCurrentDomain.ExecuteAssembly(assemblyFilename);

But for good reasons this interface has been deprecated. The old API had huge parts in unmanaged code, which proved to be a great disadvantage. Retrieving values or manipulating objects from an AppDomain in unmanaged code resulted in a lot of Marshaling (a.k.a. Serialization), which severely affects performance. The marshaling was also implicit, so it was not always obvious where it took place. Sometimes even the whole AppDomain was marshaled. So we will not use this interface, but instead use the new one and keep all our custom code running within an AppDomain.

New API

The new version of the CLR Hosting interfaces has been reworked. Much of the API has been moved from unmanaged code to managed code. In order to obtain an AppDomain instance, one has to register an AppDomainManager implementation. The good part is that it is much easier and faster to develop in C#. The code also gets cleaner, because you don't have to write as much boilerplate code.

To be able to register a new AppDomainManager, we will need an interface called ICLRControl. This interface contains a method SetAppDomainManagerType, which loads your managed implementation of the AppDomainManager.

MC++
ICLRControl* pCLRControl = nullptr;
hr = pRuntimeHost->GetCLRControl(&pCLRControl);
LPCWSTR assemblyName = L"SampleAppDomainManager";
LPCWSTR appDomainManagerTypename = L"SampleAppDomainManager.CustomAppDomainManager";
hr = pCLRControl->SetAppDomainManagerType(assemblyName, appDomainManagerTypename);

That is what you need to do to override it. You just need an implementation to go with it. I have made a basic one in managed code, called CustomAppDomainManager. Below is the source listing of the implementation of my CustomAppDomainManager (SampleAppDomainManager.dll).

C#
[GuidAttribute("0C19678A-CE6C-487B-AD36-0A8B7D7CC035"), ComVisible(true)]
public sealed class CustomAppDomainManager : AppDomainManager, ICustomAppDomainManager
{
  public CustomAppDomainManager()
  {
     System.Console.WriteLine("*** Instantiated CustomAppDomainManager");
  }

  public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
  {
     System.Console.WriteLine("*** InitializeNewDomain");
     this.InitializationFlags = AppDomainManagerInitializationOptions.RegisterWithHost;
  }

  public override AppDomain CreateDomain(string friendlyName,
          Evidence securityInfo, AppDomainSetup appDomainInfo)
  {
     var appDomain = base.CreateDomain(friendlyName, securityInfo, appDomainInfo);
     System.Console.WriteLine("*** Created AppDomain {0}", friendlyName);
     return appDomain;
  }
}

Now you can, for example, execute a method residing in an assembly like this. Notice that since we are not using the AppDomain directly we will avoid all data marshaling.

MC++
hr = pRuntimeHost->Start();
DWORD returnValue = 0;
// Executing public static Start(string arg)
hr = pRuntimeHost->ExecuteInDefaultAppDomain(totalPath, L"SampleApp1.Program", L"Start", L"", &returnValue);
hr = pRuntimeHost->Stop();

Running the sample app now gives us the following output:

Image 2

What we really wanted was to execute the Main method of the assembly directly, via the ExecuteAssembly call like we did on the AppDomain. There is a minor problem. There is no ExecuteAssembly method, but there is an ExecuteApplication method we can try instead.

MC++
int retVal = 0;
LPCWSTR dummy = L"";
DWORD dwManifestPaths = 0;
DWORD dwActivationData = 0;
hr = pRuntimeHost->ExecuteApplication(totalPath, 
                                      dwManifestPaths,
                                      &dummy,
                                      dwActivationData,
                                      &dummy,
                                      &retVal);

Unfortunately, I didn't get it to work. The documentation says something about manifests and Click-Once deployment. The only error I get is E_UNEXPECTED as the HRESULT error. This is a minor problem, since we can work around it easily when we create our CustomAppDomainManager implementation, simply by adding a method, which calls ExecuteAssembly either on the default AppDomain or a newly created one like this:

MC++
[GuidAttribute("0C19678A-CE6C-487B-AD36-0A8B7D7CC035"), ComVisible(true)]
public sealed class CustomAppDomainManager : AppDomainManager, ICustomAppDomainManager
{
  // ... Rest of class members abbreviated for brevity
  public void Run(string assemblyFilename, string friendlyName)
  {
     AppDomain ad = AppDomain.CreateDomain(friendlyName);
     int exitCode = ad.ExecuteAssembly(assemblyFilename);
     AppDomain.Unload(ad);
     return exitCode;
  }
}

Modifying the SampleApp to use this Run method gives us the following output:

Image 3

It runs the Main method of an Assembly, exactly as we want it to. We are not there just yet, although very close. I deliberately jumped a step just to show you the end result. What is missing is a way to obtain the pointer to our CustomAppDomainManager. If you remember, it is not created by us, but by the CLR framework. We will have to implement another interface called IHostControl.

IHostControl

This is a class that the CLR will query for implementation of alternative Managers, we should of course instantiate our customized versions if we have any and return them.

Examples of handlers or managers that can be overridden with a user implementation can be seen below:

  • IID_IHostTaskManager
  • IID_IHostThreadpoolManager
  • IID_IHostSyncManager
  • IID_IHostAssemblyManager
  • IID_IHostGCManager
  • IID_IHostPolicyManager

In the AppDomainManager case, the CLR will actually call IHostControl::SetAppDomainManager with a pointer to the instance of the class we told it to create. If you remember, we called a method with a similar name ICLRRuntimeHost::SetAppDomainType.

Implementing IHostControl

Below is a listing of a minimal implementation of the IHostControl interface. For brevity I have removed the boiler plate code, such as constructors, destructors, AddRef, and Release required by COM.

MC++
class MinimalHostControl : public IHostControl
{
public:
   HRESULT STDMETHODCALLTYPE GetHostManager(REFIID riid,void **ppv)
   {
      *ppv = NULL;
      return E_NOINTERFACE; 
   }   
   HRESULT STDMETHODCALLTYPE SetAppDomainManager(
           DWORD dwAppDomainID, IUnknown *pUnkAppDomainManager)
   {
      HRESULT hr = S_OK;
      hr = pUnkAppDomainManager->QueryInterface(__uuidof(ICustomAppDomainManager), 
                                (PVOID*) &m_defaultDomainManager);
      return hr;
   }
   HRESULT STDMETHODCALLTYPE QueryInterface( const IID &iid, void **ppv)
   {
      if (!ppv) return E_POINTER;
      *ppv= this;
      AddRef();
      return S_OK;
   }
   
   // Added in order to get a reference to our AppDomainManager implementation
   ICustomAppDomainManager* GetDomainManagerForDefaultDomain()
   {
      if (m_defaultDomainManager)
      {
         m_defaultDomainManager->AddRef();
      }
      return m_defaultDomainManager;
   }
private:
   ICustomAppDomainManager* m_defaultDomainManager;
};

With this final class, we are ready to launch a managed assembly via the AppDomainManager.

Running a managed app

Putting it all together now. We will be able to launch a managed application (from our unmanaged application hosting the CLR runtime).

MC++
...
ICLRControl* pCLRControl = nullptr;
hr = pRuntimeHost->GetCLRControl(&pCLRControl);

// Set our own IHostControl implementation
MinimalHostControl* pMyHostControl = pMyHostControl = new MinimalHostControl();
hr = pRuntimeHost->SetHostControl(pMyHostControl);

// Set our own AppDomainManager implementation
LPCWSTR appDomainManagerTypename = L"SampleAppDomainManager.CustomAppDomainManager";
LPCWSTR assemblyName = L"SampleAppDomainManager";
hr = pCLRControl->SetAppDomainManagerType(assemblyName, appDomainManagerTypename);

hr = pRuntimeHost->Start();

// Get a pointer to our CustomAppDomainManager
ICustomAppDomainManager* pAppDomainManager = pMyHostControl->GetDomainManagerForDefaultDomain();

// Invoke the Run method, which in turn invokes the ExecuteAssembly in a new AppDomain
BSTR assemblyFilename = fileName;
BSTR friendlyname = L"TestApp";
hr = pAppDomainManager->Run(assemblyFilename, friendlyname);
hr = pRuntimeHost->Stop();

Conclusion

Why did we go through all this trouble just to execute a managed app? A managed app is already executable by clicking on it or launching it from a Cmd prompt.

Well, this is just the first step. We have not yet implemented anything of use, but there is a small difference. We executed the managed app in a new AppDomain, not in the default one. The advantage of this is that you can create a supervisor launcher. The next step would be to implement and replace the default Managers, that the CLR queries the IHostControl about. If we are uncertain about the origin of an application, we can with this type of hosting actually strengthen the security of the application, and sandbox it the way we want. It is of course a double edged sword. It can also be used to remove security from an application.

I have a strong interest in debugging and testing. Customizing the runtime will let me do more sophisticated loggers, without having to modify any code. It will just work, and the app will be unaware of the change.

Continuation - Part 2

There is a follow up article, where we will implement AppDomainManagers with functionality for sandboxing, exception handling,  and alternative assembly loading.

Points of interest

The main source of documentation is course MSDN itself, .NET Framework 2.0 Hosting Interfaces.

A great book regarding the CLR Hosting API, is Customizing the Microsoft® .NET Framework Common Language Runtime. It is a bit old, but the best (and perhaps the only one in existence) I think.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Architect Visma Software AB
Sweden Sweden
Mattias works at Visma, a leading Nordic ERP solution provider. He has good knowledge in C++/.Net development, test tool development, and debugging. His great passion is memory dump analysis. He likes giving talks and courses.

Comments and Discussions

 
QuestionI have a problem about unloading the appsample, need your help Pin
Member 105057565-Jan-17 15:57
professionalMember 105057565-Jan-17 15:57 
Question2 .net runtimes inside single process Pin
Zergatul14-May-15 5:26
Zergatul14-May-15 5:26 
AnswerRe: 2 .net runtimes inside single process Pin
Mattias Högström19-Oct-16 21:58
Mattias Högström19-Oct-16 21:58 
QuestionMy vote of 5 Pin
Matth Moestl7-Jul-14 9:47
professionalMatth Moestl7-Jul-14 9:47 
AnswerRe: My vote of 5 Pin
Mattias Högström15-Jul-14 2:01
Mattias Högström15-Jul-14 2:01 
GeneralMy vote of 5 Pin
Mohammed Hameed26-Jun-13 3:42
professionalMohammed Hameed26-Jun-13 3:42 
GeneralRe: My vote of 5 Pin
Mattias Högström30-Jun-13 4:17
Mattias Högström30-Jun-13 4:17 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA8-Aug-12 5:27
professionalȘtefan-Mihai MOGA8-Aug-12 5:27 
Questionvery nice Pin
BillW336-Aug-12 5:11
professionalBillW336-Aug-12 5:11 
QuestionGreat article Pin
Mike Hankey16-Jul-12 6:25
mveMike Hankey16-Jul-12 6:25 
AnswerRe: Great article Pin
Mattias Högström16-Jul-12 11:05
Mattias Högström16-Jul-12 11:05 
GeneralMy vote of 5 Pin
Thornik9-Jul-12 11:09
Thornik9-Jul-12 11:09 
QuestionManaged code loading time Pin
Oren Rosen9-Jul-12 1:43
Oren Rosen9-Jul-12 1:43 
AnswerRe: Managed code loading time Pin
Mattias Högström9-Jul-12 2:34
Mattias Högström9-Jul-12 2:34 
QuestionRe: Managed code loading time Pin
Oren Rosen9-Jul-12 2:48
Oren Rosen9-Jul-12 2:48 
AnswerRe: Managed code loading time Pin
Mattias Högström9-Jul-12 7:46
Mattias Högström9-Jul-12 7:46 
QuestionRe: Managed code loading time Pin
Oren Rosen9-Jul-12 7:57
Oren Rosen9-Jul-12 7:57 
AnswerRe: Managed code loading time Pin
Mattias Högström10-Jul-12 6:01
Mattias Högström10-Jul-12 6:01 

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.