Creating a Host to the .NET Common Language Runtime
We have all seen the different type of applications the .NET Common Language Runtime (CLR) caters for,
Web applications (ASP.NET), GUI-based ones (WinForms), simple console-based ones and managed ActiveX controls
hosted by Internet Explorer
When we write code targeted for the 'managed world' in the languages that .NET currently supports, .NET
aware language compilers emit assemblies that contain the CPU agnostic IL in them. These assemblies are
executed by the CLR
One could guess that in Windows' unmanaged world, when one invokes an executable from the Windows shell,
it would have to be a WIN32 process with its own 4GB address spaces. Hence one could also guess that when
one invokes a .NET application, before the managed code starts to execute on the CLR, a small amount of
unmanaged code must run to creates the environment necessary for the CLR. This piece of code is known as the
'host' for the CLR
It is this host that initializes entities called AppDomains within the managed world of the .NET CLR.
AppDomains are CLR's way of representing isolated application services (much like operating system tasks).
One or more AppDomains can coexist within the CLR without interfering with each other, just as WIN32
processes share computing resources without interference
Some standard CLR hosts that are shipped as a part of the .NET runtime are
ASP.NET
Loads the runtime into the process that handles Web requests. ASP.NET creates an AppDomain for each Web
application that will run on a Web server. ASP.NET applications are actually loaded and executed by an
ISAPI filter DLL extension to the IIS web server, that accepts HTTP requests for the .aspx files associated
with the application.
Internet Explorer
Creates AppDomains to run managed controls. The .NET framework supports the downloading and execution of
browser-based controls. It uses the extensibility mechanism of Internet Explorer through a MIME filter to
create AppDomains for any managed controls hosted on a web-site. By default, one AppDomain is created for
each web-site visited.
Shell Executables
Invokes runtime hosting code to transfer control to the runtime each time an executable is launched from
the shell. (Note: A .NET Portable Executable (PE) file contains unmanaged code at the entrypoint to set up
the runtime and transfer control to it and thereby the rest of the managed code. Naturally, this runs first
when the executable is launched from the Windows shell.)
The .NET framework allows easy usage of managed components running under the CLR in the unmanaged world:
it creates COM Callable Wrappers (CCW) silently. Consider the following example for a component written in
C#.
using System;
using System.Reflection;
using System.Windows.Forms;
namespace Ranjeet.SimpleCLRHost
{
public class HelloHostDemo
{
public void Hi()
{
MessageBox.Show("YOO HOO from the Managed World!",
"And now for this message");
}
}
}
To use this (not very useful) C# component in an unmanaged application as a COM component, one
would:
- compile the component as a .NET assembly (a DLL)
- use the command-line tool regasm.exe to register the DLL. This generates an equivalent type library for
use in COM environments, and creates the traditional COM registry entries (HKCR/CLSID) for
CoCreateInstance
/CreateObject
to locate these types.
But, have you ever wondered what exactly happens when this tool is run? Well, it generates an equivalent
type-library for use in the COM environment and creates the traditional COM registry entries under
"HKCR\CLSID" for CoCreateInstance or CreateObject to locate the types in it. Here's what the registry
entries look like:
Notice that the InprocServer32 section's default key points to mscoree.dll? Aha!! The other
keys show that there is information available to the CLR that indicate the assembly name and the type name,
among other attributes of the managed component.
This begs the question: 'What is mscoree.dll doing under that InprocServer32 section?'
Mscoree.dll is the Microsoft .NET Runtime Execution Engine, which hosts the .NET CLR in an unmanaged
environment, and exposes a generic-hosting API for the same.
Here is the TypeLibrary generated by RegAsm.
When one wants to use the type-library generated through the RegAsm or TlbExp tools:
A call is made to CoCreateInstance
with the GUID of the CoClass within the generated type-library
that loads mscoree.dll.
The method DllGetClassObject (internally called by CoCreateInstance, traditional COM) is
exported by the mscoree.dll. The COM Interoperability portion of .NET provides a standard
class-factory implementation to create an instance of any .NET framework class. This class-factory is
returned to the caller, who makes a call to the CreateInstance method, passing in the type requested.
In our sample above, HelloHostDemo is a class with a single method Hi
in it. Since no explicit
interfaces is declared nor any advanced type-library overriding attributes are used, the type-library
generates types for the ClassInterface, after exposing NO dispids(by default). Hence IDispatch is the
default interface.
This interface pointer is what is returned back by the call to CreateInstance from the
class factory, which is the COM Callable Wrapper (known as CCW for short) that's fabricated by the CLR at
runtime.
This is how .NET uses mscoree.dll to add a layer above a managed component, so that COM is totally
unaware of any such thing as a managed environment.
Now that we've finished explaining (whew!) how a CCW is used to make calls in to any .NET component within
an unmanaged environment, let's talk about what this article is all about, how mscoree.dll actually
loads the CLR within the process address-space of the callee and how we can write a very simple host for the
CLR.
Here is what our simple CLR host would do:
- host the Common Language Runtime using CLR's Hosting API's.
- load a type from a .NET assembly -
HelloHostDemo
, in our example, from the HelloMsgBox.dll
assembly. (See instructions on how to build the sample for the .NET Framework SDK, Beta2.)
- retrieve this type as a
IDispatch
pointer and use that to locate the dispid of the method
(Hi
, in our example) through a call to GetIDsOfNames
.
- Use
Invoke
on the IDispatch
pointer with the dispid retrieved, and that's all!
The most important hosting API is CorBindToRuntimeEx
as described by Steven Pratschner from Microsoft,
is more like a 'shim' code that exports the above mentioned API.
CorBindToRuntimeEx
accepts version numbers of CLR's and other configurable parameters based on
which it returns an instance of the ICorRuntimeHost
interface pointer.
The Hosting API lets a custom host fine tune some of the CLR settings like:
- Version of the CLR to be loaded
- Build of the CLR to be loaded, Workstation or Server for client applications and multiprocessor
server scenarios respectively. In case of multi-processor machines the server build allows garbage collection
to be done on each processor in parallel.
- Specifying the garbage collection settings as concurrent or non-concurrent. Concurrent
means garbage collections happens on background threads rather than the thread running user code, hence
beneficial for UI apps. Non-concurrent means garbage collection happens on the same thread as the
thread running user code, mostly used for server/UI-less Apps. Anyway, on single processor machines
non-concurrent GC happens only.
- Domain Neutrality settings, that specify the way in which copies of frequently used Assemblies in
multiple AppDomains are loaded, either different copies for all AppDomains or Shared copy across all
AppDomains, the latter being AppDomain neutral.
The call to this function is made as follows:
HRESULT hr = CorBindToRuntimeEx(
NULL,
L"wks",
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN |
STARTUP_CONCURRENT_GC,
CLSID_CorRuntimeHost,
IID_ICorRuntimeHost,
(void**)&spRuntimeHost
);
The ICorRuntimeHost
allows the host to specify additional configurable parameters like/p>
start/stop control over CLR, e.g.:
spRuntimeHost->Start();
retrieve a pointer to the default AppDomain of the managed code, e.g.:
spRuntimeHost->GetDefaultDomain(&pUnk);
retrieve a pointer to the ICorConfiguration
through which additional fine tuning can be achieved
(see the documentation
for CLR issues involved.)
There! Notice that in the SimpleCLRHost example, we find that no call is made to to the traditional
CoInitialize
function before retrieving the COM interface, so no apartment is specified explicitly.
I have a clue to this, but need more ideas to confirm/clarify my suspicions.
To run the sample, first run buildmanaged.bat before building and executing the
SimpleCLRHost.
There are a couple of things to note before running the sample:
- The sample uses the header file mscoree.h which is located typically in the
C:\PROGRAM FILES\ MICROSOFT.NET\FRAMEWORKSDK\INCLUDE directory, and so I chose to add that in my list
of include paths through Tools -> Options in Visual Studio.
- It uses the MsCorLib.Tlb which provides interface definitions used within the program, which is
located typically in the path C:\WINNT\Microsoft.NET\Framework\v1.0.2914 for beta2 versions of the
CLR, so I just hardcoded the path.
References: