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.
#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:
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
.
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
.
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).
[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.
hr = pRuntimeHost->Start();
DWORD returnValue = 0;
hr = pRuntimeHost->ExecuteInDefaultAppDomain(totalPath, L"SampleApp1.Program", L"Start", L"", &returnValue);
hr = pRuntimeHost->Stop();
Running the sample app now gives us the following output:
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.
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:
[GuidAttribute("0C19678A-CE6C-487B-AD36-0A8B7D7CC035"), ComVisible(true)]
public sealed class CustomAppDomainManager : AppDomainManager, ICustomAppDomainManager
{
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:
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.
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;
}
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).
...
ICLRControl* pCLRControl = nullptr;
hr = pRuntimeHost->GetCLRControl(&pCLRControl);
MinimalHostControl* pMyHostControl = pMyHostControl = new MinimalHostControl();
hr = pRuntimeHost->SetHostControl(pMyHostControl);
LPCWSTR appDomainManagerTypename = L"SampleAppDomainManager.CustomAppDomainManager";
LPCWSTR assemblyName = L"SampleAppDomainManager";
hr = pCLRControl->SetAppDomainManagerType(assemblyName, appDomainManagerTypename);
hr = pRuntimeHost->Start();
ICustomAppDomainManager* pAppDomainManager = pMyHostControl->GetDomainManagerForDefaultDomain();
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.