Customizing the .NET Common Language Runtime
Posted by Dan Buskirk on 05/1/07 | DotNet
The user double-clicks on a .NET program. The CLR loads it and runs it. Works like a champ. Why would anyone need to customize the .NET Runtime?
In fact, there are several good reasons. Access to the debugging API is one. Low-level monitoring of things like garbage collection is another. But the most important reason for many developers is the creation of robust extensible applications. In addition to running their own code, extensible applications run code provided by the user. Any mishandling of memory or security could, in principle, damage the entire application. The user library which extends the application needs to be carefully managed. Experienced .NET programmers know that the unit of isolation for memory management and security within .NET is the AppDomain, much as the process is the unit of management within the operating system. The .NET 2.0 runtime provides new interfaces which permit developers to provide their own customized AppDomain management. While the CLR must be launched using unmanaged code, a custom AppDomain manager for version 2.0 can be written in C# or your favorite .NET language.
One example of an program which uses a customized CLR is Microsoft’s SQL Server 2005. If a stored procedure or function is implemented as .NET code, that code cannot be allowed to request memory from Windows. Any server expected to run 24/7 must manage its memory very carefully, and SQL Server cannot have memory being requested behind its back, so to speak. The customized CLR running .NET code within SQL Server routes all requests for memory through SQL Server rather than to the OS directly, thereby ensuring that SQL Server can manage its memory successfully. A service or application which loads the CLR into its own process to run managed code is said to host the CLR.
Of course, it is not only mission-critical server database systems which need to make use of such features. Imagine a small financial services firm which provides software for its customers’ use. Some of the customers are quite sophisticated and request special features. Of course, each customer requests a different special feature. One solution to this problem is to make the core application extensible. Special features can then be implemented as add-ins, vastly simplifying the management of the core application’s development.
This will be the scenario for our exploration of the .NET runtime and its customization. We will start by examining how the CLR loads and runs a simple program.
How Windows Loads a .NET Executable
All Windows executable files contain more than just compiled program code. The file start with a header, which provides the operating system with a great deal of information about the executable code. Headers can be viewed with the dumpin.exe tool that comes with Microsoft Visual Studio. (Dumpbin.exe may be found in the C:\Program Files\Microsoft Visual Studio 8\VC\bin directory.) To use this utility, you will first have to run the vcvars32.bat batch file that also comes with the Visual Studio and is found in the same directory. The batch file ensures that various environment settings are set. You can then run dumpbin /headers <filename> on the command line.
Towards the end of the output you will see an entry
RVA [size] of COM Descriptor Directory
In a native Windows executable file, this entry will have a value of 0. If the value is not 0, Windows knows that this is a managed file, and begins to load the Common Language Runtime. In Windows XP and later, the OS directly calls a function _CorExeMain, which is located in mscoree.dll. This function identifies the required CLR version, loads it, and executes the managed program file under the default settings.
One of our major motivations in developing hosting applications is to go beyond limitations imposed by those constraints.
Writing a Custom .NET Host
Writing a custom host is the first step to customizing the CLR. Writing a custom host even allows you to store your executable code in forms other than the classic "file", though we won't be exploring that possiblity in this series. As an example, SQL Server 2005 stores in managed code for functions, triggers and stored procedures in its system tables rather than access DLL files from the file system. If no dll file needs read when a managed stored procedure is called, SQL Server does not have to concern itselp with the security of such files.
To start with, we will create a simple host that merely runs a .NET executable file. Of course, such a program must be written in unmanaged code. Let's look at the key features of an extremely simple host. This sample is similar to that provided by Steven Pratschner in his MS Press book Customizing the Microsoft.NET Framework Common Language Runtime
1 // SvrHost.cpp : Defines the entry point for the console application.
2 //
3 //
4
5 #include "stdafx.h"
6 #include <mscoree.h>
7
8 // include the tlb for mscorlib for access to the default AppDomain through COM Interop
9 #import <mscorlib.tlb> raw_interfaces_only high_property_prefixes("_get","_put","_putref")
10 using namespace mscorlib;
11
12 int _tmain(int argc, _TCHAR* argv[])
13 {
14 /* Use the hosting interfaces from .Net Framework 1.1
15 even though we are loading version 2.0
16 */
17 // (1) Variable to hold our old-fashioned CLR reference
18 ICorRuntimeHost *pCLR = NULL;
19
20 // (2)
21 // Initialize the CLR. Specify the Server build. Make sure
22 // version 2.0 is loaded.
23 HRESULT hr = CorBindToRuntimeEx(
24 L"v2.0.50727",
25 L"svr", // "wks" is the default
26 NULL, // (3) Startup flags
27 CLSID_CorRuntimeHost,
28 IID_ICorRuntimeHost,
29 (PVOID*) &pCLR);
30
31 assert(SUCCEEDED(hr));
32
33 // Start the CLR
34 pCLR->Start();
35
36 // (4) Get a pointer to the default AppDomain
37 _AppDomain *pDefaultDomain = NULL;
38 IUnknown *pAppDomainPunk = NULL;
39
40 hr = pCLR->GetDefaultDomain(&pAppDomainPunk);
41 assert(pAppDomainPunk);
42
43 hr = pAppDomainPunk->QueryInterface(__uuidof(_AppDomain), (PVOID*) &pDefaultDomain);
44 assert(pDefaultDomain);
45
46 // get the name of the exe to run
47 long retCode = 0;
48 BSTR asmName = SysAllocString(argv[1]);
49
50
51
52 // (5) Run the managed exe in the default AppDomain
53 hr = pDefaultDomain->ExecuteAssembly_3(asmName, NULL, NULL, &retCode);
54 assert(SUCCEEDED(hr));
55
56
57 SysFreeString(asmName);
58 pAppDomainPunk->Release();
59 pDefaultDomain->Release();
60
61 _tprintf(L"\nReturn Code: %d\n", retCode);
62 return retCode;
63 }
64
In step (1) we are simply declaring declaring a variable to hold our CLR reference. While variable declaration is not usually very exciting, this declaration is very important. The variable type is ICorRuntimeHost. This is the older interface which was created for .NET version 1. The interface which provides newer and more powerful options is ICLRRuntime. However, more powerful often means more complex, and that is certainly the case here. We are using the older interface in this example to get a feel for how a host works; we shall soon move to the newer interface.
Step (2) is where the action is. We call the CorBindToRuntimeEx() function, which is defined in mscoree.dll. In this case, we insist on the .NET 2.0 runtime. We also want the server version, which is optimized to run server applications on symmetric multiprocessing machine. (If there is only one processor we'll always get the workstation CLR, even if we ask for server.) We supply classid values defined in the header files and we get back a reference to the CLR. CorBindToRuntimeEx() returns an HRESULT to indicate success or failure. That's right; this is all COM code. While COM is being banished from the toolbox of the application developer, it is alive and well at a lower level. The CLR itself is, in a very real sense, a COM application and we will use COM interfaces for all our interaction with it from the outside (i.e. unmanaged) world.
The parameter for CorBindToRuntimeEx() marked with (3) allows you to set flags which determine key features of the runtime at startup. For example, we can specify whether garbage collection should be concurrent or not. In this example, we are not using the flags at all; we accept the default by supplying NULL. Later, however, we shall explore how to load different code into different AppDomains, and this parameter will become very important.
In the code near markers (4) and (5) we ask the COM mechanisms to give us a reference to an AppDomain, and then use this pointer to launch our application. It is the code in this section which disappears in the newer .NET 2.0 ICLRRuntime interface. Let's take a quick peek at a simple use of the ICLRRuntime interface.
Using the ICLRRuntime interface
In this extremely simple example, we won't be launching an application, we will be invoking a method using a function called ExecuteInDefaultAppDomain(). We can certainly invoke a method from an exe file. However, that method cannot be Main(). ExecuteInDefaultAppDomain() requires that the method we invoke accept a string argument and return an integer. As a program entry point, Main() can receive no arguments, or it can receive a string array - it is therefore out-of-the-running as a candidate for ExecuteInDefaultAppDomain(). Similar code is discussed in the excellent Richter book CLR Via C#: Applied Microsoft .Net Framework 2.0 Programming, also from MS Press.
Let's look at the code:
1 #include <Windows.h>
2 #include "assert.h"
3 #include <MSCorEE.h>
4 #include <stdio.h>
5 #include <CorError.h>
6
7 void main(int argc, WCHAR **argv) {
8 // Load the CLR
9 //(1)****************************
10 ICLRRuntimeHost *pClrHost;
11 HRESULT hr = CorBindToRuntimeEx(
12 L"v2.0.50727", // desired CLR version (NULL=latest)
13 NULL, // desired CLR Optimization (NULL=workstation)
14 STARTUP_CONCURRENT_GC, // desired startup flags
15 CLSID_CLRRuntimeHost, // CLSID of CLR
16 IID_ICLRRuntimeHost, // IID of ICLRRuntimeHost
17 (PVOID*) &pClrHost); // returned COM interface
18
19 //(2) ****************************
20 // (This is where you would set Host managers)
21 // (This is where you could get CLR managers)
22 ICLRControl *pCLRControl;
23 hr = pClrHost->GetCLRControl(&pCLRControl);
24
25 assert(SUCCEEDED(hr));
26 // must get this stuff before the CLR is started
27 ICLRGCManager *ppObject;
28 hr = pCLRControl->GetCLRManager(
29 IID_ICLRGCManager,
30 (VOID **) &ppObject);
31
32 //wprintf(L"\n%d\n",hr);
33 assert(SUCCEEDED(hr));
34
35 //(3) *****************************
36 // Initialize and start the CLR
37 pClrHost->Start();
38
39 // Load an assembly and call a static method that
40 // takes a String and returns an Int32
41 DWORD retVal;
42
43 hr = pClrHost->ExecuteInDefaultAppDomain(
44 L"HelloWorld.exe",
45 L"HelloWorld.Program", L"Hola", L"mundo", &retVal);
46
47 assert(SUCCEEDED(hr));
48 // Show the result returned from managed code
49 wprintf(L"\nManaged code returned %d", retVal);
50
51 // (4) *****************************
52 //hr=ppObject->Collect(0L); //Force a garbage collection
53 //assert(SUCCEEDED(hr));
54
55 COR_GC_STATS pStats;
56 pStats.CommittedKBytes=0L;
57 pStats.GenCollectionsTaken[0]=0L;
58 pStats.LargeObjectHeapSizeKBytes=0;
59 pStats.Gen0HeapSizeKBytes=0;
60 pStats.KBytesPromotedFromGen0=0;
61 hr= ppObject->GetStats (&pStats);
62 assert(SUCCEEDED(hr));
63
64 wprintf(L"\nGenCollectionsTaken: %lu",pStats.GenCollectionsTaken[0]);
65 wprintf(L"\nLargeObjectHeapSizeKBytes: %lu",pStats.LargeObjectHeapSizeKBytes);
66 wprintf(L"\nCommittedKBytes: %lu",pStats.CommittedKBytes);
67 wprintf(L"\nGen0HeapSizeKBytes: %lu",pStats.Gen0HeapSizeKBytes);
68 wprintf(L"\nKBytesPromotedFromGen0: %lu",pStats.KBytesPromotedFromGen0);
69
70 }
Code section (1) is similar to that in the previous example, but the interface is the new ICLRRuntimeHost interface. This new interface bears little resemblance to the interface used in the 1.0 and 1.1 versions of the CLR. In particular, the methods for launching an executable assembly are quite distinct. These will be discussed in the next installment of this column. Section (2) of the sample code is not necessary for this example, but we have included it to give you the flavor of customization. In this case, we are obtaining a pointer to the ICLRControl interface, which will then provide us with a pointer to the Garbage Collection Manager. Obtaining these pointers, and other comparable pointers used in CLR customization, must be done before the CLR is started.
In code section (3), we start the CLR and ask it to run a static method for us. As we have noted, ExecuteInDefaultAppDomain() requires that this method receive a string argument and return an Int32. In this example, we assume that the executable file HelloWorld.exe is in the same directory as the host program. If it is not, then the first argument of ExecuteInDefaultAppDomain() must be a fully qualified path. The source for the executable used in this example is included in the appendix, but you should have no difficulty experimenting with your own.
Code section (4) is not necessary for execution; it simply illustrates how to use the pointer we obtained earlier to get detailed information on the status of the Garbage Collector. We create and initialize a COR_GC_STATS structure and pass it to the GetStats method.
Next Month: Creating a Custom AppDomain Manager
Next month we shall build on this example to create a custom AppDomain manager using C#, and lay the foundation for an extensible application.
Appendix
As if the world needs another HelloWorld program:
1 using System;
2 using System.Collections.Generic;
3 using System.Text;
4
5 namespace HelloWorld
6 {
7 class Program
8 {
9 static void Main()
10 {
11 Hola("mundo");
12
13 }
14 static int Hola(string jrichter)
15 {
16 Console.WriteLine("Hola, {0}!", jrichter);
17 return 0;
18 }
19 }
20 }
Source Code
The source code for all 3 examples (Simple Command Host Line, Simple Command Host Line 2, and Hello World) can be found here.
About Dan Buskirk:
Dan Buskirk had been a research scientist for many years when he left the university labs to participate in a startup venture to design and build databases for medical science. Since that time he has managed a consulting practice specializing in database design and development. Dan balances this with training on Microsoft SQL Server, Microsoft Analysis Services, and .NET programming. His interests include mathematical methods for data mining and computing methods for advanced data categorization on Windows clusters.
Want to learn more about .NET Training?