Previous section   Next section

17.6 Multi-Module Assemblies

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.

A multi-module 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.6.1 Benefitting from Multi-Module Assemblies

Multi-module assemblies have advantages for real-world programs, especially if they are developed by multiple developers or are very large.

Imagine that 25 developers are working on a single project. If they were to create a single-module assembly to build and test the application, all 25 programmers would have to check in their latest code simultaneously, and the entire mammoth application would be built. That creates a logistical nightmare.

If they each build their own modules, however, the program can be built with the latest available module from each programmer. This relieves the logistics problems; each module can be checked in when it is ready.

Perhaps more importantly, multiple modules make it easier to deploy and to maintain large programs. Imagine that each of the 25 developers builds a separate module, each in its own DLL. The person responsible for building the application would then create a 26th module with the manifest for the entire assembly. These 26 files can be deployed to the end user. The end user then need only load the one module with the manifest, and he can ignore the other 25. The manifest will identify which of the 25 modules has each method, and the appropriate modules will be loaded as methods are invoked. This will be transparent to the user.

As modules are updated, the programmers need only send the updated modules (and a module with an updated manifest). Additional modules can be added and existing modules can be deleted; the end user continues to load only the one module with the manifest.

In addition, it is entirely likely that not all 25 modules will need to be loaded into the program. By breaking the program into 25 modules, the loader can load only those parts of the program that are needed. This makes it easy to shunt aside code that is only rarely needed into its own module, which might not be loaded at all in the normal course of events. Although this was the theory behind DLLs all along, .NET accomplishes this without "DLL Hell," a monumental achievement described later in this chapter.

17.6.2 Building a Multi-Module Assembly

To demonstrate the use of multi-module assemblies, Example 17-1 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. The Fraction class
Option Strict On
Imports System

Namespace ProgVB
  
   Public Class Fraction
      
      Public Sub New(numerator As Integer, denominator As Integer)
         Me.numerator = numerator
         Me.denominator = denominator
      End Sub 'New
      
      Public Function Add(rhs As Fraction) As Fraction
         If rhs.denominator <> Me.denominator Then
            Throw New ArgumentException("Denominators must match")
         End If
         
         Return New Fraction(Me.numerator + rhs.numerator, Me.denominator)
      End Function 'Add
      
      Public Overrides Function ToString( ) As String
         Return numerator.ToString( ) + "/" + denominator.ToString( )
      End Function 'ToString
      
      Private numerator As Integer
      Private denominator As Integer
   End Class 'Fraction
End Namespace 'ProgVB

Notice that the Fraction class is in the ProgVB namespace. The full name for the class is ProgVB.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
Option Strict On
Imports System

Namespace ProgVB
   
   Public Class myCalc
      
      Public Function Add(val1 As Integer, val2 As Integer) As Integer
         Return val1 + val2
      End Function 'Add
      
      Public Function Mult(val1 As Integer, val2 As Integer) As Integer
         Return val1 * val2
      End Function 'Mult
   End Class 'myCalc
End Namespace 'ProgVB

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

This is sufficient to create an assembly. Use an AssemblyInfo.vb file to add some metadata to the assembly. The use of metadata is covered in Chapter 18. An example AssemblyInfo.vb file is shown in Example 17-3.

Example 17-3. AssemblyInfo.vb
Option Strict On
Imports System.Reflection
Imports System.Runtime.InteropServices

<Assembly: AssemblyTitle("")> 
<Assembly: AssemblyDescription("")> 
<Assembly: AssemblyCompany("")> 
<Assembly: AssemblyProduct("")> 
<Assembly: AssemblyCopyright("")> 
<Assembly: AssemblyTrademark("")> 
<Assembly: Guid("401658E1-6FC7-4BB4-AE86-8463FEB1703B")> 
<Assembly: AssemblyVersion("1.0.*")>

You can write your own AssemblyInfo.vb file, but the simplest approach is to let Visual Studio generate one for you automatically by creating a dummy application and then just borrowing the resulting AssemblyInfo.vb file.

Visual Studio creates single-module assemblies by default. You can create a multi-module resource option using the command-line compiler with the /addModules option. The easiest way to compile and build a multi-module 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 only 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.

Example 17-4 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.vb, Fraction.vb, and AssemblyInfo.vb. Start up a .NET command window and cd to that directory. Invoke nmake without any command switches. You will find the SharedAssembly.dll in the \binsubdirectory.

Example 17-4. The makefile
ASSEMBLY= MySharedAssembly.dll

BIN=.\bin
SRC=.
DEST=.\bin

VBC=vbc /nologo /debug+ /debug:full

MODULETARGET=/t:module
LIBTARGET=/t:library
EXETARGET=/t:exe

REFERENCES=System.dll

MODULES=$(DEST)\Fraction.dll $(DEST)\Calc.dll
METADATA=$(SRC)\AssemblyInfo.vb

all: $(DEST)\MySharedAssembly.dll

# Assembly metadata placed in same module as manifest
$(DEST)\$(ASSEMBLY): $(METADATA) $(MODULES) $(DEST)
    $(VBC) $(LIBTARGET) /addmodule:$(MODULES: =,) /out:$@ %s

# Add Calc.dll module to this dependency list
$(DEST)\Calc.dll: Calc.vb $(DEST)
    $(VBC) $(MODULETARGET) /r:$(REFERENCES: =;) /out:$@ %s

# Add Fraction
$(DEST)\Fraction.dll: Fraction.vb $(DEST)
    $(VBC) $(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:

BIN=.\bin
SRC=.
DEST=.\bin

Build the assembly as follows:

$(DEST)\$(ASSEMBLY): $(METADATA) $(MODULES) $(DEST)
    $(VBC) $(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 assembly consists of the metadata and the modules, and it provides the command line required to build the assembly.

The metadata is defined earlier as:

METADATA=$(SRC)\AssemblyInfo.vb

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)
    $(VBC) $(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.vb source file for this; tell nmake on the command line to build that DLL:

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

Then do the same thing for fraction.dll:

$(DEST)\Fraction.dll: Fraction.vb $(DEST)
    $(VBC) $(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
figs/pvn2_1703.gif

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
figs/pvn2_1704.gif

You first see an external assembly for the core library (mscorlib), followed by the two modules, ProgVB.Fraction and ProgVB.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.6.2.1 Testing the assembly

To use these modules, you need to create a driver program that will load in the modules as needed. Create a new Console application in Visual Studio .NET in the same directory as the dll files and name it TestVB. Add a reference to MySharedAssembly by right-clicking on the References in the Solution window, and then clicking on the Add References pop-up menu choice. Click on the Projects tab and Browse to the .dll. Once you select it, it will appear in the Selected Components window, and clicking OK will add it to your references.

Create a new file called module1.vb and add the code shown in Example 17-5.

Example 17-5. TestVB
Option Strict On
Imports System

Namespace ProgVB

    Public Class Test

        ' main will not load the shared assembly
        Shared Sub Main( )
            Dim t As New Test( )
            t.UseCS( )
            t.UseFraction( )
        End Sub 'Main

        ' calling this loads the myCalc assembly
        ' and the mySharedAssembly assembly as well
        Public Sub UseCS( )
            Dim calc As New ProgVB.myCalc( )
            Console.WriteLine("3+5 = {0}" + _
                ControlChars.Lf + "3*5 = {1}", _
                calc.Add(3, 5), calc.Mult(3, 5))
        End Sub 'UseCS

        ' calling this adds the Fraction assembly
        Public Sub UseFraction( )
            Dim frac1 As New ProgVB.Fraction(3, 5)
            Dim frac2 As New ProgVB.Fraction(1, 5)
            Dim frac3 As ProgVB.Fraction = frac1.Add(frac2)
            Console.WriteLine("{0} + {1} = {2}", frac1, frac2, frac3)
        End Sub 'UseFraction
    End Class 'Test
End Namespace 'ProgrammingVB

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

17.6.2.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 do not call it explicitly. Its job is to resolve the assembly name to an EXE program and load your program.

With a private assembly, the AssemblyResolver looks only in the application load directory and its subdirectories—that is, the directory in which you invoked your application.

The three DLLs produced earlier must be in the directory in which Example 17-5 executes or in a subdirectory of that directory.

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

Figure 17-5. Setting the breakpoint
figs/pvn2_1705.gif

Execute to the break point and open the Modules window. Only two modules are loaded, as shown circled in Figure 17-6.

Figure 17-6. Only two modules loaded
figs/pvn2_1706.gif

Step into the first method call and watch the modules window. As soon as you step into UseCS, the AssemblyLoader recognizes that it needs an assembly 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
figs/pvn2_1707.gif

When you step into Fraction, the final DLL is loaded. The advantage of multi-module assemblies is that a module is loaded only when it is needed.


  Previous section   Next section
Top