Customizing the Microsoft .NET Framework Common Language Runtime
|
The default behavior described previously can be customized for the version, build type, and garbage collection settings using application configuration files. The only setting you cannot change using a configuration file is the specification for which assemblies should be loaded domain neutral. The domain-neutral settings can be set only by using the CLR hosting interfaces or through a custom attribute, as discussed in Chapter 9. Application configuration files are Extensible Markup Language (XML) files that are associated with an application by naming convention and directory. When using the default host, configuration files must be placed in the same directory as the application and are named by simply appending .config to the name of the executable file for the application. For example, the configuration file for myapp.exe must be called myapp.exe.config. Configuration files can be used to customize the settings for both the managed executable scenario and the COM interoperability scenario described earlier. It might seem odd that a configuration file can be used to customize the CLR in the COM interoperability scenario. After all, the configuration file has settings that are relevant only to managed programs, yet the main executable for the application in this scenario is unmanaged. This works because the shim looks for the configuration file based on the name of the executable that started the process (as obtained by calling the Microsoft Win32 API GetModuleFileName). The fact that the primary executable does not consist of managed code is irrelevant. There are two ways to edit configuration files: by hand or with the .NET Framework configuration tool. For purposes of this discussion, the configuration tool is somewhat limited in that it doesn't enable you to set the CLR version that should be used to run the application. For this reason, and for a better understanding of what's going on under the covers, I stick to showing the XML itself. However, I point out the scenarios in which the tool can be used instead. In the following sections, I describe how to use configuration files to customize the garbage collection, build type, and version settings. Concurrent Garbage Collection
Recall that the default setting for concurrent garbage collection is on. That is, collections will happen on background threads while the application is running if the application is running on a multiprocessor machine. As described, the default for concurrent garbage collection and that of the build type (workstation) work nicely together. It is possible, however, to disable concurrent garbage collection through the configuration file. The value for concurrent garbage collection is set using the <gcConcurrent> element in the configuration file. This element must always be nested inside the <runtime> element. (See the .NET Framework SDK documentation for a description of the complete configuration file schema.) The element <gcConcurrent> has a single attribute called enabled whose value is either the string "true" (the default) or "false". The following configuration file shows how to disable concurrent garbage collection for a given application: <configuration> <runtime> <gcConcurrent enabled="false" /> </runtime> </configuration
Concurrent garbage collection is one of the settings that can be changed using the .NET Framework configuration tool as well. You can find a shortcut to the tool in Control Panel under Administrative Tools. I don't go into detail on using the tool here, but if you've never used the tool, an overview can be found in the SDK guide. The options for concurrent garbage collection are shown in the Properties dialog box for an application, as shown in Figure 4-5. Figure 4-5. Setting concurrent garbage collection using the .NET Framework configuration tool
The Properties dialog box presents the options as a set of radio buttons under Garbage Collection Mode. The wording of the options conveys whether collections are done in the background or not. The option Run In Background For User Applications corresponds to the default of enabled, whereas the Run In Foreground For Server Applications option corresponds to not enabled. Be careful not to confuse the term server applications in this dialog box with the server build of the CLR. By choosing this option, you are configuring the workstation build to run without concurrent garbage collectionyou are not directing the shim to load the server build. You can specify that the server build should be used either by calling CorBindToRuntimeEx as described in Chapter 3 or by using a configuration file as described in the next section. Build Type
You can change the default build type of workstation to server using the gcServer element in an application configuration file. The gcServer element has an attribute called enabled that you set to true if you'd like to run the server build. Keep in mind that the server build is loaded only on multiprocessor machines, however. Even if you set gcServer to true on a single-processor machine, the workstation build will be loaded. The following configuration file uses the gcServer element to specify that the server build of the CLR should be used: <configuration> <runtime> <gcServer enabled="true" /> </runtime> </configuration
Unlike the settings for concurrent garbage collection, there is no support in the .NET configuration tool for setting the build typeyou must edit the XML directly. As I explained in Chapter 3, you can also specify a build type using the pwszBuildFlavor parameter to CorBindToRuntimeEx. If you specify a build type using both CorBindToRuntimeEx and the gcServer element in the application's configuration file, the setting passed to CorBindToRuntimeEx takes precedence. Changing the Build Type on Older Versions of the CLR
The ability to specify a build type using an application configuration file is new in .NET Framework 2.0. If you want to load the server build for executables running with either .NET Framework 1.0 or .NET Framework 1.1, you must write a small CLR host that uses CorBindToRuntimeEx to specify the build type. Fortunately, it's very easy to write an application launcher that calls CorBindToRuntimeEx to set the server build and then runs the original application. The following sample does just that. svrhost.exe is a CLR host that can be used to run managed executables from the command line using the server build of the CLR. svrhost.exe takes the managed executable to run as a command-line parameter. In addition to the executable, you can also pass any command-line arguments for the managed executable to svrhost.exe as well. For example, say you have an application called paystub.exe that you normally invoke as follows: C:\> paystub stevenpr 013103
To run paystub.exe with the server build of the CLR, you'd invoke it using svrhost.exe as follows: C:\> svrHost paystub stevenpr 013103 The code for svrhost.exe is very straightforward, as shown in Listing 4-1. svrhost.exe uses CorBindToRuntimeEx to load the server build of the CLR. After the CLR has started, svrhost.exe gathers the name of the program to execute along with any arguments from the command line. It then uses the ExecuteAssembly method on the System.AppDomain class to run the specified program in the default application domain. Notice that svrhost.exe uses an interface called ICorRuntimeHost instead of the ICLRRuntimeHost interface I introduced in Chapter 2. ICorRuntimeHost is the primary hosting interface in .NET Framework 1.0 and .NET Framework 1.1. In .NET Framework 2.0, that interface is replaced with ICLRRuntimeHost. Because you want svrhost.exe to run on these older versions of the CLR, the sample sticks with ICorRuntimeHost. Listing 4-1. svrhost.cpp
#include "stdafx.h" // needed for CorBindToRuntimeEx #include <mscoree.h> // include the typelib for mscorlib for access to the default AppDomain through COM Interop #import <mscorlib.tlb> raw_interfaces_only high_property_prefixes("_get","_put","_putref") using namespace mscorlib; int _tmain(int argc, _TCHAR* argv[]) { if (argc < 2) { printf("Usage: SvrHost <Managed Exe> [Arguments for Managed Exe]\n"); return 0; } ICorRuntimeHost *pCLR = NULL; // Initialize the CLR. Specify the server build. Note that I'm // loading the CLR from .NET Framework 1.1. HRESULT hr = CorBindToRuntimeEx( L"v1.1.4322", L"svr", NULL, CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (PVOID*) &pCLR); assert(SUCCEEDED(hr)); // Start the CLR pCLR->Start(); // Get a pointer to the default AppDomain _AppDomain *pDefaultDomain = NULL; IUnknown *pAppDomainPunk = NULL; hr = pCLR->GetDefaultDomain(&pAppDomainPunk); assert(pAppDomainPunk); hr = pAppDomainPunk->QueryInterface(__uuidof(_AppDomain), (PVOID*)&pDefaultDomain); assert(pDefaultDomain); // get the name of the exe to run long retCode = 0; BSTR asmName = SysAllocString(argv[1]); // Collect the command-line arguments to the managed exe. These must be // packaged as a SAFEARRAY to pass to ExecuteAssembly. SAFEARRAY *psa = NULL; SAFEARRAYBOUND rgsabound[1]; rgsabound[0].lLbound = 0; rgsabound[0].cElements = (argc - 2); psa = SafeArrayCreate(VT_BSTR, 1, rgsabound); assert(psa); for (int i = 2; i < argc; i++) { long idx[1]; idx[0] = i-2; SafeArrayPutElement(psa, idx, SysAllocString(argv[i])); } // Run the managed exe in the default AppDomain hr = pDefaultDomain->ExecuteAssembly_3(asmName, NULL, psa, &retCode); assert(SUCCEEDED(hr)); // clean up SafeArrayDestroy(psa); SysFreeString(asmName); pAppDomainPunk->Release(); pDefaultDomain->Release(); _tprintf(L"\nReturn Code: %d\n", retCode); return retCode; }
Version
Microsoft's goal is to keep each release of the CLR as backward compatible as possible with all previous releases. (I say as possible because we all know that complete compatibility can never be guaranteed.) This is the approach taken for years by other Microsoft products, including the Windows operating system, Microsoft Office, and Microsoft SQL Server. Because of the focus on backward compatibility, it is quite likely that an application built with one version of the CLR will run just fine on a later version of the CLR. The default behavior of the shim is to run an application with the version of the CLR used to build it (minus the upgrades scenarios I discussed). However, it might be the case that you as an application author want to be able to take advantage of a new CLR when it is released without having to rebuild and re-release your application. Each new version of the CLR will undoubtedly contain numerous bug fixes and performance enhancements, and because backward compatibility is likely, you can choose to have the shim run your application with a new version when it becomes available. The <supportedRuntime> Element
The default host enables you to state your desire to run with different versions of the CLR using the <supportedRuntime> element in your application configuration file. The <supportedRuntime> element has a single attribute called version that indicates the version of the CLR you support. By including a <supportedRuntime> element for a given version, you are telling the shim that you are willing to have your application run with that version of the CLR. You might also specify more than one <supportedRuntime> element. This enables your application to run on any machine that has at least one of your supported versions. If, through testing, you determine that several different versions of the CLR are acceptable, supplying the full list can dramatically broaden the number of machines your application can run on without you having to worry about redistributing a given version of the .NET Framework. Clearly, the safest way to determine which CLR versions your application can run on is through testing. As discussed earlier, backward compatibility cannot be universally guaranteed and can really be defined only within the context of a given application. Be aware of the potential for compatibility issues when specifying a <supportedRuntime> element for a version of the CLR you haven't tested with. If you've specified multiple <supportedRuntime> elements, the order in which the elements appear in the configuration file is the order in which the shim will try to load those versions of the CLR. If none of the versions you specify can be found, the shim will display an error and your application won't run. All <supportedRuntime> elements must be nested inside the <startup> tag of the runtime section of your configuration file. The value of the version attribute is a string in the following format: "v + <major number> + <minor number> + <build number>" For example, setting version to "v1.1.4322" indicates that your application can be run with the version of the CLR that shipped with .NET Framework 1.1. Recall from the previous discussion in Chapter 3 of how the shim locates a CLR that this string maps to the name of the subdirectory (under the main CLR installation root directory) in which the desired version of the CLR is installed. Here's an example configuration file that causes the shim to attempt to run the application with the last two publicly released versions of the CLR. Note that the versions are specified from newest to oldest. In this way, the version with the latest bug fixes and performance enhancements is tried first. <configuration> <startup> <supportedRuntime version="v2.0.41013" /> <supportedRuntime version="v1.1.4322" /> </startup> </configuration By design, the <supportedRuntime> element enables you to run your application with a version of the CLR that is different from the one used to develop the application. So it's important to use this element with caution. The whole premise behind the side-by-side architecture of the .NET Framework is to provide a platform where applications can remain isolated from changes made to the system without their knowledge. The <supportedRuntime> element is a handy tool to use when you have completed testing and are ready to make an explicit decision to allow your application to be upgraded. However, if you allow your application to be upgraded without explicit testing, you're opening yourself and your customers to potential unwanted compatibility problems. The <requiredRuntime> Element and .NET Framework 1.0
The preceding section describes how the <supportedRuntime> element can make it easier to deploy your application when you're not sure which version of the CLR is installed on a given target machine. This statement is true with one exception: you cannot use the < supportedRuntime> element on a machine that has only the version of the CLR that shipped with .NET Framework 1.0. That original version of the CLR does not include support for this element. Instead, the design at the time used a different element that is now deprecated called < requiredRuntime>. This element is similar in spirit to <supportedRuntime> in that it is used to indicate which version of the CLR an application should run with. However, because <requiredRuntime> has been retired in favor of <supportedRuntime>, it really has use only when you have an application you've built with .NET Framework 1.1, but you want to make sure it runs on machines with only .NET Framework 1.0 installed. (Of course, this scenario also requires that you haven't used any features available only in the new releases, but I'm ignoring that fact for now.) To support this scenario, you need a configuration file that contains a <requiredRuntime> element for the original version of the CLR and a <supportedRuntime> element for .NET Framework 1.1. Here's an example: <configuration> <startup> <supportedRuntime version="v1.1.4322" /> <requiredRuntime version="v1.0.3705" /> </startup> </configuration>
This configuration file enables an application built with either .NET Framework 1.0 or .NET Framework 1.1 regardless of which version is installed on the machine. Note
As described earlier, the order of these elements in the configuration file determines their priority. Hence, if both versions 1.1.4322 and 1.0.3705 are installed, 1.1.4322 will be used. If neither of those versions is installed, the shim will run the application with v1.0.3705 (that is, .NET Framework 1.0) if it is present. Unfortunately, your configuration file needs more work if you want to run an application built with .NET Framework 1.1 on .NET Framework 1.0. (Keep in mind, too, that these scenarios are restricted to the cases in which you're sure you don't rely on any features that aren't available in .NET Framework 1.0and that you've tested your application on that version.) Recall that in Chapter 3 I said that when you pick a version of the CLR to run your application, you also get the matching set of class library assemblies by default. Recall also that this behavior occurs only when either .NET Framework 1.1 or .NET Framework 2.0 is installed on the machine. If only .NET Framework 1.0 is available, you must include additional statements in the configuration file to redirect all assembly references back down to the versions that shipped with .NET Framework 1.0. You do this using the <bindingRedirect> tag in your application configuration file. You must include one <bindingRedirect> tag for each assembly that ships in the .NET Framework redistributable (unless you're sure there are assemblies you don't use). I know this sounds cumbersome, but fortunately, the same configuration file works for all applications, so once you get it right, you're set. Here's the configuration file used earlier with <bindingRedirect> entries for the .NET Framework assemblies. (I haven't included them all for the sake of brevity but the entire configuration file is available from the Microsoft Press download site for this book. See the Introduction of this book for the URL.) <configuration> <startup> <supportedRuntime version="v1.1.4322" /> <requiredRuntime version="v1.0.3705" /> </startup> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="System.Security" publicKeyToken="b03f5f7f11d50a3a" culture=""/> <bindingRedirect oldVersion="0.0.0.0-65535.65535.65535.65535" newVersion="1.0.3300.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Runtime.Remoting" publicKeyToken="b77a5c561934e089" culture=""/> <bindingRedirect oldVersion="0.0.0.0-65535.65535.65535.65535" newVersion="1.0.3300.0"/> </dependentAssembly> <--! Lots of others need to be included here..... </assemblyBinding> </runtime> </configuration> If you've been following along closely and are familiar with how <bindingRedirect> statements work, you're probably thinking that the configuration file I've just shown works great if my application runs with .NET Framework 1.0, but what if my application runs with .NET Framework 1.1 or 2.0? Won't all my assembly references get redirected down to the versions of the class library assemblies that shipped with .NET Framework 1.0? Well, the answer is yesthe configuration file as I've defined it doesn't work with all versions of the .NET Framework. What I'd really like is for the <bindingRedirect> statements to apply as specified if my application runs with .NET Framework 1.0 but to be ignored if my application runs with a later version (because later versions will ensure that my references to system assemblies automatically get redirected to the version that matches the CLR I've chosen). To accommodate this scenario, you must add the appliesTo attribute to the <assemblyBinding> element (under which all of your <bindingRedirect> statements lie) in your application configuration file. The value of the appliesTo attribute is the version of the CLR to which the statements in the <assemblyBinding> section of the configuration file apply. In other words, the <bindingRedirect> statements take effect only if the version of the CLR identified by the appliesTo attribute is loaded. In this case, I clearly want the <bindingRedirect> statements to apply only if version 1.0.3705 (also known as .NET Framework 1.0) is loaded. As a result, the final configuration file looks like this: <configuration> <startup> <supportedRuntime version="v2.0.1111" /> <supportedRuntime version="v1.1.4322" /> <requiredRuntime version="v1.0.3705" /> </startup> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1" appliesTo="v1.0.3705"> <dependentAssembly> <assemblyIdentity name="System.Security" publicKeyToken="b03f5f7f11d50a3a" culture=""/> <bindingRedirect oldVersion="0.0.0.0-65535.65535.65535.65535" newVersion="1.0.3300.0"/> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Runtime.Remoting" publicKeyToken="b77a5c561934e089" culture=""/> <bindingRedirect oldVersion="0.0.0.0-65535.65535.65535.65535" newVersion="1.0.3300.0"/> </dependentAssembly> <--! Lots of others need to be included here... </assemblyBinding> </runtime> </configuration> |
|