Section 17.5. Multimodule Assemblies


17.5. Multimodule Assemblies

Assemblies can consist of more than one module, though this isn't supported by Visual Studio 2005.

A single-module assembly has a single file that can be an EXE or DLL file. This single module contains all the types and implementations for the application. The assembly manifest is embedded within this module.

Each module has a manifest of its own that is separate from the assembly manifest. The module manifest lists the assemblies referenced by that particular module. In addition, if the module declares any types, these are listed in the manifest along with the code to implement the module. A module can also contain resources, such as the images needed by that module.

A multimodule assembly consists of multiple files (zero or one EXE and zero or more DLL files, though you must have at least one EXE or DLL). The assembly manifest in this case can reside in a standalone file, or it can be embedded in one of the modules. When the assembly is referenced, the runtime loads the file containing the manifest and then loads the required modules as needed.

17.5.1. Building a Multimodule Assembly

To demonstrate the use of multimodule assemblies, the following example creates a couple of very simple modules that you can then combine into a single assembly. The first module is a Fraction class. This simple class will allow you to create and manipulate common fractions. Example 17-1 illustrates.

Example 17-1. The Fraction class
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace ProgCS {    public class Fraction    {       private int numerator;       private int denominator;       public Fraction( int numerator, int denominator )       {          this.numerator = numerator;          this.denominator = denominator;       }       public Fraction Add( Fraction rhs )       {          if ( rhs.denominator != this.denominator )          {             return new Fraction(                 rhs.denominator * numerator +                  rhs.numerator * denominator,                  denominator * rhs.denominator);          }          return new Fraction(              this.numerator + rhs.numerator,                   this.denominator );       }       public override string ToString( )       {          return numerator + "/" + denominator;       }    } }

Notice that the Fraction class is in the ProgCS namespace. The full name for the class is ProgCS.Fraction.

The Fraction class takes two values in its constructor: a numerator and a denominator. There is also an Add( ) method, which takes a second Fraction and returns the sum, assuming the two share a common denominator. This class is simplistic, but it will demonstrate the functionality necessary for this example.

The second class is the MyCalc class, which stands in for a robust calculator. Example 17-2 illustrates.

Example 17-2. The calculator
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace ProgCS {    public class MyCalc    {       public int Add( int val1, int val2 )       {          return val1 + val2;       }       public int Mult( int val1, int val2 )       {          return val1 * val2;       }    } }

Once again, MyCalc is a very stripped-down class to keep things simple. Notice that MyCalc is also in the ProgCS namespace.

This is sufficient to create an assembly. Use an AssemblyInfo.cs file to add some metadata to the assembly. The use of metadata is covered in Chapter 18.

You can write your own AssemblyInfo.cs file, but the simplest approach is to let Visual Studio generate one for you automatically.


Visual Studio creates only single-module assemblies.

You can create a multimodule resource with the /addModules command-line option. The easiest way to compile and build a multimodule assembly is with a makefile, which you can create with Notepad or any text editor.

If you are unfamiliar with makefiles, don't worry; this is the only example that needs a makefile, and that is just to get around the current limitation of Visual Studio creating only single-module assemblies. If necessary, you can just use the makefile as offered without fully understanding every line. For more information, see Managing Projects with make (O'Reilly).


Example 17-3 shows the complete makefile (which is explained in detail immediately afterward). To run this example, put the makefile (with the name makefile) in a directory together with a copy of Calc.cs, Fraction.cs, and AssemblyInfo.cs. Start up a .NET command window and cd to that directory. Invoke nmake without any command switch. You will find the SharedAssembly.dll in the \bin subdirectory.

Example 17-3. The complete makefile for a multimodule assembly
ASSEMBLY= MySharedAssembly.dll BIN=.\bin SRC=. DEST=.\bin CSC=csc /nologo /debug+ /d:DEBUG /d:TRACE MODULETARGET=/t:module LIBTARGET=/t:library EXETARGET=/t:exe REFERENCES=System.dll MODULES=$(DEST)\Fraction.dll $(DEST)\Calc.dll METADATA=$(SRC)\AssemblyInfo.cs all: $(DEST)\MySharedAssembly.dll # Assembly metadata placed in same module as manifest $(DEST)\$(ASSEMBLY): $(METADATA) $(MODULES) $(DEST)     $(CSC) $(LIBTARGET) /addmodule:$(MODULES: =;) /out:$@ %s # Add Calc.dll module to this dependency list $(DEST)\Calc.dll: Calc.cs $(DEST)     $(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s # Add Fraction $(DEST)\Fraction.dll: Fraction.cs $(DEST)     $(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s $(DEST):: !if !EXISTS($(DEST))         mkdir $(DEST) !endif

The makefile begins by defining the assembly you want to build:

ASSEMBLY= MySharedAssembly.dll

It then defines the directories you'll use, putting the output in a bin directory beneath the current directory and retrieving the source code from the current directory:

SRC=. DEST=.\bin

Build the assembly as follows:

$(DEST)\$(ASSEMBLY): $(METADATA) $(MODULES) $(DEST)     $(CSC) $(LIBTARGET) /addmodule:$(MODULES: =;) /out:$@ %s

This places the assembly (MySharedAssembly.dll) in the destination directory (bin). It tells nmake (the program that executes the makefile) that the $(DEST)\$(ASSEMBLY) build target depends upon the three other build targets listed, and it provides the command line required to build the assembly.

The metadata is defined earlier as:

METADATA=$(SRC)\AssemblyInfo.cs

The modules are defined as the two DLLs:

MODULES=$(DEST)\Fraction.dll $(DEST)\Calc.dll

The compile line builds the library and adds the modules, putting the output into the assembly file MySharedAssembly.dll:

$(DEST)\$(ASSEMBLY): $(METADATA) $(MODULES) $(DEST)     $(CSC) $(LIBTARGET) /addmodule:$(MODULES: =;) /out:$@ %s

To accomplish this, nmake needs to know how to make the modules. Start by telling nmake how to create Calc.dll. You need the Calc.cs source file for this; tell nmake the command line to build that DLL:

$(DEST)\Calc.dll: Calc.cs $(DEST)     $(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s

Then do the same thing for Fraction.dll:

$(DEST)\Fraction.dll: Fraction.cs $(DEST)     $(CSC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s

The result of running nmake on this makefile is to create three DLLs: Fraction.dll, Calc.dll, and MySharedAssembly.dll. If you open MySharedAssembly.dll with ILDasm, you'll find that it consists of nothing but a manifest, as shown in Figure 17-3.

Figure 17-3. MySharedAssembly.dll


If you examine the manifest, you see the metadata for the libraries you created, as shown in Figure 17-4.

Figure 17-4. The manifest for MySharedAssembly.dll


You first see an external assembly for the core library (mscorlib), followed by the two modules, ProgCS.Fraction and ProgCS.myCalc.

You now have an assembly that consists of three DLL files: MySharedAssembly.dll with the manifest, and Calc.dll and Fraction.dll with the types and implementation needed.

17.5.1.1 Testing the assembly

To use these modules, you'll create a driver program. Example 17-4 illustrates. Save this program as Test.cs in the same directory as the other modules.

Example 17-4. A module test-driver
namespace Programming_CSharp {     using System;     public class Test     {         // main will not load the shared assembly         static void Main( )         {             Test t = new Test( );             t.UseCS( );             t.UseFraction( );         }         // calling this loads the myCalc assembly         // and the mySharedAssembly assembly as well         public void UseCS( )         {             ProgCS.myCalc calc = new ProgCS.myCalc( );             Console.WriteLine("3+5 = {0}\n3*5 = {1}",                 calc.Add(3,5), calc.Mult(3,5));         }         // calling this adds the Fraction assembly         public void UseFraction( )         {                        ProgCS.Fraction frac1 = new ProgCS.Fraction(3,5);             ProgCS.Fraction frac2 = new ProgCS.Fraction(1,5);             ProgCS.Fraction frac3 = frac1.Add(frac2);             Console.WriteLine("{0} + {1} = {2}",                 frac1, frac2, frac3);         }     } } Output: 3+5 = 8 3*5 = 15 3/5 + 1/5 = 4/5

For the purposes of this demonstration, it is important not to put any code in Main( ) that depends on your modules. You don't want the modules loaded when Main( ) loads, and so no Fraction or Calc objects are placed in Main( ). When you call into UseFraction and UseCalc, you'll be able to see that the modules are individually loaded.

17.5.1.2 Loading the assembly

An assembly is loaded into its application by the AssemblyResolver through a process called probing. The assembly resolver is called by the .NET Framework automatically; you don't call it explicitly. Its job is to load your program.

The three DLLs produced earlier must be in the directory in which Example 17-4 executes or in a subdirectory of that directory that is in the binpath (the user-defined list of subdirectories under the root location that is specified in the application configuration file).


Put a breakpoint on the second line in Main(), as shown in Figure 17-5.

Figure 17-5. A breakpoint in Main( )


Execute to the breakpoint and open the Modules window. Only two of our modules are loaded, as shown in Figure 17-6.

Figure 17-6. Only two modules loaded


If you didn't develop Test.cs as part of a Visual Studio .NET solution, put a call to System.Diagnostics.Debugger.Launch( ) just before the second line in Main(). This lets you choose which debugger to use. (Make sure to compile Test.cs with the options /debug and /r:MySharedAssembly.dll.)


Step into the first method call and watch the Modules window. As soon as you step into UseCS, the AssemblyLoader recognizes that it needs a module from MySharedAssembly.dll. The DLL is loaded, and from that assembly's manifest the AssemblyLoader finds that it needs Calc.dll, which is loaded as well, as shown in Figure 17-7.

Figure 17-7. Modules loaded on demand


When you step into Fraction, the final DLL is loaded.



Programming C#(c) Building. NET Applications with C#
Programming C#: Building .NET Applications with C#
ISBN: 0596006993
EAN: 2147483647
Year: 2003
Pages: 180
Authors: Jesse Liberty

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net