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
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
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
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
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
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
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
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
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
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)
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
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
subkey. Each time a new version of the .NET Framework is installed, a new
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
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
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
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
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
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
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
At first glance, this design can seem overly
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
.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
A simple example helps
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
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
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
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
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.
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.
I expect the scenarios in which hosts must use the