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:

  1. The version of the CLR to load into the process

  2. Whether you’d like the server or workstation build

  3. Garbage collection options

  4. Settings to control domain-neutral loading of code

The following sections describe these settings in detail.

Version

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:

  • The version your application was built and tested against

  • Any functionality available in a certain release on which either your application or extensions to your application depend

  • The Microsoft strategy for issuing service releases to the .NET Framework

  • The compatibility of the .NET Framework between releases

You can start to imagine the complexities you might run into when considering multiple versions of a platform coexisting—especially 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:

  1. Registry entries that indicate a new version of the .NET Framework has been installed are made.

  2. The CLR files and a copy of the .NET Framework class libraries are installed into a subdirectory of your Windows directory.

  3. The .NET Framework class libraries are installed in the global assembly cache (GAC).

  4. The CLR startup shim is installed in the Windows system directory if the version of the CLR you are installing is newer than any other version that already exists on the machine. The shim is the centerpiece of the side-by-side architecture as I explain in a minute.

.NET Framework Registry Keys

The registry key under which all .NET Framework–related keys are written is as follows:

HKEY_LOCAL_MACHINESOFTWAREMicrosoft.NETFramework

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.

Registry entries when multiple versions of the CLR are 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.

Registry entries showing the version number for the CLR contained in .NET Framework 1.0

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.NETFramework. Figure 3-3 shows the state of this directory after both .NET Framework 1.0 and .NET Framework 1.1 have been installed.

Contents of %windir%Microsoft.NET with multiple versions of the .NET Framework 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.

Contents of the GAC with multiple versions of the .NET Framework installed

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 number—not 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

Win32 Version Number

Assembly Version Number

Microsoft .NET Framework 1.0

1.0.3705

1.0.3300

Microsoft .NET Framework 1.1

1.1.4322

1.0.5000

Microsoft .NET Framework version 2.0

2.0.41013

2.0.3600

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 stated—it 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.

The shim and multiple versions of the .NET Framework

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_MACHINESOFTWAREMicrosoft.NETFramework as shown in Figure 3-6. The setup program always sets this to %windir%Microsoft.NETFramework and currently doesn’t offer an option to change it.

The InstallRoot registry value

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:WindowsMicrosoft.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:WindowsMicrosoft.NETFrameworkv1.1.4322mscorwks.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:

  • Single bug fix releases. These releases consist of a fix for a single bug and are usually made available in response to a request from a specific customer or because a general issue related to security was found. You might have heard these releases referred to as hot fixes or something similar. These fixes usually affect only one file and are typically made available through Microsoft Windows Update or a similar mechanism aimed at wide distribution. When deployed, the affected file overwrites the existing file on disk. The host can’t explicitly control whether to accept this fix—once the administrator installs the fix, the host begins to use it.

  • Service packs. Service packs are collections of bug fixes. From a host’s point of view, a service pack behaves just as a single bug fix release does in that the affected files are overwritten, thus automatically affecting the host.

  • Feature releases. These releases contain new feature work and are installed side by side. The releases used as examples throughout this chapter (.NET Framework 1.0 and .NET Framework 1.1) are examples of feature releases. Because feature releases are installed side by side, the host has the flexibility to decide whether to pick up the new release automatically.

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 flavors—a 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 ignored—you’ll always get nonconcurrent collections.

The following points summarize how the concurrent garbage collection settings relate to the two CLR builds.

  • Server build. When using the server build, you’ll always get nonconcurrent collections as noted earlier. This combination is used when server-style applications that require high throughput are run on multiprocessor machines (recall, the server build doesn’t run on uniprocessor machines). Microsoft SQL Server and Microsoft ASP.NET are examples of hosts that use this combination.

  • Workstation build—UI-intensive applications. If your application requires a high degree of user interactivity, likely you’ll be best off using the workstation build and concurrent collections. Applications based on Microsoft Windows Forms or a similar class library are examples of the type of application benefiting from this scenario.

  • Workstation build—batch (non-UI) applicationsIf you’re running the workstation build, but your application does not display UI (or at least not much), whether to enable concurrent collections isn’t as clear. The best thing to do is to run your app in both modes and measure the performance. Several people have found that their batch-style apps perform better using nonconcurrent collections when the workstation build is used.

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 Code

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:

  • No assemblies are loaded domain neutral (except mscorlib, which is always loaded domain neutral)

  • All assemblies are loaded domain neutral

  • Only assemblies with strong names are loaded domain neutral

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.

Setting the Startup Options Using CorBindToRuntimeEx

Once you understand the CLR startup options, setting them using CorBindToRuntimeEx is easy. The version and build type options map directly to parameters to the API, and the concurrent garbage collection and the domain-neutral options are specified as flags.

Here’s the definition of CorBindToRuntimeEx from mscoree.h:

STDAPI CorBindToRuntimeEx(LPCWSTR pwszVersion,
                          LPCWSTR pwszBuildFlavor,
                          DWORD startupFlags,
                          REFCLSID rclsid,
                          REFIID riid,
                          LPVOID FAR *ppv);

Table 3-2 describes the parameters to CorBindToRuntimeEx and how they are used to set the CLR startup options.

Table 3-2. CorBindToRuntimeEx Parameters to Configure CLR Startup

CLR Startup Setting

Parameter

Legal Values

Default

Version

pwszVersion

A string describing the version of the CLR to load or specifying NULL.

The string must be in the following form:

v.major.minor.build

For example, to load the CLR that comes with .NET Framework 1.0, you’d pass

v1.0.3705.

Passing NULL loads the latest version of the CLR installed on the machine.

NULL

Build

pwszBuildFlavor

A string describing whether to load the server or workstation build. The following are valid values: svr and wks.

Remember, too, that svr value is ignored on uniprocessor machines. The workstation build is always loaded in such cases.

wks

Concurrent garbage collection

startupFlags

Concurrent garbage collection is turned on by passing STARTUP_CONCURRENT_GC to startupFlags. This flag, along with the flags for the domain-neutral options, is specified by the STARTUP_FLAGS enumeration in mscoree.h.

Enabled

Domain-neutral code

startupFlags

The following are valid values from STARTUP_FLAGS:

STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN. No assemblies are loaded domain neutral.

STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN. All assemblies are loaded domain neutral.

STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST. Strong-named assemblies are loaded domain neutral.

No assemblies loaded domain neutral (except mscorlib)

You’ll notice that CorBindToRuntimeEx has three parameters in addition to the ones described in Table 3-2: rclsid, riid, and ppv. These parameters are used to get an interface pointer of type ICLRRuntimeHost through which to access all the functionality in the COM interfaces that are part of the CLR hosting APIs. The capabilities available through ICLRRuntimeHost are described in Chapter 2. Table 3-3 describes the parameters used to get a pointer to ICLRRuntimeHost.

Table 3-3. CorBindToRuntimeEx Parameters to Return ICLRRuntimeHost

Parameter

Description

rclsid

[in][1] The CLSID of the object containing the ICLRRuntimeHost interface. Always pass CLSID_ClrRuntimeHost.[2]

riid

[in] The IID of ICLRRuntimeHost. Always pass IID_ICLRRuntimeHost.

ppv

[out] The address of the returned ICLRRuntimeHost pointer.

[1] [in] means that the parameter is passed in to the function. [out] means the parameter is returned from the function.

[2] The need to pass the CLSID and the IID for the runtime host interface might seem unnecessary. The reason these parameters exist is so the API can return interfaces of different types in a future release.

Starting the CLR

Calling CorBindToRuntimeEx sets the CLR startup options and loads the core runtime engine DLL (mscorwks.dll) into the process. However, the runtime must still be "started" before any managed code can be run. Once initialized, starting the CLR is easy. Just call the Start method on the ICLRRuntimeHost pointer you got back from calling CorBindToRuntimeEx as shown in the following example.

// Set the CLR startup options
    HRESULT hr = CorBindToRuntimeEx(
      L"v2.0.41013",
      L"svr",
      NULL,
      CLSID_CLRRuntimeHost,
      IID_ICLRRuntimeHost,
      (PVOID*) &pCLR);

   // Use ICLRRuntimeHost to start the CLR
   hr = pCLR->Start();
   assert(SUCCEEDED(hr));

Handling Failures from CorBindToRuntimeEx

Recall that only one copy of the CLR can be loaded in a process at a time. So calling CorBindToRuntimeEx multiple times has no effect. If you do so, S_FALSE is returned on all but the first call. Successful calls to the API return S_OK, as you’d expect.

If you pass in a version that is not present on the machine, CLR_E_SHIM_RUNTIMELOAD (defined in CorError.h in the .NET Framework software development kit) is returned. In addition, the dialog box shown in Figure 3-7 is displayed instructing the user to install the desired version of the CLR.

Incorrect version dialog box

Figure 3-7. Incorrect version dialog box

Clearly, scenarios exist, especially in server applications, in which a host would not want to display UI when the requested version of the CLR is not present. You can prevent this dialog box from appearing by calling the SetErrorMode Win32 API before calling CorBindToRuntimeEx. SetErrorMode takes a single parameter that controls whether dialog boxes are displayed for certain types of errors. Be sure to pass SEM_FAILCRITICALERRORS:

SetErrorMode(SEM_FAILCRITICALERRORS);

Deferring CLR Initialization and Startup

In some scenarios a host might elect to delay the initialization and loading of the CLR until it is actually needed rather than doing it upfront. Loading the CLR lazily can reduce both your startup time and startup working set. However, waiting until later to call CorBindToRuntimeEx opens a window in which a copy of the CLR might get loaded into the process without your knowledge and therefore without an opportunity for you to set the startup options you want.

The classic case in which this can occur is with COM interoperability. If your host doesn’t initialize the CLR at startup and some unmanaged code in your process loads a managed type through COM, the activation of that type brings the CLR into the process. Because the host is out of the loop at this point, the default settings chosen by the default CLR host are used (refer to Chapter 4 to see how the default CLR host chooses these settings). This can result in loading a CLR that does not match what the host would have chosen. For example, a different version of the CLR might be loaded or the wrong build type might be chosen.

Fortunately, hosts can use a function on the shim called LockClrVersion to close this window. When you call LockClrVersion, you’re essentially stating that you are the only one allowed to initialize the CLR in this process. When the shim receives the first request to run managed code, it will call a function that you provide to enable you to initialize the CLR the way you see fit. In the preceding scenario, the host’s function would have been called when the managed type was accessed through the COM interoperability layer, thereby giving the host a chance to initialize and load the CLR.

Here’s the definition of LockClrVersion as found in mscoree.h:

typedef HRESULT (__stdcall *FLockClrVersionCallback) ();
STDAPI LockClrVersion(FLockClrVersionCallback hostCallback,
                      FLockClrVersionCallback *pBeginHostSetup,
                      FLockClrVersionCallback *pEndHostSetup);

The parameters to LockClrVersion are summarized in Table 3-4.

Table 3-4. Parameters to LockClrVersion

Parameter

Description

hostCallback

[in] A pointer to a host-supplied function the shim will call when the CLR needs to be initialized

pBeginHostSetup

[out] A pointer to a CLR-supplied function that the host must call just before it begins to initialize the CLR

pEndHostSetup

[out] A pointer to a CLR-supplied function that the host must call after it has initialized the CLR

At first glance, pBeginHostSetup and pEndHostSetup might seem unnecessary. After all, the shim can tell when the CLR has been initialized after the call to hostCallBack returns. The reason these additional parameters are needed is to notify the shim about which thread the initialization is happening on. The shim needs this information mostly for internal implementation reasons relating to the fact that only one thread in the process is allowed to initialize the CLR. The shim also uses the information to block other threads that might enter the process and wish to run managed code from proceeding until the initialization is done.

Example 3-1 from the sample DeferredStartup.exe shows how to use LockClrVersion to delay the loading of the CLR. In this example, I force the CLR into the process by activating a managed type through the COM Interoperability layer as in the scenario described earlier. When the managed type is activated, the host-supplied function is called to initialize the CLR.

Example 3-1. Using LockClrVersion to Load the CLR

#include "stdafx.h"
#include <mscoree.h>

// Declare globals to hold function pointers to call to notify the shim when the
// initialization of the CLR is beginning and ending.
FLockClrVersionCallback g_beginInit;
FLockClrVersionCallback g_endInit;

// This function is registered as the host callback provided to the CLR by LockClrVersion.
//
 The shim will call this function the first time it receives a request to run managed code.
STDAPI InitializeCLR()
{
   // Notify the CLR that initialization is beginning
   g_beginInit();

   // Initialize the CLR.
   ICLRRuntimeHost *pCLR = NULL;
    HRESULT hr = CorBindToRuntimeEx(
      L"v2.0.41013",
      NULL,
      NULL,
      CLSID_CLRRuntimeHost,
      IID_ICLRRuntimeHost,
      (PVOID*) &pCLR);

   assert(SUCCEEDED(hr));

   // Start the CLR.
   pCLR->Start(NULL, NULL);

   // Notify the CLR that initialization has completed.
   g_endInit();

   return S_OK;

}

int main(int argc, char* argv[])
{
   HRESULT hr = S_OK;

   // Call LockClrVersion so the InitializeCLR always get called to set up the runtime.
   LockClrVersion(InitializeCLR, &g_beginInit, &g_endInit);

   // Initialize COM and create an instance of a managed type through COM Interop. This
   // will require the CLR to be loaded—InitializeCLR will be called.
   CoInitialize(NULL);

   CLSID clsid;
   hr = CLSIDFromProgID(L"System.Collections.SortedList", &clsid);
   assert(SUCCEEDED(hr));

   IUnknown *pUnk = NULL;
   hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (LPVOID *) &pUnk);
   assert(SUCCEEDED(hr));

   pUnk->Release();

   CoUninitialize();

   return 0;

}

LockClrVersion can be used for another, less obvious purpose—to prevent the CLR from ever being loaded in a process. The scenarios in which you’d want to do this are clearly limited, but if you had a requirement to prevent managed code from running in your process completely, LockClrVersion is the way to do it. As you’ve seen, LockClrVersion takes a callback function that gets invoked when the CLR needs to be initialized. If you return a failure HRESULT from this callback, the CLR would not be loaded. In this way, you can prevent the CLR from ever entering your process.

Note

Note

There’s another technique you can use to accomplish a subset of what LockClrVersion provides. Recall that CorBindToRuntimeEx has a startupFlags parameter. One of the flags you can pass is called STARTUP_LOADER_SETPREFERENCE. When you pass this flag to CorBindToRuntimeEx, the startup shim does not initialize the CLR, but rather just remembers the version number you’ve passed through the pwszVersion parameter. Later, when the CLR needs to be initialized—either implicitly (as happens when a managed type is created from COM) or explicitly by another call to CorBindToRuntimeEx that does not set STARTUP_LOADER_SETPREFERENCE—the shim loads the version of the CLR that it remembered earlier. In this way, you can use CorBindToRuntimeEx to indicate which version of the CLR to load in a delayed fashion.

Using STARTUP_LOADER_SETPREFERENCE with CorBindToRuntimeEx is more limited than LockClrVersion in three key ways:

  • You can specify only a version numberSTARTUP_LOADER_SETPREFERENCE remembers only the version number you passed to CorBindToRuntimeEx. It does not remember your preference for build type, your garbage collection settings, or your domain-neutral code settings. When the CLR is started later, defaults are used for these other settings.

  • You cannot provide a callback to do extra processing. As you’ve seen, LockClrVersion takes a callback that is invoked when the CLR needs to be initialized. In addition to calling CorBindToRuntimeEx, hosts often use this callback to create their host control object and report it to the CLR so the appropriate hosting managers are set up properly. If you don’t use LockClrVersion, there’s no way to accomplish this, or other related startup tasks, in a delayed fashion.

  • You cannot prevent the CLR from entering the process. You can use LockClrVersion to prevent the CLR from ever entering a process by returning a failure HRESULT from your callback. You can’t do this using STARTUP_LOADER_SETPREFERENCE and CorBindToRuntimeEx.

Even with these limitations, passing STARTUP_LOADER_SETPREFERENCE to CorBindToRuntimeEx can still be useful. If all you need to do is ensure a specific version of the CLR is loaded in a lazy fashion, you can do this with much less code than it takes to use LockClrVersion properly.

The CLR and Process Lifetime

Once loaded into a process, the CLR can never be completely removed. That is, there is no way to guarantee that all files and data structures associated with the CLR are cleaned up and all the memory they required is returned. You can, however, disable the CLR. Specifically, when you disable the CLR, it no longer is able to run managed code in the process. The process itself is not damaged in the sense that unmanaged code continues to run just fine, but further attempts to run managed code are not honored.

You disable the CLR by using the Stop method on ICLRRuntimeHost. Calls to Stop must be paired with calls to ICLRRuntimeHost::Start, which is discussed earlier in the chapter. That is, you must call Stop once for each call you made to Start for the CLR to be disabled. It’s also important to note that once the CLR has been disabled using Stop, it can never be restarted in the same process again.

Summary

The CorBindToRuntimeEx API gives you explicit control over when the CLR is loaded and enables you to configure the basic settings that determine how the CLR behaves in your process. By using CorBindToRuntimeEx you can specify which version of the CLR gets loaded, whether the CLR is optimized for a server application or a workstation application, and how code is shared across the application domains in the process. Of these settings, determining the most appropriate version to load is the most complex because you must understand the implications of having more than one version of the .NET Framework installed on a single machine. In addition to understanding the basic side-by-side architecture of the CLR, you also must be aware of more subtle issues such as the impact of CLR servicing releases on the version you choose. Although the CLR has rich support to customize startup, the support to control CLR shutdown is less sophisticated. There is no way to remove the CLR from a process completely after it has been loaded, but you can disable it. Disabling the CLR prevents you from running managed code in the future and removes some of the CLR files and data structures from the process, but the shutdown isn’t completely clean. In addition, even if you disable the CLR in this fashion, you cannot restart it again. You must start an entire new process to reinitialize the CLR.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.104.183