Sunday, July 17, 2005 2:58 AM
bart
CLR Hosting - part 1 (A brief overview)
Introduction
This is the first real post in my "CLR Hosting" blogseries that's coming up in the next couple of weeks (or months?). CLR Hosting is in my opinion a great API in a sense that it allows third parties to integrate the CLR in their products. Well, that's completely true of course, but what's the value "normal" developers? My personal answer to this question is: you can learn a lot of how the CLR actually works by taking a look at this API. It also helps you to understand how SQL Server 2005 can adopt the CLR in the database engine in such a way that the CLR behaves in exactly the way the SQL OS folks want it to behave (compare with "parental control").
The CLR's execution engine
Let's start by taking a look at the basics of the CLR when it comes to executing code. The central file in this story is mscoree.dll (Component Object Runtime Execution Engine), which contains the execution engine. Well, that's not completely true actually. Mscoree is called the "startup shim" and is unique on the machine, regardless of the number of side-by-side installations of the .NET Framework (e.g. 1.0.3705, 1.1.4322, 2.0.x). You'll find the file in the system32 folder on your system. It's the task of the mscoree.dll file to hand over execution to a specific version of the CLR depending on a number of factors. Such an installation of a version of the CLR contains a bunch of files starting with mscor, such as:
- mscorwks.dll - the workstation version of the CLR
- mscorsvr.dll - the server version of the CLR (I'll talk about the workstation and server versions in a later post when talking about the garbage collector etc)
- mscorlib.dll - contains a part of the System namespace of managed classes (e.g. System.Activator is in there, whileas System.Uri lives in the System.dll assembly); this file contains low-level functionality that has a close relationship with the CLR itself (e.g. code to support concepts such as application domains)
- mscorjit.dll - the just-in-time compiler of the CLR to compile IL-code to native code at runtime
You can find all these files in the Microsoft.NET\Framework folders in your Windows directory. As you can have multiple different versions of the CLR on one machine, it's the job of the startup shim (which is not installed on a version-per-version basis) to load a specific version of the CLR and to hand over execution to that particular version.
Now, how does the CLR get loaded when a managed assembly is started? The answer depends on the operating system you're running. Let's start at the end of the story: mscoree.dll contains an "entry-point" for managed execution of an assembly, called _CorExeMain (and _CorDllMain). This function has to be called to hand over execution of a managed assembly (which is wrapped inside a standard PE - portable execution format - file) to the CLR. Machines with Windows XP and Windows Server 2003 know how to recognize a managed assembly and call this function directly when such an assembly is loaded by the PE operating system loader. On other versions of the Windows operating system, a small launch routine is inserted in the PE-file to hand over control to the CLR, by calling the _CorExeMain function. You can find another post on this subject on http://blogs.wwwcoder.com/rajaganesh/archive/2005/06/30/5386.aspx. To find out about a PE file containing managed code, XP and W2K3 (and later) check the "COM Descriptor Directory" entry in the file header. You can take a look at this information yourself by using the dumpbin tool with the switch /headers:
C:\Documents and Settings\BartDS>dumpbin /headers hello.exe
Microsoft (R) COFF/PE Dumper Version 7.10.3077
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file hello.exe
PE signature found
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
14C machine (x86)
2 number of sections
422F31FB time date stamp Wed Mar 09 18:27:23 2005
0 file pointer to symbol table
0 number of symbols
E0 size of optional header
10E characteristics
Executable
Line numbers stripped
Symbols stripped
32 bit word machine
OPTIONAL HEADER VALUES
10B magic # (PE32)
8.00 linker version
400 size of code
200 size of initialized data
0 size of uninitialized data
22DE entry point (004022DE)
2000 base of code
4000 base of data
400000 image base (00400000 to 00405FFF)
2000 section alignment
200 file alignment
4.00 operating system version
0.00 image version
4.00 subsystem version
0 Win32 version
6000 size of image
200 size of headers
0 checksum
3 subsystem (Windows CUI)
400 DLL characteristics
No safe exception handler
400000 size of stack reserve
1000 size of stack commit
100000 size of heap reserve
1000 size of heap commit
0 loader flags
10 number of directories
0 [ 0] RVA [size] of Export Directory
228C [ 4F] RVA [size] of Import Directory
0 [ 0] RVA [size] of Resource Directory
0 [ 0] RVA [size] of Exception Directory
0 [ 0] RVA [size] of Certificates Directory
4000 [ C] RVA [size] of Base Relocation Directory
0 [ 0] RVA [size] of Debug Directory
0 [ 0] RVA [size] of Architecture Directory
0 [ 0] RVA [size] of Global Pointer Directory
0 [ 0] RVA [size] of Thread Storage Directory
0 [ 0] RVA [size] of Load Configuration Directory
0 [ 0] RVA [size] of Bound Import Directory
2000 [ 8] RVA [size] of Import Address Table Directory
0 [ 0] RVA [size] of Delay Import Directory
2008 [ 48] RVA [size] of COM Descriptor Directory
0 [ 0] RVA [size] of Reserved Directory
SECTION HEADER #1
.text name
2E4 virtual size
2000 virtual address (00402000 to 004022E3)
400 size of raw data
200 file pointer to raw data (00000200 to 000005FF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
60000020 flags
Code
Execute Read
SECTION HEADER #2
.reloc name
C virtual size
4000 virtual address (00404000 to 0040400B)
200 size of raw data
600 file pointer to raw data (00000600 to 000007FF)
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
42000040 flags
Initialized Data
Discardable
Read Only
Summary
2000 .reloc
2000 .text
C:\Documents and Settings\BartDS>
Make sure you're using the 7.0 version of dumpbin as in earlier versions the "COM Descriptor Directory" is still called "Reserved Directory" and there are a couple of these reserved directory entries in the list :-). The 7.0 (and higher) versions also have a switch /CLRHEADER that is useful to display the CLR-header that's embedded in a PE file:
C:\Documents and Settings\BartDS>dumpbin /clrheader hello.exe
Microsoft (R) COFF/PE Dumper Version 7.10.3077
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file hello.exe
File Type: EXECUTABLE IMAGE
clr Header:
48 cb
2.00 runtime version
2068 [ 224] RVA [size] of MetaData Directory
1 flags
6000001 entry point token
0 [ 0] RVA [size] of Resources Directory
0 [ 0] RVA [size] of StrongNameSignature Directory
0 [ 0] RVA [size] of CodeManagerTable Directory
0 [ 0] RVA [size] of VTableFixups Directory
0 [ 0] RVA [size] of ExportAddressTableJumps Directory
Summary
2000 .reloc
2000 .text
C:\Documents and Settings\BartDS>
On a non-managed file, you won't get any information other than the summary when running a dumpbin /clrheaders against the file.
Introduction to the CLR Hosting API
Now it's time to JMP to the real stuff. As I've explained, mscoree.dll is responsible to load a specific version of the CLR and thus to tell that particular version how to initialize. The mscoree.dll version will always be the version of the most recent CLR version running on your system and has to maintain things as backward compatible as possible as the file is subject to the old "COM DLL Hell" (it resides in system32 and has to be registered on the system). In the bin\include subdirectory of your SDK installation folder of that particular most recent version of the .NET Framework, you'll find a mscoree.idl file that contains the public export information of the functions inside the library. For version 2.0 of the .NET Framework you'll find a section marked with:
//*****************************************************************************
// New interface for hosting mscoree
//*****************************************************************************
The stuff in this section will be the subject of this and upcoming posts in the "CLR Hosting" series, i.e.:
interface ICLRRuntimeHost : IUnknown
The functions that go in there will be explained later on, but some of these should look familiar: Start, Stop, ExecuteApplication, etc. Others give access to the IHostControl object that can be used to configure the CLR prior to startup. The general principle of writing a CLR Host is implementing interfaces that start with IHost. These implementations contain your code to tell the CLR how to behave and allow you to control the overall behavior of the CLR that you want to control in your specific scenario. Examples are assembly loading, memory management, threading and locking, and so on. A more complete list follows. On the other hand there are a bunch of interfaces that start with ICLR which indicates that the CLR itself is responsible to provide an implementation. So, how do we get an instance of an ICLRRuntimeHost object to kick off with our hosting stuff? The answer is CorBindToRuntimeEx, which is the function to load the CLR in a process. Open up mscoree.h and you should find this line:
STDAPI CorBindToRuntimeEx(LPCWSTR pwszVersion, LPCWSTR pwszBuildFlavor, DWORD startupFlags, REFCLSID rclsid, REFIID riid, LPVOID FAR *ppv);
The first parameter takes the version (e.g. L"v2.0.40607"), the second one the build flavor being workstation or server (e.g. L"wks" for the workstation build, more information follows later when talking about the GC), next we have a DWORD variable for startup flags of the following enum:
// By default GC is non-concurrent and only the base system library is loaded into the domain-neutral area.
typedef enum {
STARTUP_CONCURRENT_GC = 0x1,
STARTUP_LOADER_OPTIMIZATION_MASK = 0x3<<1, // loader optimization mask
STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN = 0x1<<1, // no domain neutral loading
STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN = 0x2<<1, // all domain neutral loading
STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST = 0x3<<1, // strong name domain neutral loading
STARTUP_LOADER_SAFEMODE = 0x10, // Do not apply runtime version policy to the version passed in
STARTUP_LOADER_SETPREFERENCE = 0x100, // Set preferred runtime. Do not actally start it
STARTUP_SERVER_GC = 0x1000, // Use server GC
STARTUP_HOARD_GC_VM = 0x2000, // GC keeps virtual address used
STARTUP_LEGACY_IMPERSONATION = 0x10000, // Do not flow impersonation across async points by default
} STARTUP_FLAGS;
I'll explain the difference between the concurrent_gc and server_gc later on. The next two parameters take some information about the runtime host, more specifically the CLSID and the IID of the interface:
EXTERN_GUID(CLSID_CLRRuntimeHost, 0x90F1A06E, 0x7712, 0x4762, 0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x01);
EXTERN_GUID(IID_ICLRRuntimeHost, 0x90F1A06C, 0x7712, 0x4762, 0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x01);
Finally, the last parameter is the one you need to obtain an object to work with. It's a pointer to an object that will contain the ICLRRuntimeHost instance that can be used to do, well all of the stuff that's mentioned in the IDL definition for this interface. So, the way to use it is to pass in an address of a pointer of the type ICLRRuntimeHost*, with some casting (C++ you know :-)). The overall result with my installation of the .NET Framework v2.0 looks like this:
ICLRRuntimeHost *pHost = NULL;
HRESULT res = CorBindToRuntimeEx(L"v2.0.40607", L"wks", STARTUP_SERVER_GC, CLSID_CLRRuntimeHost, IID_CLRRuntimeHost, (PVOID*) &pHost);
Once this code has been executed the CLR has been loaded in the process space and is ready to be launched but still waiting on a Start command. Before calling start you can actually manipulate the CLR's settings as I'll explain in much more detail later on. To start the CLR, just call Start:
pHost->Start();
Who's implementing what?
Time for the discovery phase. In the previous section I explained briefly that it's possible to take responsibility for certain CLR-related functionality by implementing certain interfaces. This way, you just implement what you need. For example, you might need more control over memory allocation whileas you don't need to control threading in your scenario. This flexibility results in another complexity however and that's the problem of discovery: finding out who implements what. For example, the CLR needs to know what responsibilities the host wants to take over. For that purpose, there's an interface called IHostControl:
interface IHostControl : IUnknown
{
HRESULT GetHostManager(
[in] REFIID riid,
[out] void **ppObject);
/* Notify Host with IUnknown with the pointer to AppDomainManager */
HRESULT SetAppDomainManager(
[in] DWORD dwAppDomainID,
[in] IUnknown* pUnkAppDomainManager);
HRESULT GetDomainNeutralAssemblies(
[out] ICLRAssemblyReferenceList **ppReferenceList);
}
We're interested in the first method, being GetHostManager. It's your task as a CLR hosting developer to implement this interface and this method. When the CLR is started and the host control is bound to the CLR runtime host object, the CLR initiates a dialog based on a series of IIDs for everything the host can take responsibility for. To put this in simple words, a dialog like this is going on:
- Host to CLR runtime host (pHost): here's the host control object (MyHostControl)
- CLR to host control object (MyHostControl) via method GetHostManager:
- Hi there, do you implement a memory manager? If so, please give me a reference to it?
- Hi there, do you implement a garbage collector manager? If so, please give me a reference to it?
- Hi there, do you implement a thread pool manager? If so, please give me a reference to it?
- and so on...
What you have to do, is implementing the IHostControl interface and give an implementation for the GetHostManager method that looks like this:
HRESULT __stdcall MyHostControl::GetHostManager(REFIID id, void **ppHostManager)
{
if (id == IID_IHost...Manager)
{
MyHost...Manager *p...Manager = new MyHost...Manager(); //implements IHost...Manager
//other stuff to initialize the manager
*ppHostManager = (IHost...Manager*) p...Manager;
return S_OK;
}
else if (id == IID_IHost...Manager)
{
//same story over here for this particular manager
}
else if (id == IID_IHost...Manager)
{
//same story over here for this particular manager
}
else
{
*ppHostManager = NULL;
return E_NOINTERFACE; //tell the CLR we don't take care for the requested manager
}
}
In this case, initialization looks as follows:
ICLRRuntimeHost *pHost = NULL;
HRESULT res = CorBindToRuntimeEx(L"v2.0.40607", L"wks", STARTUP_SERVER_GC, CLSID_CLRRuntimeHost, IID_CLRRuntimeHost, (PVOID*) &pHost);
MyHostControl *pHostControl = new MyHostControl();
pHost->SetHostControl((IHostControl*) pHostControl);
If you as a host want to know what the CLR's managers are for various tasks, the process works in a similar fashion. First, you do obtain a reference to the ICLRControl (just substitute Host with CLR and you're usually right):
ICLRControl pCLRControl = NULL;
pHost->GetCLRControl(&pCLRControl);
Next, you use the IID_ICLR...Manager values to ask the CLR for a particular manager. You'll find the complete list of managers in the mscoree.idl file when looking for IID_ICLR...Manager values in there. The skeleton looks like this:
ICLR...Manager *p...Manager = NULL;
pCLRControl->GetCLRManager(IID_ICLR...Manager, (void**) &p...Manager);
I won't give an overview of the various managers you can decide to implement, as I'll focus on these individually later on.
Del.icio.us |
Digg It |
Technorati |
Blinklist |
Furl |
reddit |
DotNetKicks
Filed under: .NET Framework v2.0, CLR Hosting