Home > Articles > Programming > Windows Programming

Exploring the CLR

  • Print
  • + Share This
This chapter is from the book

How the Runtime Locates Assemblies

Now that I have discussed the contents of an assembly (metadata and MSIL), I can turn my attention to how the CLR locates assemblies prior to running them. One of the key differences between the .NET Framework and COM is the notion of side-by-side execution. In a nutshell, this is the idea that two completely different versions of a component may be resident and even executing at the same time on a single machine. With COM, each COM class (CLSID) on a machine has entries in the registry under the HKEY_CLASSES_ROOT key that identifies the complete path to the DLL or executable that implements the type. There can only be one path for any given CLSID, so there can only be one implementation of a particular type on a given machine. All clients that use the type are limited to using this one implementation of the type, even if those clients were actually built with a different version of the type than is currently resident on this machine.

Things are completely different with the .NET Framework. When you compile a piece of software that uses a type that resides in an external assembly (I'll call this software "the client" not because it has to be UI code, but because it is a consumer of an external assembly), your managed code compiler will insert TypeRef entries into the metadata for the client that identify the assembly that contains that type. For instance, if you look at the client application that you built earlier to test the Employee and Manager classes from our multifile assembly using the advanced parameter on ILDASM (I won't repeat the steps to do this because I have done it several times before), you can find the TypeRef for the Employee class by scrolling down and looking for the TypeRefName called AssemblyDemo.Employee as follows:

TypeRef #6 (01000006)
-------------------------------------------------------
Token:       0x01000006
ResolutionScope:  0x23000002
TypeRefName:    AssemblyDemo.Employee

The ResolutionScope (0x23000002) identifies the assembly that this type can be found in. This resolution scope corresponds to an AssemblyRef. If you scroll down further in the ILDASM window, you should be able to find the AssemblyRef with a token=0x23000002 as follows:

AssemblyRef #2
-------------------------------------------------------
  Token: 0x23000002
  Public Key or Token: ef 41 b5 08 ea 1c fb 8b 
  Name: multifile
  Major Version: 0x00000001
  Minor Version: 0x00000002
  Build Number: 0x00000003
  Revision Number: 0x00000004
Locale: <null>
  HashValue Blob:

Notice that this AssemblyRef identifies the name of the assembly (multifile); the public key token for the assembly (which is just a statistically unique hashed representation of the public key); the major, minor, build, and revision numbers for the assembly, which are 1, 2, 3, and 4, respectively; and the locale for the assembly. However, the AssemblyRef does not identify the path to the assembly. When the client application first uses a type that resides in the assembly, the CLR will use the Assembly Resolver to find the assembly identified by the AssemblyRef, that is, version 1.2.3.4 of the assembly called multifile with a public key token of ef41b508ea1cfb8b.

The Assembly Resolver

The Assembly Resolver is an internal component within the CLR that is responsible for taking assembly names and finding the requested assembly. It does not load the assembly; there is another module within the CLR called the Assembly Loader that does that. The Assembly Resolver's job is simply to map an assembly name (which includes the version number and [optionally] public key information) to a file on disk that contains the requested version of the assembly. The Assembly Resolver is exposed to developers through the Load method in the System.Reflection.Assembly class.

Every assembly has a four-part name. This four-part name consists of the friendly name for the assembly, the version number (which is the the concatenation of the major, minor, build, and revision numbers), the locale, and the public key token. Here is the full name for the multifile assembly:

Multifile,Version=1.2.3.4,Culture=Neutral,PublicKeyToken=8a707be49fd7d8f4

The Assembly Resolver can use either the full name for an assembly, or it can use a partial name, which will contain the assembly name but may be missing any of the other three elements of the full name.

The Algorithm for Locating an Assembly

To find an assembly, the Assembly Resolver takes the name and executes the algorithm shown in Figure 3–9.

Figure 09Figure 3–9 The assembly binding algorithm.


There are five main steps to this algorithm, and I have outlined those steps in bold on the flowchart.

  1. Apply versioning policy.

  2. Check if the desired version is loaded already.

  3. Check the GAC.

  4. Look in the codebase specified in the configuration file.

  5. Probe.

Let's look at each of these steps.

Versioning Policy

With the .NET Framework, you can use multiple versions of the same assembly on a single machine. By default, the Assembly Resolver will attempt to use the same version of an assembly that the client application was built with. However administrators or users of a machine can redirect a particular client application--or all client applications on the machine--to use a different version of an assembly than the one they were built with. This is called version redirection

An administrator or user may choose to use version redirection if they know that a newer version of a component has substantial improvements that client applications can use, and they know that the new version of the assembly will not break any of these client applications. The beauty of the .NET approach is that users and administrators have choice. They can choose to use a newer version of an assembly or not and if they attempt to use a newer version and run into problems, they can quickly roll back to the previous version by changing one entry in a configuration file. Version redirection can be done at three levels:

  1. Machine—All applications on a machine that use a version of an assembly are redirected to use a different version.

  2. Application—A particular application is redirected to a different version of a dependent assembly.

  3. Publisher policies—The vender of a software component publishes a special, shared assembly that contains only a configuration file that instructs all clients who use a particular version of an assembly to use a different version. You can turn off support for publisher policies if you want in your application-level configuration file.

Version policies are specified using configuration files. These configuration files are XML documents that obey the schema shown in Figure 3-10.

Figure 10Figure 3–10 The XML schema for .NET configuration files.


The mechanisms that you use to perform version redirection are the same; the only difference between doing version redirection at the machine, application or publisher policy level is which configuration file you use to specify the redirection. Machine, Application, and Publisher Policy configuration files all use the same XML schema, but not all of the elements can be used in each type of file. I used line thickness on elements in Figure 3–10 to show which elements can be applied where. The thinnest lines, probing for instance, indicate that the element can only be used in an Application configuration file. The medium thickness lines, System.Runtime.Remoting for instance, indicates that the element can be used on either Application or Machine configuration files. The thickest lines, Runtime for instance, indicate that the element can be used on Application, Machine, or Publisher Policy configuration files.

Assembly binding is controlled by the assemblyBinding element (and its subelements) in the configuration file. This element can be used in an Application, Machine, or Publisher Policy configuration file. Versioning policy is controlled by the bindingRedirect subelement and the publisherPolicy element. The bindingRedirect element appears beneath the dependentAssembly element, and it allows you to redirect a client that is trying to bind to one version of a dependent assembly to a different version. The publisherPolicy element, which appears both directly beneath the assemblyBinding element and also as a subelement of the dependentAssembly element, allows you to turn on or off support for publisherPolicies. You can do this either for all dependent assemblies used by the current application, in which case you would use the publisherPolicies element directly beneath the assemblyBinding key, or you can turn on or off support for publisherPolicies just for a particular assembly, in which you would use the publisherPolicies element directly beneath the dependentAssembly element. You can probably understand how these elements work by looking at some examples using the multifile assembly that you built earlier.

<configuration xmlns:asm="urn:schemas-microsoft-com:asm.v1">
<runtime>
<asm:assemblyBinding>
<asm:dependentAssembly>
<asm:assemblyIdentity name="multifile" 
   publicKeyToken="8a707be49fd7d8f4" />
<asm:bindingRedirect oldVersion="1.2.3.4" 
   newVersion="2.0.0.0" />
<asm:codeBase version="2.0.0.0" 
href="file://C:/Alan/books/dotnetbook/demos/chapter3/
    multifileassembly/multifileassembly_2_0/multifile.dll" />
</asm:dependentAssembly>
</asm:assemblyBinding>
</runtime>
</configuration>

The configuration file shown previously will redirect a client that was using version 1.2.3.4 of the assembly to use version 2.0.0.0. Notice that in addition to using the "asm:bindingRedirect" key to redirect the assembly reference to version 2.0.0.0 of the assembly, I also use the "asm:codeBase" key to specify where version 2.0.0.0 of the assembly can be found. Of course, to make this redirection work, you will need to save this document to the same directory as the client executable using the filename of the client with .config appended to it. For example, if the name of the application is myapp.exe, then you should name the configuration file myapp.exe.config.

If you want to redirect all clients on a machine from version 1.2.3.4 to version 2.0.0.0 of an assembly, you will need to add the entries shown above to your machine configuration file. You can find the machine configuration file in the config directory of your .NET Framework installation (for example, D:\WINNT\Microsoft.NET\Framework\v1.0.3705\CONFIG).

Another way to redirect references from one version of an assembly to another is with a Publisher Policy file. Publisher Policy files are used by component vendors to state which versions of their components are backward compatible with each other. A Publisher Policy file is just a configuration file that is embedded in a digitally signed, shared assembly and then installed into the GAC. The naming convention for this assembly is as follows:

policy.major.minor.assemblyname.dll

"Major" and "minor" are the major and minor version number of the assembly that the Publisher Policy should be applied to. A Publisher Policy file that would redirect version 1.2.3.4 to version 1.3.0.0 of the multifile assembly would have the following file name:

policy.1.2.multifile.dll

It would also contain the following contents:

<configuration xmlns:asm="urn:schemas-microsoft-com:asm.v1">
<runtime>
<asm:assemblyBinding>
<asm:dependentAssembly>
<asm:assemblyIdentity name="multifile" 
publicKeyToken="8a707be49fd7d8f4" />
<asm:bindingRedirect oldVersion="1.2.3.4" 
newVersion="1.3.0.0" />
<asm:codeBase version="1.3.0.0" 
href="file://C:/Alan/books/dotnetbook/demos/chapter3/
multifileassembly/multifileassembly_1_3/multifile.dll" />
</asm:dependentAssembly>
</asm:assemblyBinding>
</runtime>
</configuration>

Notice that there is no difference in terms of the XML schema and content between this configuration file and the application and machine configuration files that I showed you before. The only difference between this file and the application and machine configuration files is the way that they are deployed. To deploy a Publisher Policy file, you must build an assembly with this configuration file embedded into it and then register this file in the GAC. To do this, I saved the configuration file into a file called multifileassembly.config. I then ran the following command to create the assembly:

al /link:multifileassembly.config /out:policy.1.2.multifile.dll 
/keyf:mykey.snk /v:1.3.0.0

I then installed this assembly into the GAC using the gacutil command-line tool in the .NET Framework SDK:

gacutil –I policy.1.2.multifile.dll

The fact that you can only specify the major and minor version numbers in the name of the Publisher Policy file means that you can have only one Publisher Policy for each major/minor number combination. Therefore, you cannot redirect versions 1.2.3.4 and 1.2.4.0 to different new versions.

Now that you have seen three different ways to redirect an assembly reference from one version of an assembly to another (Application Configuration file, Machine Configuration file, or Publisher Policy file), the obvious question is how do the three forms of version redirection interact with each other? In other words, if I have an Application Configuration file, a Machine Configuration file, and a Publisher Policy file all in place for an assembly, which version number will I eventually get?

Version redirects in Application Configuration, Publisher Policy, and Machine Configuration files are applied serially as shown in Figure 3–11.

Figure 11Figure 3–11 How versioning policies are applied.

The original assembly reference (version 1.2.3.4) is first run through any version redirects in the Application Configuration file. This may lead to a change in the requested version (perhaps to version 1.3.0.0). This updated version number is then run through the Publisher Policy file, which may again change the requested version number. Keep in mind that the search for the Publisher Policy file in the GAC will be based on the major and minor version numbers of the request after it has been run through the Application Configuration file. Therefore, in this case, the Assembly Resolver will look for a Publisher Policy file called policy.1.3.multifile.dll because the requested version number after it has been run through application configuration file is 1.3.0.0.

A machine configuration file is the last step in the versioning policy chain. After the requested version of the assembly is (potentially) altered by the Application Configuration file and a Publisher Policy, the resulting version number is then run through the Machine Configuration file, which may alter the version number one more time. The Machine Configuration file is usually controlled by a system administrator, and it allows the system administrator final control over which version of an assembly is used to satisfy a particular request. More important, the Machine Configuration file is applied for all binding requests to a particular assembly, regardless of which client application the request came from, unlike Application Configuration files that only affect the assembly binding for a particular application. The example shown in Figure 3–11 has an initial version number of 1.2.3.4 that is redirected through the application configuration file to version 1.3.0.0. A Publisher Policy for major.minor version number 1.3 will then redirect the request to version 2.0.0.0. Finally, the Machine Configuration file will redirect the request to version 2.1.0.0. Let's test your understanding of how the process works. Refer back to Figure 3–11. Let's say that the Publisher Policy file redirects version 1.3.0.0 to version 1.4.0.0 instead of 2.0.0.0. What version number will the client application bind to? The answer is 1.4.0.0. Why? Because after the Publisher Policy file is processed, the requested version number is 1.4.0.0. Therefore, the version redirect in the machine configuration file from version 2.0.0.0 to version 2.1.0.0 will not be processed.

You can turn off support for Publisher Policy files in the Application Configuration file using the publisherPolicy key. You can do this on a per-assembly basis using the publisherPolicy element as a subelement of the dependentAssembly element as follows:

<configuration xmlns:asm="urn:schemas-microsoft-com:asm.v1">
<runtime>
<asm:assemblyBinding>
<asm:dependentAssembly>
<asm:assemblyIdentity name="multifile" 
publicKeyToken="8a707be49fd7d8f4" />
<asm:publisherPolicy apply="no" />
</asm:dependentAssembly>
</asm:assemblyBinding>
</runtime>
</configuration>

You can also turn off Publisher Policies for all assemblies used by an application using the publisherPolicy element as a subelement of assemblyBinding as follows:

<configuration xmlns:asm="urn:schemas-microsoft-com:asm.v1">
<runtime>
<asm:assemblyBinding>
<asm:publisherPolicy apply="no" />
<asm:dependentAssembly>
<asm:assemblyIdentity name="multifile" 
publicKeyToken="8a707be49fd7d8f4" />
</asm:dependentAssembly>
</asm:assemblyBinding>
</runtime>
</configuration>

The ability to turn off Publisher Policies is a key improvement over COM provided by .NET. With COM, after you installed a new version of a component, all clients on that machine that use the component will automatically start using the new version of the component. This was a main cause of DLL hell and the brittleness of some COM-based systems. If the new version was not backward compatible with the version that the clients were built against, the system would break. The way that COM worked was essentially equivalent to having publisherPolicies files for a component that redirect every past version to the latest version. The advantage of .NET is that you have the option of overriding the component vendor's backward compatibility matrix as expressed in the Publisher Policy file and either forcing a client to use the version of the component that it was built with or explicitly specifying some other version using a version redirect.

Check if the Desired Version Is Loaded Already

The next step in the assembly binding process—checking to see if the desired version of the assembly is already loaded—should be fairly simple to understand. After the Assembly Resolver has determined the final version number of the requested assembly, it checks to see if the requested version of the assembly has already been loaded by the application. If it has, the binding process is successful, and the assembly-binding process stops. If the assembly has not been loaded, the binding process continues.

Check the GAC

The next step in the assembly-binding process is to check the GAC. The GAC stores assemblies that are shared by several applications. To be installed in the GAC, an assembly must have a strong name. You can install assemblies in the GAC by dragging and dropping an assembly into the WinNT\Assembly directory, using the gacutil command-line tool in the .NET Framework SDK, or using Windows Installer 2.0 or later. To install an assembly into the GAC using gacutil, run the following command:

gacutil –i assembly.dll

To remove an assembly from the GAC, execute the following command:

gacutil –u assembly.dll

In most cases, you will not want to install your assemblies into the GAC. Microsoft recommends that you use private assemblies that are installed in a subdirectory of their client application. Only this approach is absolutely guaranteed to never cause versioning problems. Still, there are good reasons for creating shared assemblies. The assemblies in the .NET Framework class library are all deployed as shared assemblies. These assemblies are fairly large, and they will be used by every .NET application on a particular machine. It would be impractical to install a version of these assemblies for each application, although there is nothing stopping you from doing this if you want to.

In most cases, you will not be creating shared assemblies, so, if the assembly is not found in the GAC, the Assembly Resolver next searches through a configurable set of directories for the assembly. This search is called probing.

Probing

The probing process starts at an application's AppBase. The AppBase is the directory that contains the configuration file for an application. For an executable, the AppBase is the directory where the executable was started. For a Web application, the AppBase is a little harder to pin down because each directory beneath the root of the virtual directory structure of the application can have a configuration file. Therefore, the AppBase may be different for each page, but, in general, the AppBase will be at the root of the virtual directory structure. Secure Web sites will likely put the pages that need to be protected in a separate directory, and the secure pages will likely have a different configuration file in that secure directory. Therefore, if you went to a nonsecure page, the AppBase will be the root of the virtual directory structure. If you went to a secure page, the AppBase would be the root of the secure part of the virtual directory structure.

The probing process first looks for the assembly in the AppBase itself. If it does not find it there, it looks for a directory beneath the AppBase that has the same name as the assembly. Therefore, with the multifile assembly, and a client that resides at c:\myclient\myclient.exe, the Assembly Resolver will look in the following directories:

C:\myclientC:\myclient\multifile\

Within these directories, the CLR will look first for a file with the name multifile.dll and then for a file with the name multifile.exe. Therefore, the Assembly Resolver will look for an assembly that can satisfy the bind request at the following paths in the order shown:

C:\myclient\multifile.dll
C:\myclient\multifile\multifile.dll
C:\myclient\multifile.exe
C:\myclient\multifile\multifile.exe

The Assembly Resolver is not smart enough to continue looking if it finds a file with the correct file name and strong name, but the incorrect version number. Therefore, if a client application that resides at c:\myclient\myclient.exe requests version 1.3.0.0 of an assembly, and version 1.2.3.4 resides in c:\myclient and version 1.3.0.0 resides in c:\myclient\multifile\, the Assembly Resolver will fail and throw version mismatch exception back to the client because it will find version 1.2.3.4 first. If you deleted version 1.2.3.4 from the c:\myclient directory, the bind would succeed because the Assembly Resolver will eventually find version 1.3.0.0 in c:\myclient\multifile\.

In addition to the default probing locations, you can instruct the Assembly Resolver to probe into additional subdirectories beneath the AppBase using the privatePath element in the configuration file as follows:

<configuration xmlns:asm="urn:schemas-microsoft-com:asm.v1">
<runtime>
<asm:assemblyBinding>
<asm:probing privatePath="bin;assemblies" />
<asm:dependentAssembly>
<asm:assemblyIdentity name="multifile" 
publicKeyToken="8a707be49fd7d8f4" />
<asm:bindingRedirect oldVersion="1.2.3.4" 
newVersion="1.3.0.0" />
</asm:dependentAssembly>
</asm:assemblyBinding>
</runtime>
</configuration>

Notice that this configuration file adds the bin and assemblies subdirectories to the search path, so now the list of directories searched is as follows:

C:\myclient\multifile.dll
C:\myclient\multifile\multifile.dll
C:\myclient\bin\multifile.dll
C:\myclient\bin\multifile\multifile.dll
C:\myclient\assemblies\multifile.dll
C:\myclient\assemblies\multifile\multifile.dll
C:\myclient\multifile.exe
C:\myclient\multifile\multifile.exe
C:\myclient\bin\multifile.exe
C:\myclient\bin\multifile\multifile.exe
C:\myclient\assemblies\multifile.exe
C:\myclient\assemblies\multifile\multifile.exe

Notice how the additional subdirectories that I added through the privatePath element (bin and assemblies in this case) are interpreted by the Assembly Resolver to be subdirectories beneath the AppBase (C:\myclient\ in this case).

The search paths that you have seen so far are the paths that the Assembly Resolver will use if the requested assembly has a neutral culture, which will be true of all code assemblies. If the Assembly Resolver is probing to find a culture-specific assembly (these are called satellite assemblies) that contains localized strings or other resources, the probing sequence is a little different. The Assembly Resolver will append the culture name to the search path.

NOTE

Culture names in .NET use the RFC 1766 standard, which uses the following format:

<2-letter language code> – < country/region code>
The lower-case language code is derived from International Organization 
for Standardization (ISO) 639-1 and the upper-case country/region codes 
are derived from ISO 3166. Some example culture names are as follows:
Culture Name    Country/Region
en-US       English – United States
en-GB       English – United Kingdom
fr-FR       French – France
fr-CA       French – Canada
es-ES       Spanish – Spain
es-MX       Spanish – Mexico
zh-CHS       Chinese (Simplified)
zh-CHT       Chinese (Traditional)
To see a complete list of culture names, see the documentation for the
System.Globalization.CultureInfo class. 

Therefore, if I am on a machine where the local culture setting is Spanish – Mexico, the Assembly Resolver will search the following directories for a satellite assembly that contains localized resources:

C:\myclient\es-MX\multifile.dll
C:\myclient\es-MX\multifile\multifile.dll
C:\myclient\bin\es-MX\multifile.dll
C:\myclient\bin\es-MX\multifile\multifile.dll
C:\myclient\assemblies\es-MX\multifile.dll
C:\myclient\assemblies\es-MX\multifile\multifile.dll
C:\myclient\es-MX\multifile.exe
C:\myclient\es-MX\multifile\multifile.exe
C:\myclient\bin\es-MX\multifile.exe
C:\myclient\bin\es-MX\multifile\multifile.exe
C:\myclient\assemblies\es-MX\multifile.exe
C:\myclient\assemblies\es-MX\multifile\multifile.exe

Assembly Binding in Development Mode

The assembly-binding process makes it complicated to develop shared assemblies. When you compile a new version of a shared assembly, you will need to remember to remove the old version from the GAC and install the new version into the GAC. This is obviously time consuming and error prone. To make development of shared assemblies easier, you can use the CLR's development mode. In this mode, the CLR will first look for assemblies in the directory specified by the DEVPATH environment variable. You can set this environment variable to point to the directory where your development tool writes your compiled assemblies. You must also tell the CLR to run in development mode. You do this by adding the developmentMode tag to your machine configuration file. Simply add the following element to your machine.config file:

<configuration>
// the rest of the configuration file is omitted.
<runtime>
   <developmentMode developerInstallation="true" />
  </runtime>
</configuration>

Because this setting is configured in machine.config, after it is set, it will apply to all applications currently running on that machine. Remember that this mode should only be used during development. The CLR does not do version checking when you are running in development mode.

Debugging the Assembly-Binding Process

Now that you understand the assembly-binding process, you are probably thinking that it must be difficult to debug. Fortunately, the .NET Framework SDK comes with a command-line tool called the Assembly Binding Log Viewer, which has the rather odd executable file name of fuslogvw.exe. You can find this tool in the "bin" directory of your .NET Framework SDK installation. On my machine it resides at:

D:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin 

If you run this tool, you will see the window shown in Figure 3–12.

Figure 12Figure 3–12 The Assembly Binding Log Viewer.

As long as the Log Failures checkbox is set, this tool will log all binding failures. Figure 3–13 shows the entry that you will see when a binding error occurs.

Figure 13Figure 3–13 The Assembly Binding Log Viewer.

If you either double-click the entry that appears in the Assembly Binding Log Viewer or select the entry and click the View Log button, you will see detailed binding error information similar to the following (assuming the bind failed, of course):

*** Assembly Binder Log Entry (1/16/2002 @ 12:33:01 AM) ***

The operation failed.
Bind result: hr = 0x80131040. No description available.

Assembly manager loaded from: 
D:\WINNT\Microsoft.NET\Framework\v1.0.3328\fusion.dll
Running under executable 
C:\Alan\books\dotnetbook\demos\chapter3TestVersioningPolicy\bin\Debug\TestVersioningPolicy.exe
--- A detailed error log follows. 

=== Pre-bind state information ===
LOG: DisplayName = multifile, Version=1.2.3.4, Culture=neutral, 
PublicKeyToken=8a707be49fd7d8f4
 (Fully-specified)
LOG: Appbase = 
C:\Alan\books\dotnetbook\demos\chapter3\TestVersioningPolicy\bin\DebugLOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = NULL
Calling assembly : TestVersioningPolicy, Version=1.0.737.3124, 
Culture=neutral, 
PublicKeyToken=null.
===
LOG: Processing DEVPATH.
LOG: DEVPATH is not set. Falling through to regular bind.
LOG: Private path hint found in configuration file: bin;assemblies.
LOG: Host configuration file not found.
LOG: Redirect found in application 
configuration file 1.2.3.4 -> 1.3.0.0.
LOG: Publisher policy file is not found.
LOG: Using machine configuration file from
D:\WINNT\Microsoft.NET\Framework\v1.0.3328\config\machine.config.
LOG: Post-policy reference: multifile, Version=1.3.0.0, 
Culture=neutral, PublicKeyToken=8a707be49fd7d8f4
LOG: Cache Lookup was unsuccessful.
LOG: Attempting download of new URL 
file:///C:/Alan/books/dotnetbook/demos/chapter3/TestVersioningPolic
y/bin/Debug/multifile.DLL.
LOG: Attempting download of new URL 
file:///C:/Alan/books/dotnetbook/demos/chapter3/TestVersioningPolic
y/bin/Debug/multifile/multifile.DLL.
LOG: Assembly download was successful. Attempting setup of file: 
C:\Alan\books\dotnetbook\demos\chapter3\TestVersioningPolicy\binDebug\multifile\multifile.DLL
LOG: Entering run-from-source setup phase.
WRN: Comparing the assembly name resulted in the 
     mismatch: Minor Version
ERR: The assembly reference did not match the 
     assembly definition found.
ERR: Failed to complete setup of assembly (hr = 0x80131040). 
     Probing terminated.

This particular binding log shows the error that you will see when the assembly resolver finds an assembly with the wrong version number. In this case, the application configuration file contained a redirect from version 1.2.3.4 to version 1.3.0.0, but the application's directory contained version 1.2.3.4 of the assembly. By default, the Assembly Binding Log Viewer will not display any information if the bind was successful. You can get the Assembly Binding Log Viewer to display information even for successful binds by adding the following key to the registry and then setting its value equal to 1:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Fusion\ForceLog

This will cause the Assembly Binding Log Viewer to display information for all assembly binds. The following output shows the result of a successful bind:

*** Assembly Binder Log Entry (1/16/2002 @ 12:28:18 AM) ***

The operation was successful.
Bind result: hr = 0x0. The operation completed successfully.

Assembly manager loaded from: 
D:\WINNT\Microsoft.NET\Framework\v1.0.3328\fusion.dll
Running under executable C:\Alan\books\dotnetbook\demos\chapter3TestVersioningPolicy\bin\Debug\TestVersioningPolicy.exe
--- A detailed error log follows. 

=== Pre-bind state information ===
LOG: DisplayName = multifile, Version=1.2.3.4, 
Culture=neutral, PublicKeyToken=8a707be49fd7d8f4
 (Fully-specified)
LOG: Appbase = C:\Alan\books\dotnetbook\demos\chapter3TestVersioningPolicy\bin\DebugLOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = NULL
Calling assembly : TestVersioningPolicy, 
Version=1.0.737.3124, Culture=neutral, PublicKeyToken=null.
===

LOG: Processing DEVPATH.
LOG: DEVPATH is not set. Falling through to regular bind.
LOG: Private path hint found in configuration file: bin;assemblies.
LOG: Host configuration file not found.
LOG: Redirect found in application configuration 
     file 1.2.3.4 -> 1.3.0.0.
LOG: Publisher policy file is not found.
LOG: Using machine configuration file from 
D:\WINNT\Microsoft.NET\Framework\v1.0.3328\config\machine.config.
LOG: Post-policy reference: multifile, Version=1.3.0.0, 
Culture=neutral, PublicKeyToken=8a707be49fd7d8f4
LOG: Cache Lookup was unsuccessful.
LOG: Attempting download of new URL 
file:///C:/Alan/books/dotnetbook/demos/chapter3/
TestVersioningPolicy/bin/Debug/multifile.DLL.
LOG: Attempting download of new URL 
file:///C:/Alan/books/dotnetbook/demos/chapter3/
TestVersioningPolicy/bin/Debug/multifile/multifile.DLL.
LOG: Assembly download was successful. Attempting setup of file: 
C:\Alan\books\dotnetbook\demos\chapter3TestVersioningPolicy\bin\Debug\multifile\multifile.DLL
LOG: Entering run-from-source setup phase.

You can interface with the assembly-binding process programmatically through the Load method in the System.Reflection.Assembly class. This method takes an assembly name. The assembly name can be a full name like the following:

multifile, Version=1.3.0.0, Culture=neutral, 
PublicKeyToken=8a707be49fd7d8f4

or it can be a partial name like the following:

multifile

With either name, it will load the requested assembly. The source code for this book includes an example program in the Chapter 3 directory called the AssemblyMetaDataViewer that shows you how to call the Load method in System.Reflection.Assembly. If you download and run the AssemblyMetaDataViewer application, you will see the window shown in Figure 3–14.

Figure 14Figure 3–14 The Assembly Metadata Viewer.

You can then type in either a full or partial name and click the Load button. Assuming the application is able to bind to the assembly, it will display the metadata for the assembly and the location where it loaded the assembly from. The code behind the Load button is as follows:

private void cmdLoad_Click(object sender, 
  System.EventArgs e)
{
  System.Reflection.Assembly anAssembly;
  try
  {
    anAssembly=Assembly.Load(txtAssemblyName.Text);
    lblLocation.Text=anAssembly.Location;
  // The LoadMetadata method populates the tree view
  // with the meta-data for the assembly.
    LoadMetadata(anAssembly);
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
}

The Assembly Metadata Viewer together with the Assembly Binding Log Viewer are the only tools that you need to understand the assembly-binding process. You can type in an assembly name in the Assembly Metadata Viewer and then see the location where the application found the assembly. You can play around with configuration files, publisher policies, GAC, and probing paths and see how the chosen assembly changes. This is how I understood the assembly-binding process. If the bind fails, you can then use the Assembly Binding Log Viewer to see why.

Loading the Code in an Assembly

The Assembly Resolver takes either a full or partial assembly name and turns it into a physical path where an assembly that can satisfy the binding request resides. The Assembly Loader is responsible for actually loading this assembly into memory. You can interface with the Assembly Loader using the LoadFrom method in the System.Reflection.Assembly class. The AssemblyMetaDataViewer example in this chapter also allows you to test the LoadFrom method. To test the method, enter a path into the File Name edit box (See Figure 3–14) or click the Browse button, choose a file, and then click the Load From File button. The code behind this button is shown here:

private void cmdLoadFromFile_Click(object sender,
  System.EventArgs e)
{
  try
  {
    mSelectedAssembly=
      Assembly.LoadFrom(txtFileName.Text);
    LoadMetadata(mSelectedAssembly);
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.Message);
  }
}

Executing the Code in an Assembly

After the assembly is loaded, the CLR's next job is to execute the code in the assembly. With native x86 code, the execution process was almost exclusively the job of the CPU. With managed code modules, things become a lot more complicated because the code in an assembly is MSIL code that will not execute on any CPU. The code must be compiled into x86 code before it can execute. One approach to this problem would be for the CLR to invoke a compiler, which would compile all the code into native x86 before it executed anything. There are a number of problems with this approach. The first problem is that, in most cases, you would compile a lot of code that was never used. Typically, when you use an application, you only use a small amount of the code in the application. Think about Microsoft Word for instance. Microsoft Word has several hundred commands and (probably) several million lines of code. In any given session, you will most likely use only a few dozen commands and a small fraction of the code in the application. Compiling all of the code in this application would be wasteful if you only use a small portion of the code at any time. A better solution is to compile only the code that you need when you need it. The execution process for .NET assemblies, which is called the VES, compiles the MSIL code in the assembly as it is needed. This process is called JIT compilation.

The process begins when you run a managed code executable. Each managed code executable will contain a small, unmanaged stub function that is the entry point for the executable. This stub function loads the execution engine of the CLR (MSCorEE.dll) and calls a method called _CorExeMain in the execution engine. This function will initialize the runtime and then call the managed code entry point of the executable. Remember that the managed code entry point will be a static member function called main in a class. Therefore, before the runtime can execute the managed code entry point, it must load the class that contains the main method.

NOTE

You can only have one main method in a managed code executable. If you try to compile an executable that has more than one static class method called main, you will receive an error that tells you that the executable has more than one entry point defined.

The class loader in the CLR will lay out the class in memory and insert a small stub function for each of the methods of the class. This stub method invokes the JIT compiler to compile the MSIL code for the method. After the compiler has compiled the method, the CLR will replace the stub method with the compiled native code. Therefore, the next time the CLR calls the method, there is no need to compile the method again. The CLR can simply start executing the native code.

As it runs, the main method will call other methods in the class or instantiate other classes. The CLR will load these classes and lay them out in memory with stub functions that compile the MSIL code for the method when they are called. The metadata in the executable will contain a reference to any assemblies that the executable was built with, and, as soon as you use a class that resides in an external assembly, the CLR will load that assembly and then load the class, and the process repeats. More and more code is compiled as you run the executable.

On resource-constrained platforms, the CLR will support code pitching. If the host machine does not have enough memory to hold all of the compiled code that it needs in memory, it can simply toss the compiled native code by replacing it with the stub function, which is more compact. The next time the CLR calls the method, the stub function will compile the MSIL back to native code again.

You're probably thinking that this JIT compilation process must be slow. It's actually much faster than you might think for a number of reasons. First, the JIT compiler is converting MSIL code (not source code) to native code. The MSIL code has already been compiled and checked for correct syntax by your language compiler. Second, MSIL code is structurally similar to native machine language, so the compilation process is extremely fast. Also, consider that you are only going to pay a penalty with JIT compilation on startup. On interactive applications, you will very quickly reach a steady state where all of the code that you are currently using has been compiled. Users will only notice a slight delay when they use a new feature for the first time in their session.

Of course, there are still going to be some times when startup is critical. For these situations, there is the Native Image Generator.

The Native Image Generator

The Native Image Generator (ngen.exe) is a command-line tool that is included with the .NET Framework SDK. It allows you to precompile .NET assemblies so that you will not incur the performance penalty of JIT –compilation. The Native Image Generator stores the compiled code in the native image cache. The CLR searches this cache for precompiled code before it compiles the MSIL code in an assembly. Even if you use the Native Image Generator, the original assembly with the MSIL still has to be present on disk. If the compiled code in the Native Image Cache differs from the requested assembly in any way, the CLR will revert back to JIT compiling the MSIL code in the assembly. Ngen will store a unique identifier called a MVID along with the code that it compiled in the native image cache. When the CLR fetches compiled code from the native image cache, it will check the MVID of the cached code against the MVID of the assembly that resides on disk. If they do not match, that means that the compiled code in the native image cache was not generated from the assembly that the CLR tried to bind to. In this case, the CLR will fall back to JIT compiling the code in the assembly. When you precompile an assembly using Ngen, you have the option of specifying whether the code that it creates has debugging or profiling information included. If the CLR is running in a debugging or profiling mode, it will fall back to using JIT compilation unless the code in the native image cache also has debugging and profiling information included. You can include debugging and/or profiling information in an assembly that is compiled with Ngen using the /debug, /debugopt, and /prof options

  • + Share This
  • 🔖 Save To Your Account