Dynamic Linking

When you ran the C# compiler in Chapter 1, you created an assembly named Hello.exe. Hello.exe is as simple as an assembly can be: it contains but one file and it lacks a strong name, meaning that the common language runtime performs no version checking when loading it.

Weakly named, single-file assemblies are fine for the majority of applications. But occasionally developers need more. For example, you might want to write a library of routines that other applications can link to, similar to a dynamic link library (DLL) in Windows. If you do, you’ll need to know more about assemblies. Perhaps you’d like to write a library for the private use of your application. Or maybe you’ve heard that Microsoft .NET solves the infamous DLL Hell problem and you’d like to know how. The next several sections walk you, tutorial-style, through the process of creating, deploying, and dynamically linking to a multifile assembly. During the journey, you’ll see firsthand how such assemblies are produced and what they mean to the design and operation of managed applications. And just to prove that the framework is language-agnostic, you’ll write half of the assembly in C# and half in Visual Basic .NET.

Creating a Multifile Assembly

The assembly that you’re about to create contains two classes: one named SimpleMath, written in Visual Basic .NET, and another named ComplexMath, written in C#. SimpleMath has two methods: Add and Subtract. ComplexMath has one method, Square, which takes an input value and returns the square of that value.

Physically, the assembly consists of three files: Simple.netmodule, which holds the SimpleMath class; Complex.netmodule, which holds ComplexMath; and Math.dll, which houses the assembly manifest. Because the managed modules containing SimpleMath and ComplexMath belong to the same assembly, clients neither know nor care about the assembly’s physical makeup. They simply see one entity—the assembly—that contains the types they’re interested in.

Here’s how to create the assembly:

  1. Create a new text file named Complex.cs and enter the source code shown in Figure 2-1.

  2. Compile Complex.cs into a managed module with the command

    csc?/target:module?complex.cs

    The /target switch tells the C# compiler to generate a managed module that is neither an EXE nor a DLL. Such a module can’t be used by itself, but it can be used if it’s added to an assembly. Because you didn’t specify a file name with a /out switch, the compiler names the output file Complex.netmodule.

  3. In the same directory, create a new text file named Simple.vb. Type in the source code shown in Figure 2-2.

  4. Compile Simple.vb with the following command:

    vbc?/target:module?simple.vb

    This command produces a managed module named Simple.netmodule, which makes up the Visual Basic .NET half of the assembly’s code.

  5. Create an assembly that binds the two managed modules together by running the SDK’s AL (Assembly Linker) utility as follows:

    al?/target:library?/out:Math.dll?simple.netmodule
    ????????????complex.netmodule

    The resulting file—Math.dll—contains the assembly manifest. Inside the manifest is information identifying Simple.netmodule and Complex.netmodule as members of the assembly. Also encoded in the assembly manifest is the assembly’s name: Math.

    Complex.cs
    using?System;
    
    public?class?ComplexMath
    {
    ????public?int?Square?(int?a)
    ????{
    ????????return?a?*?a;
    ????}
    }
    Figure 2-1
    The ComplexMath class.
    Simple.vb
    Imports?System
    
    Public?Class?SimpleMath
    ????Function?Add?(a?As?Integer,?b?As?Integer)?As?Integer
    ????????Return?a?+?b
    ????End?Function
    
    ????Function?Subtract?(a?As?Integer,?b?As?Integer)?As?Integer
    ????????Return?a?-?b
    ????End?Function
    End?Class
    Figure 2-2
    The SimpleMath class.

You just created the .NET Framework equivalent of a DLL. Now let’s write a client to test it with.

Dynamically Linking to an Assembly

Follow these simple steps to create a console client for the Math assembly:

  1. In the same directory that Math.dll, Simple.netmodule, and Complex.netmodule reside in, create a new text file named MathDemo.cs. Then enter the code shown in Figure 2-3.

  2. Compile MathDemo.cs with the following command:

    csc?/target:exe?/reference:math.dll?mathdemo.cs

    The compiler creates an EXE named MathDemo.exe. The /reference switch tells the compiler that MathDemo.cs uses types defined in the assembly whose manifest is stored in Math.dll. Without this switch, the compiler would complain that the types are undefined.

Notice that in step 2, you did not have to include a /reference switch pointing to Simple.netmodule or Complex.netmodule, even though that’s where SimpleMath and ComplexMath are defined. Why? Because both modules are part of the assembly whose manifest is found in Math.dll.

MathDemo.cs
using?System;

class?MyApp
{
????static?void?Main?()
????{
????????SimpleMath?simple?=?new?SimpleMath?();
????????int?sum?=?simple.Add?(2,?2);
????????Console.WriteLine?("2?+?2?=?{0}",?sum);

????????ComplexMath?complex?=?new?ComplexMath?();
????????int?square?=?complex.Square?(3);
????????Console.WriteLine?("3?squared?=?{0}",?square);
????}
}
Figure 2-3
Client for the Math assembly.

Now that you have a client ready, it’s time to test CLR-style dynamic linking. Here’s a script to serve as a guide:

  1. In a command prompt window, run MathDemo.exe. You should see the output shown in Figure 2-4, which proves that MathDemo.exe successfully loaded and used SimpleMath and ComplexMath.

  2. Temporarily rename Complex.netmodule to something like Complex.foo.

  3. Run MathDemo again. A dialog box appears informing you that a FileNotFoundException occurred. The exception was generated by the CLR when it was unable to find the module containing ComplexMath. Click the No button to acknowledge the error and dismiss the dialog box.

  4. Restore Complex.netmodule’s original name and run MathDemo again to verify that it works.

  5. Modify MathDemo.cs by commenting out the final three statements—the ones that use ComplexMath. Then rebuild MathDemo.exe by repeating the command you used to build it the first time.

  6. Run MathDemo. This time, the only output you should see is “2 + 2 = 4.”

  7. Temporarily rename Complex.netmodule again. Then run MathDemo.exe. No exception occurs this time because MathDemo.exe doesn’t attempt to instantiate ComplexMath. The CLR doesn’t load modules that it doesn’t need to. Had this code been deployed on the Internet, the CLR wouldn’t have attempted to download Complex.netmodule, either.

  8. Restore Complex.netmodule’s name, uncomment the statements that you commented out in step 5, and rebuild MathDemo.exe one more time.

    Figure 2-4
    MathDemo output.

You’ve now seen firsthand how dynamic linking works in the .NET Framework and demonstrated that the CLR loads only the parts of an assembly that it has to. But what if you wanted to install the assembly in a subdirectory of the application directory? Here’s how to deploy the assembly in a subdirectory named bin:

  1. Create a bin subdirectory in the application directory (the directory where MathDemo.exe is stored).

  2. Move Math.dll, Simple.netmodule, and Complex.netmodule to the bin directory. Run MathDemo.exe again. The CLR throws a FileNotFoundException because it can’t find the assembly in the application directory.

  3. Create a new text file named MathDemo.exe.config in the application directory, and then enter the statements shown in Figure 2-5. MathDemo.exe.config is an XML application configuration file containing configuration data used by the CLR. The probing element tells the CLR to look in the bin subdirectory for assemblies containing types referenced by MathDemo.exe. You can include multiple subdirectory names by separating them with semicolons.

  4. Run MathDemo again and verify that it works even though the assembly is now stored in the bin directory.

These exercises demonstrate how assemblies containing types used by other applications are typically deployed. Most assemblies are private to a particular application, so they’re deployed in the same directory as the application that they serve or in a subdirectory. This model is consistent with the .NET Framework’s goal of “XCOPY installs,” which is synonymous with simplified install and uninstall procedures. Because MathDemo.exe doesn’t rely on any resources outside its own directory tree, removing it from the system is as simple as deleting the application directory and its contents.

MathDemo.exe.config
<configuration>
??<runtime>
????<assemblyBinding?xmlns="urn:schemas-microsoft-com:asm.v1">
??????<probing?privatePath="bin" />
????</assemblyBinding>
??</runtime>
</configuration>
Figure 2-5
MathDemo.exe’s application configuration file.
Versioning an Assembly

If you were to modify Simple.vb or Complex.cs right now and inadvertently introduce an error, the CLR would be happy to load the buggy assembly the next time you run MathDemo.exe. Why? Because the assembly lacks a strong name. The CLR’s versioning mechanism doesn’t work with weakly named assemblies. If you want to take advantage of CLR versioning, you must assign the assembly a strong name. Strong naming is the key to avoiding DLL Hell.

Use the following procedure to create a strongly named assembly containing the SimpleMath and ComplexMath classes:

  1. Go to the bin subdirectory and run the SDK’s SN (Strong Name) utility. The following command generates a “key file” named Keyfile.snk containing public and private keys that can be used for strong naming:

    sn?/k?Keyfile.snk
  2. Use AL to create a strongly named assembly that uses the keys found in Keyfile.snk:

    al?/keyfile:keyfile.snk?/target:library/out:Math.dll?
    /version:1.0.0.0?simple.netmodule?complex.netmodule

    The /keyfile switch identifies the key file. The /version switch specifies the version number written to the assembly’s manifest. The four values in the version number, from left to right, are the major version number, the minor version number, the build number, and the revision number.

  3. Go to MathDemo.exe’s application directory and rebuild MathDemo.cs using the following command:

    csc?/target:exe?/reference:bin\math.dll?mathdemo.cs

    This time, MathDemo.exe is bound to the strongly named Math assembly. Moreover, the new build of MathDemo.exe contains metadata noting what version of the assembly it was compiled against.

  4. Verify that MathDemo.exe works as before by running it.

So far, so good. You’ve created a version of MathDemo.exe that is strongly bound to version 1.0.0.0 of a private assembly whose manifest is stored in Math.dll. Now use the following exercises to explore the ramifications:

  1. Execute the following command in the bin directory to increment the assembly’s version number from 1.0.0.0 to 1.1.0.0:

    al?/keyfile:keyfile.snk?/target:library/out:Math.dll?
    /version:1.1.0.0?simple.netmodule?complex.netmodule
  2. Run MathDemo.exe. Because MathDemo.exe was compiled against version 1.0.0.0 of the assembly, the CLR throws a FileLoadException.

  3. Restore the assembly’s version number to 1.0.0.0 with the following command:

    al?/keyfile:keyfile.snk?/target:library?/out:Math.dll?
    /version:1.0.0.0?simple.netmodule?complex.netmodule
  4. Open Complex.cs and change the statement

    return?a?*?a;

    to read

    return?a?+?a;

    Clearly this is a buggy implementation because the Square method now doubles a rather than squaring it. But the version number has been reset to 1.0.0.0—the one MathDemo.exe was compiled against. What will the CLR do when you rebuild Complex.netmodule and run MathDemo again?

  5. Rebuild Complex.netmodule with the command

    csc?/target:module?/out:bin\Complex.netmodule?complex.cs

    Run MathDemo.exe. Once again, the CLR throws an exception. Even though the version number is valid, the CLR knows that Complex.netmodule has changed because Math.dll’s manifest contains a cryptographic hash of each of the files in the assembly. When you modified Complex.netmodule, you modified the value it hashes to as well. Before loading Complex.netmodule, the CLR rehashed the file and compared the resulting hash to the hash stored in the assembly manifest. Upon seeing that the two hashes didn’t match, the CLR threw an exception.

Now suppose circumstances were reversed and that version 1.0.0.0 contained the buggy Square method. In that case, you’d want MathDemo.exe to use version 1.1.0.0. You have two options. The first is to recompile MathDemo.exe against version 1.1.0.0 of the assembly. The second is to use a binding redirect to tell the CLR to load version 1.1.0.0 of the assembly when MathDemo asks for version 1.0.0.0. A binding redirect is enacted by modifying MathDemo.exe.config as follows:

<configuration>
??<runtime>
????<assemblyBinding?xmlns="urn:schemas-microsoft-com:asm.v1">
??????<dependentAssembly>
????????<assemblyIdentity?name="Math"
??????????publicKeyToken="cd16a90001d313af" />
????????<bindingRedirect?oldVersion="1.0.0.0" newVersion="1.1.0.0" />
??????</dependentAssembly>
??????<probing?privatePath="bin" />
????</assemblyBinding>
??</runtime>
</configuration>

The new dependentAssembly element and its subelements instruct the CLR to resolve requests for Math version 1.0.0.0 by loading version 1.1.0.0 instead. The publicKeyToken attribute is a tokenized representation (specifically, a 64-bit hash) of the public key encoded in Math’s assembly manifest; it was obtained by running SN with a /T switch against Math.dll:

sn?/T?math.dll

Your assembly’s public key token will be different from mine, so if you try this out on your code, be sure to plug your assembly’s public key token into MathDemo.exe.config’s publicKeyToken attribute.

Now you can have your cake and eat it too. The CLR enacts a strong versioning policy to prevent incorrect versions of the assembly from being loaded, but if you want to load another version, a simple configuration change makes it possible.

Sharing an Assembly: The Global Assembly Cache

Suppose that you build the Math assembly with the intention of letting any application, not just MathDemo.exe, use it. If that’s your goal, you need to install the assembly where any application can find it. That location is the global assembly cache (GAC), which is a repository for shared assemblies. The FCL is several shared assemblies. Only strongly named assemblies can be installed in the GAC. When the CLR attempts to load an assembly, it looks in the GAC even before it looks in the local application directory.

The .NET Framework SDK includes a utility named GacUtil that makes it easy to install and uninstall shared assemblies. To demonstrate, do this:

  1. Create a directory named Shared somewhere on your hard disk. (There’s nothing magic about the directory name; call it something else if you like.) Move the files in MathDemo.exe’s bin directory to the Shared directory. Then delete the bin directory.

  2. Go to the Shared directory and install the Math assembly in the GAC by executing the following command:

    gacutil?/i?math.dll
  3. Run MathDemo.exe. It should run fine, even though the assembly that it relies on is no longer in a subdirectory of the application directory.

  4. Remove the assembly from the GAC by executing this command:

    gacutil?/u?math
  5. Run MathDemo.exe again. This time, the CLR throws an exception because it can’t find the Math assembly in the GAC or in a local directory.

That’s shared assemblies in a nutshell. They must be strongly named, and the act of installing them in the GAC makes them shared assemblies. The downside to deploying shared assemblies is that doing so violates the spirit of XCOPY installs. Installing a shared assembly on an end user’s machine requires version 2 or later of the Windows Installer or a third-party installation program that is GAC-aware because GacUtil comes with the .NET Framework SDK and is not likely to be present on a nondeveloper’s PC. Uninstalling a shared assembly requires removing it from the GAC; simply deleting files won’t do the trick.

Applying Strong Names Using Attributes

The SDK’s AL utility is one way to create strongly named assemblies, but it’s not the only way, nor is it the most convenient. An easier way to produce a strongly named assembly is to attribute your code. Here’s a modified version of Complex.cs that compiles to a strongly named single-file assembly:

using?System;
using System.Reflection
[assembly:AssemblyKeyFile?("Keyfile.snk")]
[assembly:AssemblyVersion?("1.0.0.0")]

public?class?ComplexMath
{
????public?int?Square?(int?a)
????{
????????return?a?*?a;
????}
}

And here’s how Simple.vb would look if it, too, were modified to build a strongly named assembly:

Imports?System
Imports System.Reflection
<Assembly:AssemblyKeyFile?("Keyfile.snk")>
<Assembly:AssemblyVersion?("1.0.0.0")>

Public?Class?SimpleMath
????Function?Add?(a?As?Integer,?b?As?Integer)?As?Integer
????????Return?a?+?b
????End?Function

????Function?Subtract?(a?As?Integer,?b?As?Integer)?As?Integer
????????Return?a?-?b
????End?Function
End?Class

AssemblyKeyFile and AssemblyVersion are attributes. Physically, they map to the AssemblyKeyFileAttribute and AssemblyVersionAttribute classes defined in the FCL’s System.Reflection namespace. Attributes are mechanisms for declaratively adding information to a module’s metadata. These particular attributes create a strongly named assembly by signing the assembly and specifying a version number.

Delayed Signing

Unless an assembly is strongly named, it can’t be installed in the GAC and its version number can’t be used to bind clients to a particular version of the assembly. Strongly naming an assembly is often referred to as “signing” the assembly because the crux of strong naming is adding a digital signature generated from the assembly manifest and the publisher’s private key. And therein lies a problem. In large corporations, private keys are often locked away in vaults or hardware devices where only a privileged few can access them. If you’re a rank-and-file programmer developing a strongly named assembly and you don’t have access to your company’s private key (which is exactly the situation that Microsoft developers find themselves in), how can you fully test the assembly if you can’t install it in the GAC or use its version number to do strong versioning?

The answer is delayed signing. Delayed signing embeds the publisher’s public key (which is available to everyone) in the assembly and reserves space for a digital signature to be added later. The presence of the public key allows the assembly to be installed in the GAC. It also enables clients to build into their metadata information denoting the specific version of the assembly that they were compiled against. The lack of a digital signature means the assembly is no longer tamperproof, but you can fix that by signing the assembly with the publisher’s private key before the assembly ships.

How does delayed signing work? If Public.snk holds the publisher’s public key, the following command creates and delay-signs a Math assembly (note the /delaysign switch):

al?/keyfile:public.snk?/delaysign?/target:library?/out:Math.dll
/version:1.1.0.0?simple.netmodule?complex.netmodule

You can also delay-sign using attributes:

[assembly:AssemblyKeyFile?("Public.snk")]
[assembly:AssemblyVersion?("1.0.0.0")]
[assembly:DelaySign?(true)]

In either event, the resultant assembly contains the publisher’s public key but lacks the signature generated with the help of the private key. To sign the assembly before releasing it, have someone who has access to the publisher’s private key do this:

sn?/R?Math.dll?keyfile.snk

Using this statement assumes that Keyfile.snk holds the publisher’s public and private keys.

One trap to watch for regarding delayed signing is that neither the /delaysign switch nor the DelaySign attribute in and of itself enables the assembly to be installed in the GAC or strongly versioned. To enable both, run the SN utility against the assembly with a /Vr switch to enable verification skipping:

sn?/Vr?Math.dll

After signing the assembly with the publisher’s private key, disable verification skipping by running SN with a /Vu switch:

sn?/Vu?Math.dll

Verification skipping enables an assembly to be loaded without verifying that it hasn’t been tampered with. After all, verification can’t be performed if the assembly lacks the digital signature used for verification. Verification skipping doesn’t have to be enabled every time the assembly is built. Enabling it once is sufficient to enable verification skipping until it is explicitly disabled again by running SN with a /Vu switch.