Customizing the Microsoft .NET Framework Common Language Runtime
Authors: Pratschner S
Published year: 2005
The set of APIs described in this chapter allow the CLR to be customized to work in a variety of application environments. The extent of the customization allowed ranges from configuring basic startup parameters to controlling critical runtime notions such as how code is loaded into the process, how memory is managed, and when code is scheduled to run. The hosting API is factored into a set of managers that group logically related interfaces together. As the author of a CLR host, you get to choose which of these managers you'd like to implement so you can customize only those aspects of the CLR that are most important to your scenario.
This chapter provides an overview of the hosting API to give you an idea of the various ways the CLR can be customized. Throughout the rest of this book, we dig into different parts of the API in greater detail.
Chapter 3. Controlling CLR Startup and Shutdown
A number of configurable settings determine the basic characteristics of the CLR that gets loaded into the process. For example, various settings enable you to select the version of the CLR to load, configure the basic operations of the garbage collector, and so on. All these settings must be specified before the CLR is loaded.
If you're writing a CLR host, you can have full control over all the settings that control CLR startup. It's worth noting that you might not have to write the host yourself to configure the startup options that your scenario requires. Most hosts provide some mechanism to enable application developers or administrators to customize at least some of the CLR startup options. For example, the default CLR host offers a high degree of customization through application configuration files. The options available when using the default host are described in Chapter 4.
In this chapter, I concentrate on what you do to customize CLR startup when writing your own host. Writing your own host enables you to set all of the startup options and offers you flexibility to control when the CLR is actually loaded into the process.
I start by describing the details of the CLR startup settings, and then I describe how to use the unmanaged function CorBindToRuntimeEx to set them explicitly. When you're talking about controlling CLR startup, it's natural to also talk about controlling CLR shutdown. Although you can't completely unload the CLR from a process and reload it later, the CLR hosting API essentially enables you to disable the CLR. I end the chapter with a discussion of exactly what it means to disable the CLR and how to do it.
The CLR Startup Configuration Settings
Four primary settings can be configured as part of CLR startup. Once set, these options affect all code running in the process and cannot be changed (although the domain-neutral settings can be further refined). These options are as follows :
The following sections describe these settings in detail.
Setting the CLR version arguably requires the most thought because several criteria go into making the right choice.
Multiple versions of the CLR can be installed on a given machine at one time. Establishing how your host behaves when multiple versions of the CLR are present is one of the most critical up-front decisions you have to make. This decision is especially important because only one version of the CLR can be loaded into a process, and once that version has been loaded, it cannot be unloaded and replaced with another version. In general, you have two choices when choosing a version. First, you can specify that you always want to run using a specific version. Or you can choose always to run with the latest version of the CLR installed on the machine.
You'll see throughout this section that the trade-off is between isolating your host from version changes made to the CLR over time (to the extent possible) and always being upgraded to the CLR containing the newest functionality, the most bug fixes, and the latest performance enhancements.
In practice, most hosts choose to select a specific version and stay with it. Sticking with a single version gives the host the most control over its own environment because it minimizes the amount of changes made to the CLR the host runs with (however, you'll see in a bit that you can't completely isolate yourself from change because of service releases made to the CLR). When you specify a particular CLR version to use, if a new version of the CLR is released that you wish to support, you must update your code to specify the version number of the new CLR and then test your code against that release before shipping the new host to your customers. On the other hand, clearly, scenarios exist in which it is preferable for an application always to run with the latest CLR version. I explore both options throughout this section.
Side by Side: A Technique to Avoid DLL Hell
When multiple versions of a piece of software are installed on one computer and can be run at the same time, they are referred to as existing side by side . From the very beginning, the concept of side by side has been a specific design goal and has been built directly into the CLR.
To avoid confusion, I want to clarify that the concept of side by side can be viewed from a few different perspectives. The first perspective is the notion of side-by-side applications and assemblies. As part of the core versioning story in the CLR, multiple versions of a specific application or assembly can be installed and run simultaneously . This is in direct contrast to the Win32 and COM models in which the last version of an application or component to be installed is the one that everyone uses. This use latest approach led directly to the phenomenon referred to as "DLL Hell"the all-too-common scenario we've all experienced when the installation of one application breaks some existing application on the machine.
A major portion of the Microsoft .NET Framework approach to solving this problem is to leverage side by side as a default rather than use latest. In the side-by-side model, the installation of a new version doesn't overwrite existing versions and both versions can be run at the same time. This form of isolation through side by side is key to solving DLL Hell.
However, you can't completely isolate applications from changes unless the platform on which the application is running installs and runs side by side as well. This is the other perspective of side by side: the fact that the entire .NET Framework (including the CLR) installs side by side and that hosts or individual applications get to pick which version they'd like to run. As you can see, side by side of the platform is a direct follow-on to side by side at the application level. The two concepts are closely related and tend to bleed together rather easily when you're discussing either one specifically .
At the time of this writing, three side-by-side versions of the .NET Framework (which includes the CLR) have been shipped: Microsoft .NET Framework 1.0, .NET Framework 1.1, and .NET Framework version 2.0. Any, or all, of these can be present on a machine to which your application is deployed. The primary focus of this section is to help you determine the best course of action for selecting which version your application should run with when more than one version is installed.
The criteria that typically go into selecting a CLR version include the following:
You can start to imagine the complexities you might run into when considering multiple versions of a platform coexistingespecially if you've written a host with one version of the CLR and are asked to run a component from one of your customers written with another version!
Choosing a versioning strategy starts by considering how multiple versions of the CLR coexist on the same machine and how a particular version gets selected and loaded.
The Side-by-Side Architecture of the .NET Framework
At a high level, the .NET Framework consists of two big pieces: the CLR and the .NET Framework class libraries. Drawing a distinction between the core CLR files and the .NET Framework class libraries is useful when talking about how a side-by-side version of the .NET Framework is installed and loaded. One reason the distinction is useful is because the version numbers for the core CLR files appear differently than the version numbers for the class libraries when you view them using Windows Explorer, which can be confusing. Throughout this section, I point out where the versions appear differently and why.
For purposes of this discussion, I define the CLR as the set of unmanaged files that make up the CLR execution engine plus the managed assembly that contains the base class library. The core engine files include such items as the engine (mscorwks.dll), the jit compiler (mscorjit.dll), the base portion of the security system (mscorsec.dll), and so on. The file for the assembly containing the base class library is mscorlib.dll. The .NET Framework class libraries include all the managed assemblies that contain the classes that make up the API to the .NET platform, including system.dll, system.xml.dll, system.windows.forms.dll, and many others.
Four interesting things happen (at least from the perspective of side by side) when a version of the .NET Framework is installed:
.NET Framework Registry Keys
The registry key under which all .NET Frameworkrelated keys are written is as follows:
Information about the versions of the .NET Framework installed on the machine is kept under the Policy subkey. Each time a new version of the .NET Framework is installed, a new subkey is written under the Policy key. Figure 3-1 shows the state of the registry after both .NET Framework 1.0 and .NET Framework 1.1 have been installed.
Figure 3-1. Registry entries when multiple versions of the CLR are installed
The version numbers written into the registry are those of the core CLR files. The CLR contained in .NET Framework 1.0 has a major and minor version number of 1.0 as indicated by the v1.0 subkey, whereas the CLR in .NET Framework 1.1 has a major and minor version of 1.1 (the v1.1 subkey). Furthermore, each version of the CLR also has a build number that you should consider when determining which version to load. The build number is stored as a value under the key that describes the major and minor numbers. For example, the build number for .NET Framework 1.0 CLR is 3705, as shown in Figure 3-2.
Figure 3-2. Registry entries showing the version number for the CLR contained in .NET Framework 1.0
As you can see, the registry is the central point for determining which versions of the .NET Framework are installed on your machine. You can get this list easily using the standard Win32 registry functions.
The Versioned Installation Directory
The core CLR files and the class libraries are written to a subdirectory of your Windows directory under Microsoft.NET\Framework. Figure 3-3 shows the state of this directory after both .NET Framework 1.0 and .NET Framework 1.1 have been installed.
Figure 3-3. Contents of %windir%\Microsoft.NET with multiple versions of the .NET Framework installed
Notice that the subdirectories are named by CLR version and that those versions match the names of the keys and values in the registry. The fact that the names in the registry match the names in the file system is not a coincidence . The CLR startup shim uses this mapping to determine the presence of a given CLR version and to apply any upgrades in the appropriate scenarios (more on this later).
The Global Assembly Cache
The .NET Framework setup program installs each of the class library assemblies into the GAC. The GAC maintains the side-by-side storage of assemblies automatically, so the setup program just calls the GAC install APIs and lets the GAC sort out the storage needs. As expected, installing two versions of the .NET Framework results in two copies of each class library assembly in the GAC, as shown in Figure 3-4.
Figure 3-4. Contents of the GAC with multiple versions of the .NET Framework installed
You'll notice that the version numbers displayed for the class library assemblies are different than the version numbers displayed for the core CLR files shown earlier. Assemblies have two different version numbers (there's actually more than two, but let's ignore the rest!). Because managed code is stored in standard executable files, each assembly has a Win32 version number just as executable files containing unmanaged code do. In addition, managed code files have an assembly version number that the CLR uses when resolving references to assemblies. When you navigate to the GAC with Windows Explorer, view the GAC with the .NET Framework Administration tool, or look inside an assembly with the Microsoft IL Disassembler SDK tool (ildasm.exe) you see the assembly version numbernot the Win32 version number. If you were to use Windows Explorer to look at the properties of a managed assembly, you'd see the Win32 version there as well. This version number would be the same as the version number used to name the installation directory or to identify that version of the .NET Framework in the registry.
Table 3-1 shows the mapping between Win32 version numbers and assembly version numbers for each release of the .NET Framework.
Table 3-1. Win32 and Assembly Version Numbers for the .NET Framework
The CLR Startup Shim
Every aspect of these two installations I've discussed so far aims to keep the two versions of the .NET Framework completely separate: registry entries are stored under version-specific keys, files are installed in subdirectories based on version, and the GAC separates the storage of multiple versions of the same assembly. At this point, nothing in the architecture ties these multiple versions together. Specifically, some software component must be aware of which versions exist and have the ability to map a request to load a certain version or a request to run a certain application into a specific version of the CLR. This is the job of the CLR startup shim. The shim code is contained in the file mscoree.dll, which is installed in the Windows system directory. The shim is not installed side by side. That is, installing a new version of the .NET Framework overwrites the version of mscoree.dll that was there previously. The shim is not installed side by side out of necessity because of the reasons just statedit is the component that acts as a broker between the host application and a specific version of the CLR. In fact, the only way to load a version of the CLR is to go through the shim. Because the shim is not installed side by side, its requirements for backward compatibility are extremely high. Complete side by side is not possible on all operating systems that the .NET Framework supports, so in essence Microsoft has dramatically reduced the surface area for backward-compatibility problems down to one small DLL. Every effort is made to keep the functionality in this DLL as simple and straightforward as possible. The relationship between the shim and multiple versions of the .NET Framework is shown in Figure 3-5.
Figure 3-5. The shim and multiple versions of the .NET Framework
Two primary pieces of functionality in the shim relate to side by side. The first is the CorBindToRuntimeEx API that was introduced in Chapter 2. CorBindToRuntimeEx takes a version number (among other things) and loads that version of the CLR into the process. The other important piece of functionality in the shim is the default CLR host. The default CLR host is used in various scenarios, the most common of which is when executables are launched from the command line. The default CLR host is also invoked if a request comes in to instantiate a managed class through the COM Interoperability layer. Chapter 4 describes the inner workings of the default CLR host, including how you can configure the CLR startup options using application configuration files.
Given the earlier installation discussion, it's relatively easy to see how the shim maps a version number to the actual implementation of that version using registry keys and directory names. The one piece of information missing is the root directory under which all versions of the CLR are installed. This information is captured in the InstallRoot registry under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework as shown in Figure 3-6. The setup program always sets this to %windir%\Microsoft.NET\Framework\ and currently doesn't offer an option to change it.
Figure 3-6. The InstallRoot registry value
The shim's ultimate goal is to construct a path to the requested version of the core CLR engine DLL, mscorwks.dll. The formula it uses is straightforward:
Path to CLR engine DLL = Contents of the InstallRoot registry value (i.e., "C:\Windows\Microsoft\.NETFramework\") + Name of the Major.Minor key (i.e., "v1.1") + Value of the Build number key (i.e., "4322") + Filename of core engine DLL ("mscorwks.dll")
So, given a version number of "v1.1.4322," the shim will load C:\Windows\Microsoft\.NETFramework\v1.1.4322\mscorwks.dll.
It might seem unnecessary to use the version information in the registry. After all, the combination of the InstallRoot value plus the requested version leads right to the appropriate directory. The registry lookup is done for few reasons. The first is as an extra sanity check that the installation of the CLR is coherent . The more important reason, though, is for scenarios involving the default CLR host in which a request for a particular CLR version is upgraded to another version. In these situations, the information in the registry is required. If you're writing your own host (i.e., calling CorBindToRuntimeEx yourself), these automatic upgrades don't apply.
Once the core CLR DLL (mscorwks.dll) is loaded, the other unmanaged supporting DLLs such as the JIT compiler and class loader are loaded from the same directory. In addition, the version of mscorlib.dll that matches the given CLR is loaded. mscorlib.dll is an interesting case because even though it is a managed assembly (it contains base classes such as String, Object , and Exception ), a given version of mscorlib.dll is directly tied to the same version of mscorwks.dll and cannot be loaded independently. The two are tied closely together because they share data structures that must be in sync. Once you've picked a version of the CLR to load in the process, you have no say over which version of mscorlib.dll gets loaded.
Other than mscorlib.dll, the class library assemblies aren't tied to a particular version of mscorwks.dll, but nevertheless selecting a version of the core CLR files also influences the versions of the class libraries used. As discussed earlier, the .NET Framework consists of two major pieces: the core CLR and the class libraries. A version of the core CLR and the corresponding version of the class libraries are built and tested to work together. Using this matched set together results in the most consistent, predictable experience.
Once a CLR is loaded in the process, all requests to load one of the class library assemblies results in the loading of that version of the assembly that matches the CLR in the process. This is true even if the application running in the process references a different version. For example, say a developer has built an add-in that references the version of System.Windows.Forms that shipped with .NET Framework 1.0 (that is, version 1.0.3300). Now, say that control is hosted in a process that has loaded .NET Framework 1.1 Even though the control was compiled with a reference to version 1.0.3300, the version of System.Windows.Forms that is used at run time will be the version that shipped with .NET Framework 1.1 (that is, version 1.0.5000).
At first glance, this design can seem overly restrictive , especially in scenarios like the one I've been discussing in which a host can load add-ins written with various versions of the .NET Framework. This is one point where the different perspectives on side by side described earlier begin to blur. It is also a point at which the discussion about the compatibility between multiple versions of the .NET Framework comes into play. Earlier I said that the ability to run multiple versions of a given assembly simultaneously within a process was a key to solving DLL Hell because it loosened the backward-compatibility requirements for a given assembly. But on the other hand, I just discussed a design that doesn't use that side-by-side capability when it comes to the .NET Framework assemblies themselves . If a single version of a given assembly is always going to be loaded, its requirements for backward compatibility are very high.
Thankfully, the situation isn't as inconsistent as it seems because the behavior I've just described is only the default. You can use application configuration files to indicate that you'd like a version of a given class library assembly loaded different than the one that matches the CLR in the process. These overrides can be specified for each application domain in the process. I examine the details of how to do this in Chapter 7. Furthermore, it's likely that a future version of the CLR might have a more flexible solution to the compatibility concerns brought on by forcing only a particular set of assemblies to be loaded into a process.
.NET Framework Updates
One of the basic decisions a host has to make as part of its overall versioning strategy is what its tolerance is for handling updates to the CLR it has chosen to run with. The decision whether to load a specific CLR version always or to take the latest is a direct consequence of the degree to which the host would like to be insulated from potential compatibility issues caused by updates made to the CLR it uses.
A simple example helps illustrate the basic point. Consider a scenario in which two hosts, HostFix.exe and HostFloat.exe, exist on a machine that has .NET Framework 1.1 installed. HostFix.exe specifies that .NET Framework 1.1 should always be used, whereas HostFloat.exe indicates it would like to run with the latest version on the machine. At some later point in time, .NET Framework 2.0 is installed. The next time HostFix.exe starts, it uses .NET Framework 1.1 just as it always has. In contrast, HostFloat.exe now begins to use .NET Framework 2.0. In this scenario, the installation of .NET Framework 2.0 directly affects HostFloat.exe, but HostFix.exe remains unaffected. By choosing to specify an exact version, HostFix.exe has insulated itself from this particular update to the CLR.
It's important to notice, however, that although specifying an exact version means you won't be affected by major product releases like in the scenario just described, you still will be affected by bug fix updates made to the version of the CLR you have specified.
To understand the degree to which you are exposed to these kinds of updates, you must be aware of the following types of .NET Framework releases and how they are applied:
Choosing Your Strategy: Fix or Float
I've discussed how multiple versions of the .NET Framework are installed, how the shim loads a particular version, the implications that has on the class libraries loaded, and what the basics of the CLR upgrade story are. Given all that, how do you pick whether to always load a specific CLR version (and if so, which one) or to always take the latest?
In practice, the majority of hosts choose always to load a specific version of the CLR (and therefore the class library assemblies). Clearly, the primary advantage in this approach is control: you as a host can control your runtime environment to the greatest extent possible. We've all experienced incompatibilities from time to time when forced to upgrade to a new version of a platform without explicit consent . Also, bug fix updates still can affect you, but the .NET Framework team has built in the concept of side by side specifically to enable hosts and other applications to remain isolated. That said, the version with which you choose to run should be straightforward: always run with the version you have built and tested against. If you're unsure whether that version exists on all machines you must run on, you can always play it safe and redistribute the version of the .NET Framework you require along with your application. A primary benefit of side by side is that even when you redistribute a version of the .NET Framework along with your application, other applications on the machine do not start using it by default. So you can be assured that installing your application won't affect other applications.
The Server and Workstation Builds
The second startup setting is the choice between build types. The CLR comes in two flavorsa workstation build and a server build. As the names suggest, the workstation build is tuned for workstations, whereas the server build is tuned for the high-throughput scenarios associated with multiprocessor server machines.
The difference between the two builds is in the way the garbage collector works. The server build creates garbage collection heaps based on the number of processors and can therefore take advantage of the fact that multiple processors exist to make collections parallel. The server build of the CLR is so optimized for multiprocessor machines that the startup shim doesn't even allow it to be run on machines with just one processor! If you specify the server build on a uniprocessor machine, the workstation build is always loaded instead.
The default build type is always workstation. That is, if you don't specify a preference, the workstation build is always used. You might assume that the default build on multiprocessor machines is the server build, but it isn't. The default is always workstation regardless of the number of processors on the machine. Therefore, if you have a server-based application, you'll always want to request the server build specifically so you're sure you'll get it on multiprocessor machines.
Concurrent Garbage Collection
If you are using the workstation build of the CLR, you can specify another startup setting to configure the garbage collector. The CLR garbage collector can run in either concurrent mode or nonconcurrent mode. If you are running on a computer with more than one processor, concurrent mode causes garbage collections to happen on a background thread at the same time that user code is running on foreground threads. If your computer has only one processor, garbage collections happen on the same threads that are running user code. Concurrent collections are appropriate for applications that have a high degree of user interactivity. The goal of the concurrent collection mode is to keep the application as responsive as possible. In contrast, the nonconcurrent garbage collector does the collections on the same threads on which the user code is running. It might seem counterintuitive at first, but nonconcurrent collections result in much higher throughput overall. The primary reason for the increased throughput is because collections are done on the same thread, so there is no need to synchronize the threads doing the collections with the threads running user code.
Remember, too, that the concurrent garbage collection mode is available only when running the workstation build. If you select the server build, you'll always run with nonconcurrent collections. If you specify the server build and concurrent collections, the concurrent collection setting will be ignoredyou'll always get nonconcurrent collections.
The following points summarize how the concurrent garbage collection settings relate to the two CLR builds.
A more in-depth discussion of concurrent and nonconcurrent garbage collection is beyond the scope of this book. Jeffery Richter's book Applied Microsoft .NET Framework Programming from Microsoft Press dedicates an entire chapter to garbage collection and is widely considered the most in-depth, accurate, and well-explained description of the CLR garbage collector.
Domain neutral refers to the ability to share the jit-compiled code for an assembly across all application domains in a process, thus reducing the amount of memory used. Unlike the rest of the settings discussed throughout this chapter, you don't have to specify your domain-neutral settings at startup time. These settings can be specified in a few different ways, most of which can be done later. In addition to the startup settings, you can configure domain-neutral behavior through custom attributes, using application domain configuration settings or by implementing the IHostControl interface introduced in Chapter 2. A full description of domain-neutral code and the various ways it can be configured is given in Chapter 9. Here, I just discuss the options available for configuring domain-neutral code at startup.
At startup time, you can choose from three options to configure how the CLR loads domainneutral code:
In practice, it turns out that having just these three general options doesn't work very well in most hosting scenarios. Almost all CLR hosts contain some managed code in addition to the unmanaged code that is used to initialize and start the CLR. Ideally , the managed portion of the host would be loaded domain neutral because it is guaranteed to be used in all application domains. As a result, hosts written before the .NET Framework version 2.0 release typically choose to give the managed portion of their host a strong name and load all strong-named assemblies domain neutral. However, choosing this setting means that all add-ins that happen to be strong named are loaded domain neutral as well. The primary disadvantage of this, because assemblies loaded domain neutral cannot be unloaded, is that some add-ins exist in the process until it is shut down, thus using memory that ideally could be reclaimed for better purposes. To support this scenario, the IHostControl interface includes a method that enables a host to supply the specific assemblies that should be loaded domain neutral. In this way, a host can elect to load only its own implementation domain neutral, not any add-ins.
I expect the scenarios in which hosts must use the preceding three options to decrease as a result of the more flexible support provided by IHostControl .
Customizing the Microsoft .NET Framework Common Language Runtime
Authors: Pratschner S
Published year: 2005