Customizing the Microsoft .NET Framework Common Language Runtime

You can use three sets of APIs to specify which assemblies you'd like to load domain neutral:

  • CorBindToRuntimeEx

  • The LoaderOptimization APIs

  • The CLR hosting interfaces

CorBindToRuntimeEx and the LoaderOptimization APIs were introduced in .NET Framework 1.0, whereas the hosting interfaces for working with domain-neutral assemblies are new to .NET Framework 2.0.

CorBindToRuntimeEx

CorBindToRuntimeEx is the function used to initialize the CLR in a process. The startupFlags parameter to CorBindToRuntimeEx is used to configure various aspects of the CLR, including which assemblies should be loaded domain neutral. The valid domain-neutral settings are given by the STARTUP_FLAGS enumeration from mscoree.idl:

typedef enum { // Other flags omitted... STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN = 0x1<<1, STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN = 0x2<<1, STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST = 0x3<<1, // Other flags omitted... } STARTUP_FLAGS;

These flags offer very coarse options for controlling domain-neutral loading. Each flag is tailored for a specific scenario, as described in the following list:

  • STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN The SINGLE_DOMAIN setting specifies that no assemblies are loaded domain neutral. As its name implies, this setting is geared toward applications that have only one application domain.

  • STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN All assemblies are loaded domain neutral when you pass STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN to CorBindToRuntimeEx. This setting is best for those applications that always load the same set of assemblies into all application domains.

  • STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST The MULTI_DOMAIN_HOST setting causes all assemblies with strong names to be loaded domain neutral. Assemblies with weak names are loaded into each application domain separately. This is the setting that .NET Framework 1.0 and .NET Framework 1.1 offer for extensible applications such as those I've been discussing throughout this book. This setting works great for extensible applications in which the host runtime has a strong name, but the add-in assemblies don't (the .NET Framework assemblies all have strong names). Of course, it's overly restrictive to assume that an add-in won't have a strong name. To solve this problem, the CLR added support for specifying an exact list of assemblies to load domain neutral using the hosting interfaces. I discuss that approach later in this section.

Note

Saying that "no assemblies are loaded domain neutral" when you specify STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN isn't completely accurate. Regardless of the settings you specify, the CLR always loads mscorlib domain neutral. mscorlib is granted full trust by default security policy, and it doesn't reference any other assemblies so it's not subject to the security and version policy complications described here.

The following example uses CorBindToRuntimeEx to specify that all assemblies with strong names should be loaded domain neutral:

hr = CorBindToRuntimeEx( L"v2.0.41013", L"wks", STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST, CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (PVOID*) &pCLR);

One advantage of specifying domain-neutral settings using the values from STARTUP_FLAGS is that you don't have to worry about whether the set of assemblies you are supplying forms a full closure. Clearly, the set of no assemblies and the set of all assemblies form a closure. In addition, loading all assemblies with strong names domain neutral forms a closure, too, because an assembly with a strong name can only reference other assemblies with strong namesa reference from an assembly with a strong name to one with a weak name is not allowed.

The STARTUP_LOADER_OPTIMIZATION flags specify domain-neutral loading behavior for all application domains in the process. To specify different behavior per application domain, you must use the loader optimization API.

The Loader Optimization API

The loader optimization API is a managed API that enables you to set the same values for domain-neutral loading that CorBindToRuntimeEx does. The one difference is that the loader optimization API enables you to specify different values per application domain. The loader optimization API consists of the following components:

  • The System.LoaderOptimization enumeration

  • The System.LoaderOptimizationAttribute custom attribute

  • The LoaderOptimization property on System.AppDomainSetup

The LoaderOptimization enumeration defines the same values for managed code that the STARTUP_FLAGS enumeration does for unmanaged code, with the addition of a new value, NotSpecified:

public enum LoaderOptimization { NotSpecified = 0, SingleDomain = 1, MultiDomain = 2, MultiDomainHost = 3 }

NotSpecified works the same as if you didn't specify a value at all. Specifying NotSpecified at the process level defaults to SingleDomain. If specified at the application domain level, NotSpecified indicates that the process-wide settings should be used.

In terms of domain-neutral loading, the LoaderOptimizationAttribute is the managed equivalent to CorBindToRuntimeEx in that it enables you to specify the domain-neutral loading behavior for all application domains in the process. To specify the domain-neutral settings using the LoaderOptimizationAttribute, place the attribute on your application's main method and pass a value from the LoaderOptimization enumeration to its constructor as shown in the following example:

[LoaderOptimization(LoaderOptimization.MultiDomainHost)] static void Main(string[] args) { }

If you specify the LoaderOptimizationAttribute on a method other than main, the CLR simply ignores it.

Domain-neutral settings can also be specified on a perapplication domain basis using the AppDomainSetup object passed to System.CreateDomain. AppDomainSetup has a property called LoaderOptimization that accepts a value from the System.LoaderOptimization enumeration. Note that it is possible to specify domain-neutral settings both at the process level (using the LoaderOptimizationAttribute or CorBindToRuntimeEx) and at the application domain level (using the LoaderOptimization property of AppDomainSetup). When domain-neutral settings are specified in both places, the value that is specific to the application domain takes precedence (unless the per-domain setting is NotSpecified, as described earlier). In other words, the process-wide settings are used only in those application domains for which a specific setting wasn't provided.

Domain-Neutral Assemblies and the CLR Hosting Interfaces

In practice, the domain-neutral settings offered by CorBindToRuntimeEx and the loader optimization API were not granular enough for most extensible applications. In general, the most attractive option for extensible applications was to cause all assemblies with strong names to be loaded domain neutral. This worked great for the .NET Framework assembly and the application's host runtime, but was suboptimal if an add-in with a strong name was loaded into the process. Such an add-in would be loaded as domain neutral, and therefore couldn't be unloaded and was subject to the other restrictions described earlier in the chapter.

The CLR hosting interfaces in .NET Framework 2.0 solve this problem by letting CLR hosts supply an exact list of the assemblies to be loaded domain neutral. This list is supplied by implementing the GetDomainNeutralAssemblies method of IHostControl. GetDomainNeutralAssemblies returns the list of assemblies to load domain neutral in the form of a pointer to an ICLRAssemblyReferenceList interface, as shown in the following definition from mscoree.idl:

interface IHostControl : IUnknown { // Other methods omitted... HRESULT GetDomainNeutralAssemblies( [out] ICLRAssemblyReferenceList **ppReferenceList); }

Recall from Chapter 8 that ICLRReferenceAssemblyList pointers are obtained by passing an array of strings representing the assemblies in your list to the GetCLRAssemblyReferenceList method of ICLRAssemblyIdentityManager.

Although GetDomainNeutralAssemblies gives you the flexibility to load specific assemblies domain neutral, you must take on the burden of making sure the list you supply forms a complete closure. The only way to ensure you're supplying a closed set is to use ildasm.exe or another tool to analyze each assembly's dependencies statically. If the set of assemblies you supply doesn't form a closure, you'll see FileLoadExceptions at run time as discussed earlier.

The following sample provides an implementation of GetDomainNeutralAssemblies that returns a specific set of assemblies to load domain neutral. In this sample, I've taken the approach that most extensible applications use. Specifically, I've indicated that all of the .NET Framework assemblies and the assembly containing the host's application domain manager (BoatRaceHostRuntime in this case) should be loaded domain neutral. This achieves the goal of sharing the jit-compiled code and CLR runtime data structures for the assemblies I expect to load into every application domain, while still allowing me to load strong-named add-ins into the process and unload them later.

const wchar_t *wszDomainNeutralAssemblies[] = { L"BoatRaceHostRuntime, PublicKeyToken=38c3b24e4a6ee45e", L"mscorlib, PublicKeyToken=b77a5c561934e089", L"System, PublicKeyToken=b77a5c561934e089", L"System.Xml, PublicKeyToken=b77a5c561934e089", L"System.Data, PublicKeyToken=b77a5c561934e089", L"System.Data.OracleClient, PublicKeyToken=b77a5c561934e089", L"System.Runtime.Remoting, PublicKeyToken=b77a5c561934e089", L"System.Windows.Forms, PublicKeyToken=b77a5c561934e089", L"System.Web, PublicKeyToken=b03f5f7f11d50a3a", L"System.Drawing, PublicKeyToken=b03f5f7f11d50a3a", L"System.Design, PublicKeyToken=b03f5f7f11d50a3a", L"System.Runtime.Serialization.Formatters.Soap, PublicKeyToken=b03f5f7f11d50a3a", L"System.Drawing.Design, PublicKeyToken=b03f5f7f11d50a3a", L"System.EnterpriseServices, PublicKeyToken=b03f5f7f11d50a3a", L"System.DirectoryServices, PublicKeyToken=b03f5f7f11d50a3a", L"System.Management, PublicKeyToken=b03f5f7f11d50a3a", L"System.Messaging, PublicKeyToken=b03f5f7f11d50a3a", L"System.Security, PublicKeyToken=b03f5f7f11d50a3a", L"System.ServiceProcess, PublicKeyToken=b03f5f7f11d50a3a", L"System.Web.Mobile, PublicKeyToken=b03f5f7f11d50a3a", L"System.Web.RegularExpressions, PublicKeyToken=b03f5f7f11d50a3a", L"System.Web.Services, PublicKeyToken=b03f5f7f11d50a3a", L"System.Configuration.Install, PublicKeyToken=b03f5f7f11d50a3a", L"Accessibility, PublicKeyToken=b03f5f7f11d50a3a", L"CustomMarshalers, PublicKeyToken=b03f5f7f11d50a3a", L"cscompmgd, PublicKeyToken=b03f5f7f11d50a3a", L"IEExecRemote, PublicKeyToken=b03f5f7f11d50a3a", L"IEHost, PublicKeyToken=b03f5f7f11d50a3a", L"IIEHost, PublicKeyToken=b03f5f7f11d50a3a", L"ISymWrapper, PublicKeyToken=b03f5f7f11d50a3a", L"Microsoft.JScript, PublicKeyToken=b03f5f7f11d50a3a", L"Microsoft.VisualBasic, PublicKeyToken=b03f5f7f11d50a3a", L"Microsoft.VisualBasic.Vsa, PublicKeyToken=b03f5f7f11d50a3a", L"Microsoft.VisualC, PublicKeyToken=b03f5f7f11d50a3a", L"Microsoft.Vsa, PublicKeyToken=b03f5f7f11d50a3a", L"Microsoft.Vsa.Vb.CodeDOMProcessor, PublicKeyToken=b03f5f7f11d50a3a", L"Microsoft_VsaVb, PublicKeyToken=b03f5f7f11d50a3a", L"mscorcfg, PublicKeyToken=b03f5f7f11d50a3a", L"vjswfchtml, PublicKeyToken=b03f5f7f11d50a3a", L"vjswfccw, PublicKeyToken=b03f5f7f11d50a3a", L"VJSWfcBrowserStubLib, PublicKeyToken=b03f5f7f11d50a3a", L"vjswfc, PublicKeyToken=b03f5f7f11d50a3a", L"vjslibcw, PublicKeyToken=b03f5f7f11d50a3a", L"vjslib, PublicKeyToken=b03f5f7f11d50a3a", L"vjscor, PublicKeyToken=b03f5f7f11d50a3a", L"VJSharpCodeProvider, PublicKeyToken=b03f5f7f11d50a3a", }; // ... HRESULT STDMETHODCALLTYPE CHostControl::GetDomainNeutralAssemblies( ICLRAssemblyReferenceList **ppReferenceList) { // Get a pointer to an ICLRAssemblyIdentityManager using a helper class // called CLRIdentityManager. This class was defined in Chapter 8. CCLRIdentityManager *pIdentityClass = new CCLRIdentityManager(); ICLRAssemblyIdentityManager *pIdentityInterface = pIdentityClass->GetCLRIdentityManager(); DWORD dwCount = sizeof(wszDomainNeutralAssemblies)/ sizeof(wszDomainNeutralAssemblies[0]); // Call GetCLRAssemblyReferenceList passing in the array of // strings identifying the assemblies you'd like to load domain neutral. HRESULT hr = pIdentityInterface-> GetCLRAssemblyReferenceList(wszDomainNeutralAssemblies, dwCount, ppReferenceList); pIdentityInterface->Release(); delete pIdentityClass; return S_OK; }

The assembly names you supply to indicate which assemblies to load domain neutral can be partial. That is, the PublicKeyToken, Version, and Culture elements are all optional, just as they are when you use one of the assembly loading APIs to load an assembly given a partial name. (See Chapter 7 for details on how to use the loading APIs.) If you omit a value for one of the optional elements, the CLR treats that element as a wildcard when determining whether an assembly should be loaded domain neutral. For example, the preceding code provides a value for PublicKeyToken but not for Version or Culture. In this case, the CLR will load domain neutral all versions and all cultures of the assembly with the given name and PublicKeyToken.

Most CLR hosts that specify an exact list of assemblies using GetDomainNeutralAssemblies do not also supply domain-neutral settings using CorBindToRuntimeEx or the loader optimization APIs. It is possible to do so, however. If you do specify domain-neutral settings using more than one of the techniques I've described in this chapter, the results are combined. The following list describes which assemblies are loaded domain neutral when you specify both a specific list and one of the values for either STARTUP_FLAGS or the LoaderOptimization enumeration:

  • If the host passes STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN (load no assemblies domain neutral), only the assemblies returned from GetDomainNeutralAssemblies would be loaded domain neutral.

  • If the host passes STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN (load all assemblies domain neutral), all assemblies would be loaded domain neutral, regardless of what is returned from GetDomainNeutralAssemblies.

  • If the host passes STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST (load only strong-named assemblies domain neutral), all strong-named assemblies plus those returned from GetDomainNeutralAssemblies would be loaded domain neutral.

In this section, I've described both managed and unmanaged APIs for specifying which assemblies to load domain neutral. As discussed earlier, however, the features available to you through the various APIs are not the same. Specifically,

  • The managed APIs enable you to specify domain-neutral settings per application domain, whereas the unmanaged APIs do not.

  • The unmanaged APIs enable you to provide a specific list of assemblies to load domain neutral, whereas the managed APIs do not.

In practice, I think you'll find that returning a specific list of assemblies using GetDomainNeutralAssemblies more closely matches most application requirements than the coarse settings offered by either CorBindToRuntimeEx or the loader optimization APIs. In particular, GetDomainNeutralAssemblies enables you to implement the behavior most requested by extensible applications: the ability to load the .NET Framework assemblies and the host runtime assembly domain neutral while not loading any add-ins domain neutral regardless of whether they have a strong name.

    Категории