Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

.pdf
Скачиваний:
78
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

482 CHAPTER 15 INTRODUCING .NET ASSEMBLIES

the Win32 system registry to resolve its location (quite the radical departure from Microsoft’s legacy COM programming model). As you will discover during this chapter, the CLR makes use of an entirely new scheme to resolve the location of external code libraries.

Assemblies Are Configurable

Assemblies can be deployed as “private” or “shared.” Private assemblies reside in the same directory (or possibly a subdirectory) as the client application making use of them. Shared assemblies, on the other hand, are libraries intended to be consumed by numerous applications on a single machine and are deployed to a specific directory termed the global assembly cache, or GAC.

Regardless of how you deploy your assemblies, you are free to author XML-based configuration files. Using these configuration files, the CLR can be instructed to “probe” for assemblies under a specific location, load a specific version of a referenced assembly for a particular client, or consult an arbitrary directory on your local machine, your network location, or a web-based URL. You’ll learn a good deal more about XML configuration files throughout this chapter.

Understanding the Format of a .NET Assembly

Now that you’ve learned about several benefits provided by the .NET assembly, let’s shift gears and get a better idea of how an assembly is composed under the hood. Structurally speaking, a .NET assembly (*.dll or *.exe) consists of the following elements:

A Win32 file header

A CLR file header

CIL code

Type metadata

An assembly manifest

Optional embedded resources

While the first two elements (the Win32 and CLR headers) are blocks of data that you can typically ignore, they do deserve some brief consideration. This being said, an overview of each element follows.

The Win32 File Header

The Win32 file header establishes the fact that the assembly can be loaded and manipulated by the Windows family of operating systems. This header data also identifies the kind of application (con- sole-based, GUI-based, or *.dll code library) to be hosted by the Windows operating system. If you open a .NET assembly using the dumpbin.exe utility (via a Visual Studio 2008 command prompt) and specify the /headers flag, you can view an assembly’s Win32 header information. Figure 15-2 shows (partial) Win32 header information for the CarLibrary.dll assembly you will build a bit later in this chapter.

CHAPTER 15 INTRODUCING .NET ASSEMBLIES

483

Figure 15-2. An assembly’s Win32 file header information

The CLR File Header

The CLR header is a block of data that all .NET files must support (and do support, courtesy of the C# compiler) in order to be hosted by the CLR. In a nutshell, this header defines numerous flags that enable the runtime to understand the layout of the managed file. For example, flags exist that identify the location of the metadata and resources within the file, the version of the runtime the assembly was built against, the value of the (optional) public key, and so forth. If you supply the /clrheader flag to dumpbin.exe, you are presented with the internal CLR header information for a given .NET assembly, as shown in Figure 15-3.

Figure 15-3. An assembly’s CLR file header information

484 CHAPTER 15 INTRODUCING .NET ASSEMBLIES

Again, as a .NET developer you will not need to concern yourself with the gory details of Win32 or CLR header information (unless perhaps you are building a compiler for a new managed language!). Just understand that every .NET assembly contains this data, which is used behind the scenes by the .NET runtime and Win32 operating system.

CIL Code, Type Metadata, and the Assembly Manifest

At its core, an assembly contains CIL code, which as you recall is a platformand CPU-agnostic intermediate language. At runtime, the internal CIL is compiled on the fly (using a just-in-time [JIT] compiler) to platformand CPU-specific instructions. Given this architecture, .NET assemblies can indeed execute on a variety of architectures, devices, and operating systems. Although you can live a happy and productive life without understanding the details of the CIL programming language, Chapter 19 offers an introduction to the syntax and semantics of CIL.

An assembly also contains metadata that completely describes the format of the contained types as well as the format of external types referenced by this assembly. The .NET runtime uses this metadata to resolve the location of types (and their members) within the binary, lay out types in memory, and facilitate remote method invocations. You’ll check out the details of the .NET metadata format in Chapter 16 during our examination of reflection services.

An assembly must also contain an associated manifest (also referred to as assembly metadata). The manifest documents each module within the assembly, establishes the version of the assembly, and also documents any external assemblies referenced by the current assembly (unlike legacy COM type libraries, which did not provide a way to document external dependencies). As you will see over the course of this chapter, the CLR makes extensive use of an assembly’s manifest during the process of locating external assembly references.

Note Needless to say by this point in the book, when you wish to view an assembly’s CIL code, type metadata, or manifest, ildasm.exe or reflector.exe are the tools of choice. I will assume you will make extensive use of these tools as you work through the code examples in this chapter.

Optional Assembly Resources

Finally, a .NET assembly may contain any number of embedded resources such as application icons, image files, sound clips, or string tables. In fact, the .NET platform supports satellite assemblies that contain nothing but localized resources. This can be useful if you wish to partition your resources based on a specific culture (English, German, etc.) for the purposes of building international software. The topic of building satellite assemblies is outside the scope of this text; however, you will learn how to embed application resources into an assembly during our examination of Windows Presentation Foundation.

Single-File and Multifile Assemblies

Technically speaking, an assembly can be composed of multiple modules. A module is really nothing more than a generic term for a valid .NET binary file. In most situations, an assembly is in fact composed of a single module. In this case, there is a one-to-one correspondence between the (logical) assembly and the underlying (physical) binary (hence the term single-file assembly).

Single-file assemblies contain all of the necessary elements (header information, CIL code, type metadata, manifest, and required resources) in a single *.exe or *.dll package. Figure 15-4 illustrates the composition of a single-file assembly.

CHAPTER 15 INTRODUCING .NET ASSEMBLIES

485

Figure 15-4. A single-file assembly

A multifile assembly, on the other hand, is a set of .NET *.dlls that are deployed and versioned as a single logic unit. Formally speaking, one of these *.dlls is termed the primary module and contains the assembly-level manifest (as well as any necessary CIL code, metadata, header information, and optional resources). The manifest of the primary module records each of the related *.dll files it is dependent upon.

As a naming convention, the secondary modules in a multifile assembly take a *.netmodule file extension; however, this is not a requirement of the CLR. Secondary *.netmodules also contain CIL code and type metadata, as well as a module-level manifest, which simply records the externally required assemblies of that specific module.

The major benefit of constructing multifile assemblies is that they provide a very efficient way to download content. For example, assume you have a machine that is referencing a remote multifile assembly composed of three modules, where the primary module is installed on the client.

If the client requires a type within a secondary remote *.netmodule, the CLR will download the binary to the local machine on demand to a specific location termed the download cache. If each *.netmodule is 5MB, I’m sure you can see the benefit (compared with downloading a single 15MB file).

Another benefit of multifile assemblies is that they enable modules to be authored using multiple .NET programming languages (which is very helpful in larger corporations, where individual departments tend to favor a specific .NET language). Once each of the individual modules has been compiled, the modules can be logically “connected” into a logical assembly using the C# command-line compiler.

In any case, do understand that the modules that compose a multifile assembly are not literally linked together into a single (larger) file. Rather, multifile assemblies are only logically related by information contained in the primary module’s manifest. Figure 15-5 illustrates a multifile assembly composed of three modules, each authored using a unique .NET programming language.

486 CHAPTER 15 INTRODUCING .NET ASSEMBLIES

Figure 15-5. The primary module records secondary modules in the assembly manifest.

At this point you (hopefully) have a better understanding about the internal composition of a

.NET binary file. With this necessary preamble out of the way, we are ready to dig into the details of building and configuring a variety of code libraries.

Building and Consuming a Single-File Assembly

To begin exploring the world of .NET assemblies, you’ll first create a single-file *.dll assembly (named CarLibrary) that contains a small set of public types. To build a code library using Visual Studio 2008, simply select the Class Library project workspace (see Figure 15-6).

CHAPTER 15 INTRODUCING .NET ASSEMBLIES

487

Figure 15-6. Creating a C# code library

The design of your automobile library begins with an abstract base class named Car that defines a number of protected data members exposed through custom properties (feel free to use automatic property syntax if you wish; see Chapter 13). This class has a single abstract method named TurboBoost(), which makes use of a custom enumeration (EngineState) representing the current condition of the car’s engine:

using System;

namespace CarLibrary

{

//Represents the state of the engine. public enum EngineState

{ engineAlive, engineDead }

//The abstract base class in the hierarchy. public abstract class Car

{

protected string petName; protected int currSpeed; protected int maxSpeed;

protected EngineState egnState = EngineState.engineAlive;

public abstract void TurboBoost();

488 CHAPTER 15 INTRODUCING .NET ASSEMBLIES

public Car(){}

public Car(string name, int max, int curr)

{

petName = name; maxSpeed = max; currSpeed = curr;

}

public string PetName

{

get { return petName; } set { petName = value; }

}

public int CurrSpeed

{

get { return currSpeed; } set { currSpeed = value; }

}

public int MaxSpeed

{get { return maxSpeed; } } public EngineState EngineState

{get { return egnState; } }

}

}

Now assume that you have two direct descendents of the Car type named MiniVan and SportsCar. Each overrides the abstract TurboBoost() method by displaying an appropriate message.

using System;

using System.Windows.Forms;

namespace CarLibrary

{

public class SportsCar : Car

{

public SportsCar(){ }

public SportsCar(string name, int max, int curr) : base (name, max, curr){ }

public override void TurboBoost()

{

MessageBox.Show("Ramming speed!", "Faster is better...");

}

}

public class MiniVan : Car

{

public MiniVan(){ }

public MiniVan(string name, int max, int curr) : base (name, max, curr){ }

public override void TurboBoost()

{

// Minivans have poor turbo capabilities! egnState = EngineState.engineDead;

MessageBox.Show("Time to call AAA", "Your car is dead");

}

}

}

CHAPTER 15 INTRODUCING .NET ASSEMBLIES

489

Notice how each subclass implements TurboBoost() using the Windows Form’s MessageBox class, which is defined in the System.Windows.Forms.dll assembly. For your assembly to make use of the types defined within this external assembly, the CarLibrary project must set a reference to this binary via the Add Reference dialog box (see Figure 15-7), which you can access through the Visual Studio Project Add Reference menu selection.

Figure 15-7. Referencing external .NET assemblies begins here.

It is really important to understand that the assemblies displayed in the .NET tab of the Add Reference dialog box do not represent each and every assembly on your machine. The Add Reference dialog box will not display your custom assemblies, and it does not display all assemblies located in the GAC. Rather, this dialog box simply presents a list of common assemblies that Visual Studio 2008 is preprogrammed to display. When you are building applications that require the use of an assembly not listed within the Add Reference dialog box, you need to click the Browse tab to manually navigate to the *.dll or *.exe in question.

Note Be aware that the Recent tab of the Add Reference dialog box keeps a running list of previously referenced assemblies. This can be handy, as many .NET projects tend to use the same core set of external libraries.

Exploring the Manifest

Before making use of CarLibrary.dll from a client application, let’s check out how the code library is composed under the hood. Assuming you have compiled this project, load CarLibrary.dll into ildasm.exe (see Figure 15-8).

490 CHAPTER 15 INTRODUCING .NET ASSEMBLIES

Figure 15-8. CarLibrary.dll loaded into ildasm.exe

Now, open the manifest of CarLibrary.dll by double-clicking the MANIFEST icon. The first code block encountered in a manifest is used to specify all external assemblies that are required by the current assembly to function correctly. As you recall, CarLibrary.dll made use of types within mscorlib.dll and System.Windows.Forms.dll, both of which are listed in the manifest using the

.assembly extern token:

.assembly extern mscorlib

{

.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )

.ver 2:0:0:0

}

.assembly extern System.Windows.Forms

{

.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )

.ver 2:0:0:0

}

Here, each .assembly extern block is qualified by the .publickeytoken and .ver directives. The

.publickeytoken instruction is present only if the assembly has been configured with a strong name (more details on strong names in the section “Understanding Strong Names” later in this chapter). The .ver token defines (of course) the numerical version identifier.

After cataloging each of the external references, you will find a number of .custom tokens that identify assembly-level attributes. If you examine the AssemblyInfo.cs file created by Visual Studio (which can be viewed by expanding the Properties icon of the Solution Explorer), you will find these attributes represent basic characteristics about the assembly such as company name, trademark, and so forth.

Chapter 16 examines attributes in detail, so don’t sweat the details at this point. Do be aware, however, that the attributes defined in AssemblyInfo.cs update the manifest with various .custom tokens, such as [AssemblyTitle]:

.assembly CarLibrary

{

...

.custom instance void [mscorlib] System.Reflection.AssemblyTitleAttribute::.ctor(string) =

( 01 00 0A 43 61 72 4C 69 62 72 61 72 79 00 00 )

// ...

CarLibrary..

.hash algorithm 0x00008004

.ver 1:0:0:0

}

.module CarLibrary.dll

CHAPTER 15 INTRODUCING .NET ASSEMBLIES

491

Finally, you can also see that the .assembly token is used to mark the friendly name of your assembly (CarLibrary), while the .module token specifies the name of the module itself (CarLibrary.dll). The .ver token defines the version number assigned to this assembly, as specified by the [AssemblyVersion] attribute within AssemblyInfo.cs.

Exploring the CIL

Recall that an assembly does not contain platform-specific instructions; rather, it contains plat- form-agnostic common intermediate language (CIL) instructions. When the .NET runtime loads an assembly into memory, the underlying CIL is compiled (using the JIT compiler) into instructions that can be understood by the target platform. If you double-click the TurboBoost() method of the SportsCar class, ildasm.exe will open a new window showing the CIL tokens that implement this method:

.method public hidebysig virtual instance void

TurboBoost() cil managed

{

// Code size

.maxstack 8 IL_0000: nop IL_0001: ldstr IL_0006: ldstr IL_000b: call

18 (0x12)

"Ramming speed!"

"Faster is better..."

valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult

[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string, string) IL_0010: pop

IL_0011: ret

} // end of method SportsCar::TurboBoost

Notice that the .method tag is used to identify a method defined by the SportsCar type. Member variables defined by a type are marked with the .field tag. Recall that the Car class defined a set of protected data, such as currSpeed:

.field family int32 currSpeed

Properties are marked with the .property tag. Here is the CIL describing the public CurrSpeed property (note that the read/write nature of a property is marked by .get and .set tags):

.property instance int32 CurrSpeed()

{

.get instance int32 CarLibrary.Car::get_CurrSpeed()

.set instance void CarLibrary.Car::set_CurrSpeed(int32) } // end of property Car::CurrSpeed

As you can see, the get/set scopes of a property simply delegate to normal (and hidden) methods within the assembly (get_CurrSpeed() and set_currSpeed() in this case). Again, while most .NET developers do not need to be deeply concerned with the details of CIL, Chapter 19 will provide more details on the syntax and semantics of the common intermediate language.

Exploring the Type Metadata

Finally, if you now press Ctrl+M, ildasm.exe displays the metadata for each type within the CarLibrary.dll assembly (see Figure 15-9).