Home > Articles > Programming > Windows Programming

Exploring the CLR

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

Assemblies

You deploy .NET types in Assemblies. A .NET assembly is the unit of deployment, reuse, versioning, and security in the .NET Framework. An assembly consists of one or more modules. Each module may contain either a managed code PE file with MSIL code and metadata, or it may contain resources like JPEG or BMP files. One of the managed code PE files must contain a manifest. The manifest is part of an assembly's metadata, it contains the following information:

  • Versioning information about the assembly, such as its version number, culture information, the publisher's public key, and some additional flags.

  • An entry for each file that constitutes the assembly that contains the file name, but not the path, of each file in the assembly, a hash value of the file's contents, and some additional flags.

  • An entry for each publicly exported type in the assembly that tells the runtime which file in the assembly contains the type.

  • An entry for each resource that tells the runtime which resource file in the assembly contains a particular resource.

Figure 3–3 shows a schematic view of a single file assembly and a multifile assembly.

In the single file case, the header, the metadata—including the manifest—and the MSIL code are all contained in a file called singlefile.dll. In the multi file case, there are a total of 3 files: multifile1.dll, multifile2.dll, and multifile3.dll. Multifile1.dll is the most important file in the assembly because it contains the manifest for the assembly; it also contains MSIL code and metadata. Multifile2.dll contains MSIL code and metadata also, but notice that it does not contain a manifest. The last file in the multi file assembly, multifile3.dll, contains images and other resources.

Figure 03Figure 3–3 A multifile assembly.


To a client that is using an assembly, the fact that the assembly may consist of multiple files is completely hidden. The CLR uses information in the assembly's metadata to create a logical single-file illusion for the consumers of the assembly. Only the developer of the assembly needs to be aware that the physical implementation of the assembly consists of multiple files. When you want to use the types in an assembly, you simply reference the file that contains the manifest. (You can consider this to be the main module of the assembly although it isn't officially labeled as such.) When you first use a type or resource that is not contained in the main file, the CLR will use the information in the manifest to load the module that contains the type or resource. Before it loads the file, the CLR will verify that the module that it is loading is exactly the same as the module that the assembly was built with. It does this using information in the manifest. For each module in the assembly, the manifest stores the file name and a hash value that is generated from the contents of the file. The CLR will regenerate the hash value from the module that resides on disk and compare it to the hash value that is stored in the manifest for that module. If the two hash values do not match, the module that currently resides on disk is not the same as the module that the assembly was originally built with, and the CLR will throw an error. This process guarantees the atomicity of the assembly. In other words, it guarantees that the assembly is deployed and versioned as an indivisible whole. You cannot, for instance, fix a bug by deploying a new version of just one of the modules in an assembly. If you want to make a change to any part of an assembly, you must rebuild and redeploy the entire assembly.

You are hopefully starting to get the idea that an assembly has two views: (1) a logical view and (2) a physical view. The physical view of an assembly is a collection of files that are either managed code module files that contain metadata and MSIL code or resources files that contain JPEGs, Graphic Interchange Formats (GIFs), or other graphical elements like menus or icons. One of these managed code files contains a manifest. The logical view of an assembly is a collection of types in one or more namespaces. In this logical view, the assembly is a single, atomic whole.

By now you're probably asking why Microsoft decided to create this new assembly concept. What advantages does it have over the Win32 approach in which the unit of deployment, versioning, and security is a single executable (DLL or EXE) file? The major advantage of the assembly concept is that it is far more efficient in the scenario where code is downloaded across the Internet. The assembly concept allows you to take rarely used types and resources and package them into separate files that will only be downloaded from the Internet if they are needed. Whether you are dealing with a code library or a functional application, it is very rare for someone to use all of the capabilities of a piece of code during each use.

If you are still confused about the whole concept of an assembly, keep in mind that most assemblies that you create will consist of a single file. In fact, Visual Studio .NET only supports the creation of a single-file assembly. If you want to create a multi file assembly, you have to use the command-line tools in the .NET Framework SDK.

An Example: Building an Assembly

In order for you to gain a better understanding of assemblies, let's build a very simple example that shows you how to create a multi-file assembly. The example is a simple assembly that contains two main types: an Employee class and a Manager class that derives from the Employee class. Each of these types will reside in its own module. The third file will contain only the manifest for the assembly. A schematic of the assembly is shown in Figure 3–4.

Figure 04Figure 3–4 An example multifile assembly.


Because Visual Studio .NET does not support multi-file assemblies, you will build this assembly using the command-line tools in the .NET Framework SDK. You will also use this assembly as a running example throughout the chapter to discuss other topics like metadata and MSIL.

A Summary of the Steps

To build an assembly, you will perform the following steps:

  1. Declare an Employee class.

  2. Declare a Manager class.

  3. Compile the Employee class into a module.

  4. Compile the Manager class into a module.

  5. Compile the modules with an assembly info file (that contains metadata for the assembly) into an assembly.

  6. Create a client application that uses the assembly.

Declare an Employee Class

The Employee class is declared as follows:

using System;
namespace metadatademo
{
  public class Employee 
  {
   public Employee(int id,string name,
      decimal salary)
   {
    this.mName=name;
    this.mID=id;
    this.mSalary=salary;
   }
   public int ID
   {
    get { return mID; }
    set { mID=value; }
   }
   public string Name
   {
    get { return mName; }
    set { mName=value; }
   }
   public virtual decimal GetSalary()
   {
    return mSalary;
   }
  private int mID;
  private string mName;
  private decimal mSalary;
  }
 } 

This class is very simple. It's essentially the same definition that I used earlier when I talked about reference types, so I won't go into an in-depth discussion of this class here. If there are things that you don't understand about this definition like the use of the "using" and "namespace" keywords or the declaration of the "Name" property, don't worry about it. These things will be clearer later after I talk about C# in Chapter 4. Save this code in a file called Employee.cs.

Declare a Manager Class

The Manager class is declared as follows:

using System;
namespace metadatademo
{

  public class Manager : Employee
  {
   public Manager(int id,string name,
      decimal salary,decimal bonus) :
      base(id,name,salary)
   {
    this.mBonus=bonus;
   }
   public override decimal GetSalary()
   {
    return base.GetSalary()+mBonus;
   }
   private decimal mBonus;
  }
 }

Again, this is a very simple class. Don't worry about the syntax, such as the use of the "base" and "override" keywords; their use will be clear after Chapter 4. Save this code in a file called Manager.cs.

Compile the Employee Class into a Module

To compile the Employee class into a module, open up a command prompt. If you have Visual Studio .NET installed, select Start | Programs | Microsoft Visual Studio .NET | Visual Studio .NET Tools | Visual Studio .NET Command Prompt. This creates a command prompt session that is guaranteed to have the environment (path and so forth ) configured properly for using the command-line tools in the .NET Framework SDK. Now change directories to the directory where you saved the Employee.cs and Manager.cs files and enter the following command at the prompt:

csc /out:Employee.mod /t:module Employee.cs

This command will run the C# compiler (csc). The /out parameter allows you to specify the name of the output file, which in this case is Employee.mod. There is nothing significant about the .mod extension; you could give this file any extension you want. The /t parameter is short for "target," and it's how you specify the type of output you want the compiler to produce. In this case, I want a module file to be produced, which is a file that will be combined with other files to create an assembly. Some of the other possible values for the /t parameter to the C# compiler are the following:

  • exe—Creates a console executable assembly.

  • winexe—Creates a windows executable assembly.

  • library—Creates a DLL assembly.

  • module—Creates a module that must be combined with other files to create an asssembly.

Compile the Manager Class into a Module

To compile the Manager class into module, run the following command in the same command prompt that you used to compile the Employee class:

csc /out:Manager.mod /t:module /addmodule:Employee.mod Manager.cs

Notice that, in this case, I specified that the output file is called Manager.mod and that the output type is a module. The /addmodule parameter requires some explanation though. You cannot compile the Manager class without referring to the module that contains the Employee class. The compiler must have access to the definition of the Employee class because the Manager derives from the Employee class. You use the /addmodule parameter when you want to create modules that are dependant on types in other modules or when you are combining modules into an assembly. In this case, if you did not use the /addmodule:Employee.mod parameter, you would receive the following error at compile time:

Manager.cs (5,25): error CS0246: The type of 
namespace name 'Employee' could not be found. 

Compile the Modules into an Assembly

We now have our Employee and Manager types compiled into modules, but, at this point, they are still not usable. In order to use these types, they must be built into an assembly with a manifest. To create an assembly, I use the C# compiler to create a third file that contains a manifest that refers to the Employee and Manager modules. To do this, run the following command in the same command prompt that you used to compile the Employee and Manager classes:

csc /out:multifile.dll /t:library 
/addmodule:employee.mod /addmodule:manager.mod assemblyinfo.cs

In this case, I specify the name of the assembly as multifile.dll and the type of the output as library, which means I am creating a DLL-based assembly. I then use the /addmodule parameter to link the employee and manager modules into the assembly.

Before you can understand the other source file that appears in this command, assemblyinfo.cs, I have to discuss attributes first.

Attributes

Managed code compilers automatically add various metadata elements without you, the developer, having to specify explicitly that the metadata should be added or without having to specify what the value of that metadata element should be. An example of this is the hash value that is stored for each module in an assembly. There are other kinds of metadata for which developers must have some explicit control. Some examples of this type of metadata include:

  • Whether a class requires a transaction when it runs

  • Whether a method in a class is exposed through a Web service or the version number of an assembly

  • Whether an input parameter to a method is an input, an output, or both.

You can explicitly declare and set the values for certain kinds of metadata using attributes. There are pre-defined attributes that you can set values for. You can also create user-defined attributes. Some of the pre-defined attributes provide information that may be used by client code or code generators. Other attributes influence the behavior of the CLR, such as the AssemblyFlagsAttribute that you can use to specify the allowable side-by-side execution behavior of an assembly. Some attributes influence the way a class behaves. For instance, when a class is adorned with the Transaction attribute and the TransactionOption parameter for this attribute is TransactionOption.Required, the CLR and the COM+ runtime will make sure that a Distributed Transaction Coordinator (DTC) transaction is started whenever you call a method on an instance of the class.

In addition, you can create your own attributes. Users can apply your user-defined attributes to your types, and then, at runtime, your code can query for the values that the user has assigned to the attribute and take appropriate action.

Attributes provide support for the declarative programming style that was first popularized with COM (in the way it supports threading models using the ThreadingModel registry key) and MTS. You can support and configure functionality simply by setting an attribute that is associated with a type.

To specify a value for an attribute in C#, you use the following syntax:

[attributename]

An example is shown here:

[Transaction] 
public class Books : ServicedComponent
{
//
}

This code is using the Transaction attribute from the System.EnterpriseServices namespace. You use this attribute to specify that a class requires transaction support from the COM+ runtime infrastructure.

NOTE

You will learn more about the Transaction Attribute, serviced components, and COM+ integration in Chapter 9.

Each attribute is a class that derives from the Attribute class in the System namespace. In this case, the Transaction attribute is a class called TransactionAttribute, which resides in the System.EnterpriseServices namespace. An attribute may have parameters on its constructor that allow you to specify how the attribute behaves. An example of this is the TransactionOption parameter of the Transaction attribute. This parameter may be set to one of the four possible values of the TransactionOption enumeration shown in Table 3–4.

Table 3–4 Possible values for the TransactionOption parameter of the transaction attribute

TransactionOption Value

Description

Disabled

Ignores any transactions in the current context.

NotSupported

Will neither start a transaction nor participate in its caller's transaction.

Supported

Will not start a transaction, but will share its caller's transaction if it has one.

Required

Will share its caller's transaction if it has one; starts a new transaction if the caller does not have a transaction.

RequiresNew

Always starts a new transaction.


The default value of this parameter is Required, so the code I showed with the Books class could have been written—equivalently—as follows:

[Transaction(TransactionOption.Required)] 
public class Books : ServicedComponent
{
//
}

If you want to set the TransactionOption parameter to Supported, you could use the following code:

[Transaction(TransactionOption.Supported)] 
public class Books : ServicedComponent
{
//
}

Attributes can also have named parameters. For instance, the Transaction attribute has Timeout and Isolation parameters. You can use the Timeout parameter to specify the maximum duration of the transaction before it times out. The Isolation parameter allows you to specify the isolation level for the transaction.

NOTE

I will discuss transactions and isolation levels in Chapter 9.

In the following example, I have set the timeout to 45 seconds and the isolation level to Repeatable Read.

[Transaction(TransactionOption.Required,
 Isolation=TransactionIsolationLevel.RepeatableRead,
 Timeout=45)]
public class Books : ServicedComponent
{
//
}

The generalized form of an attribute declaration in C# is as follows:

[attributename(param1,param2,,paramN,prop1=val1, 
prop2=val2,, propN=valN)]

Where param[N] is the specified value for a parameter and prop[N] is the name of a property and val[N] is the value assigned to that property.

Attributes can be applied to an assembly; a module; or any type, such as a class, structure, enum, or delegate; they can also be applied to any member of a type, such as a field, property, method, and so forth. All attributes have a usage attribute that specifies which elements the attribute can be applied to. The Transaction attribute can only be applied to a class. The following code uses the WebMethod attribute from the System.Web.Services namespace to specify that a method will be exposed using a Web service. The WebMethod attribute can only be applied to a method.

[WebMethod]
public DataSet GetBooksByTitle(string title)
{
//
}

If you tried to apply the WebMethod attribute to a class as shown here:

[WebMethod]
public class Books : ServicedComponent
{
//
}

you will receive the following compiler error:

Attribute 'WebMethod' is not valid on this 
declaration type. It is valid on 'method' 
declarations only.

The .NET Framework contains a special set of attributes called assembly attributes that were developed to provide information about an assembly as opposed to types or members of a type. These assembly attributes are divided into four groups: identify attributes, informational attributes, manifest attributes, and strong name attributes. The assembly identity attributes are shown in Table 3-5.

Table 3–5 The assembly identity attributes

Identity Attribute

Description

AssemblyVersion

This is a numerical version number in the format major.minor.build.revision.

AssemblyCulture

If this attribute is set to something other than the empty string (""),it indicates that this is a satellite assembly that contains culture-specific resources.

AssemblyFlags

This is used to specify whether an assembly supports side-by-side execution on the same machine, in the same process, or in the same application domain.


The assembly informational attributes are shown in Table 3–6.

Table 3–6 The assembly informational attributes

Informational Attribute

Description

AssemblyCompany

Specifies the name of the company that produced the assembly.

AssemblyCopyright

Specifies copyright information for the assembly.

AssemblyFileVersion

Specifies the version number that will appear in the Win32 file version resource. It defaults to the assembly version, but does not have to be the same.

AssemblyInformationalVersion

Specifies version number information that is not used by the runtime.

AssemblyProduct

Specifies product information.

AssemblyTrademark

Specifies trademark information.


The assembly manifest attributes are shown in Table 3–7.

Table 3–7 The assembly manifest attributes

Manifest Attribute

Description

AssemblyConfiguration

Specifies the configuration of the resource, for example, retail or debug.

AssemblyDefaultAlias

Specifies a friendly name to use for the assembly if the name of the resource is not friendly.

AssemblyDescription

Specifies a short description of the purpose and description of the assembly.

AssemblyTitle

Specifies another friendly name for the assembly.


The strong name attributes are shown in Table 3–8.

Table 3–8 The assembly strong name attributes

Identity Attribute

Description

AssemblyDelaySign

A Boolean value that indicates whether you want to reserve space in the assembly for a signature, but delay actual signing until later (true) or whether you will be signing the assembly with the private key immediately (false). Using this attribute makes it easier for a company to safeguard its private key by not requiring all developers to have it.

AssemblyKeyFile

Specifies the name of the file that contains the public (or public and private) key.

AssemblyKeyName

Specifies the key container that contains the key pair.


You can assign these assembly attributes to an assembly using the standard attribute declaration prefixed with assembly: as follows:

[assembly: AttributeName(param1,param2,,paramN,prop1=val1, 
prop2=val2,, propN=valN)]

You can create an assembly without using any of these attributes. However, the assembly will not have a strong name, description, or title, and its version number will be 0.0.0.0. Therefore, in most cases, you will want to use at least some of these attributes when you create your assemblies. You can declare these attributes in any source file of the assembly that you want, but a good convention (and the convention used by Visual Studio .NET) is to create an assembly info file that contains all the assembly-level attribute declarations. You then compile this source code file into your assembly.

With that, I can move the discussion back to the assembly info file in the example. I created a file called assemblyinfo.cs for the example, and this file will contain the assembly attributes for the assembly. The contents of this file are as follows:

using System.Reflection;
[assembly: AssemblyDescription("A test multifile assembly")]
[assembly: AssemblyVersion("1.2.3.4")]
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("mykey.snk")]

In this file, I start by adding a using statement for the Reflection namespace in the System assembly because all of the assembly attributes are declared in that namespace. I use the AssemblyDescription attribute to add a description for the assembly. The description is "A test multifile assembly". I use the AssemblyVersion attribute to set the version number of the assembly to "1.2.3.4". These four numbers are the major, minor, build, and revision number, respectively. Before I talk about the AssemblyDelaySign and AssemblyKeyFile attributes I first need to discuss how to create a cryptographic key pair.

The .NET Framework SDK contains a tool called the Strong Name Tool (sn.exe) that you can use to create a cryptographic key pair. To use the Strong Name Tool, open a Visual Studio .NET command prompt by selecting Programs | Microsoft Visual Studio .NET | Visual Studio .NET Tools | Visual Studio .NET Command Prompt from the Start menu, change directories to the location where you created the assemblyinfo.cs file and then enter the following command.

sn –k mykey.snk

This command will create a file called "mykey.snk" that contains both the public and private key of a key pair. Notice that we specified the "mykey.snk" file in the AssemblyKeyFile attribute to indicate that we wish to sign the assembly with this key pair. We also use the AssemblyDelaySign attribute to indicate that we are signing the assembly immediately with the private key specified in the AssemblyKeyFile attribute.

Create a Client Application that Uses the Assembly

To test the assembly, I create a very simple console application that uses the Manager and Employee classes. The code for this test application is as follows:

using System;
using metadatademo;
class TestClass
{
  static void Main(string[] args)
  {
   Employee emp1, emp2;
   emp1=new Employee(1,"Alan Gordon",500);
   emp2=new Manager(2,"Tamber Gordon",800,200);
   System.Console.WriteLine(
      "Emp1 name = {0}",emp1.Name);
   System.Console.WriteLine(
      "Emp2 name = {0}",emp2.Name);
  }
 }

To compile this code into an executable application, save the preceding code into a file called testclass.cs and execute the following command at the same command prompt that you used previously to build the multifile assembly:

csc /out:testmultifile.exe /t:exe /r:multifile.dll testclass.cs

To build an application or module that uses types in an assembly, you must use the /reference parameter on the C# compiler, which is usually abbreviated /r, to reference the assembly. The compiler uses the metadata in the referenced assembly to do compile-time validation; it also inserts metadata in the client application that tells the CLR the name, version number, strong name, and so forth of the assembly that the application was built against. The CLR will use this information when you run the application to make sure that it uses the same, or a compatible, version of the assembly.

Try running the client application. It will print the names of two employees: Alan Gordon and Tamber Gordon. You might want to add more code to test the Employee and Manager types further. Another good exercise is to attempt to recompile one of the files in the multifile assembly without rebuilding the entire assembly. This is a definite "no-no" because the CLR will enforce the atomicity of the assembly. If the hash of one of the files that constitutes the assembly is not exactly the same as the hash in the assembly's manifest, the CLR will fail to load the file. I got the following error when I rebuilt the Employee module alone without recompiling the assembly:

System.IO.FileLoadException: The check of the 
module's hash failed for Employee.mod

Metadata

Now that you understand types, assemblies, and attributes, we are ready to begin an in-depth discussion of .NET metadata. Metadata is key to making software development manageable. Metadata is descriptive information about the types in an executable file or software library. This descriptive information includes the name of each type and the name and type of each member (fields, methods, events, and so forth) of each type. For method and events, this metadata should also include the name and type of each parameter to the method or event as well as the return type of the method or event.

Metadata is not a new concept. A header (".h") file in C/C++ is an example of metadata. If you are a COM programmer, you are probably familiar with type libraries, which are the metadata format for COM.

Metadata is used for lots of purposes. Compilers or runtime environments use it to lay out objects in memory. An example of this is a C++ compiler using the definition of a class in a header file to create the vtable for the class in memory. Metadata is also used by system-level code to implement marshaling, for example, to allow code running in one process to use code that is executing in a different process. An example of this is type-library marshaling in COM. Type library marshaling uses metadata in the type library to serialize method calls into a stream that can be sent across processes or machines. Code generators use metadata to create code. An example of this is the COM support in MFC that includes a Visual C++ code generator that allows you to create a set of ColeDispatchDriver-derived wrapper classes for a COM server from the type definitions in its type library. Metadata is also used by compilers to do compile-time error checking that would have to be deferred until runtime if metadata was not available. This error checking includes verifying that a method that you are calling on an object is actually a member of the object's class with the appropriate access and that the method is being called with the right number and types of arguments. Most programmers also use metadata as their first level of documentation. This is especially true now that most IDEs support Intellisense technology. Intellisense uses metadata to automatically display, in the programmer's editor, the complete list of available methods that may be called on an object as soon as the developer types in the method invocation symbol ("." or "->") after a variable name. Intellisense works by interrogating the metadata associated with the types that you are using.

There are a number of problems with COM type libraries that .NET metadata rectifies. First, a type library may or may not be embedded within the executable code that it describes. Most ActiveX controls have their type library embedded as a resource in their .ocx file, but the type library for Office applications (Excel, Word, and so forth) resides in a separate file. If the metadata is in a separate file, there is always a danger that it will become separated from the code that it describes or that someone will accidentally ship version X of the type library with version Y of a COM server. Another problem with COM type libraries is that they aren't guaranteed to be present. Most COM components that I have seen ship with a type library, but the COM specification does not require it.

.NET metadata fixes all of these problems. The metadata for a .NET assembly is always embedded within the assembly. A managed code compiler must generate both MSIL code and metadata. There is no danger that the assembly and its metadata will be separated or that the assembly will be shipped with the wrong version of its metadata.

The metadata in an assembly consists of a manifest and a set of metadata tables that contain the bulk of the type-specific metadata of the assembly, including descriptive information about each type and each field in each type. Regardless of how many files an assembly contains, it will only have one manifest, which may be embedded in one of the modules that comprise the assembly or it may reside in its own file.

The manifest is probably the most important single piece of metadata. The manifest is the only assembly file that a consumer of the assembly has to reference. The manifest contains a directory or index that contains all the information that the CLR needs to locate all the types that are implemented within an assembly, regardless of which module they reside in. The other files in the assembly are loaded on demand only when you use a type that is implemented in one of those files. A high-level listing of the contents of a manifest is the following:

  • Versioning information about the assembly, such as its version number, culture information, the publisher's public key, and some additional flags

  • An entry for each file that makes up the assembly that contains the file name, but not the path, of each file in the assembly and a hash value of the file's contents

  • An entry for each publicly exported type in the assembly that tells the runtime which module in the assembly contains the implementation of the type

  • An entry for each resource that tells the runtime which file in the assembly contains a particular resource

  • An entry for each external assembly on which the assembly depends that contains the name, version number, public key, and hash value of the referenced assembly

Let's look at the manifest in the multifile assembly that you created earlier. In this case, the manifest is the sole contents of the file called multifile.dll. You can view the manifest for an assembly and all the other metadata for a .NET assembly using the MSIL disassembler (ILDasm). See the sidebar on ILDASM to learn more about this powerful tool.

The MSIL Disassembler (ILDASM)

ILDASM is a tool that is bundled with the .NET Framework that allows you to view both the compiled MSIL code and the metadata within an assembly. After you have installed Visual Studio .NET, the easiest way to use ILDASM is to select Programs | Microsoft Visual Studio .NET | Visual Studio .NET Tools | Visual Studio .NET Command Prompt. This gives you a command prompt that is configured with the environment that you need to run any of the command-line tools in the .NET Framework SDK. Now you can run ILDASM by typing "ildasm [space] assemblyname". You can also fire up ILDASM without loading an assembly and then choose File | Open. ILDASM also supports drag and drop. Use the command line "ildasm /?" to view the command-line options for ILDASM. You can also find documentation about ILDASM in the .NET Framework SDK documentation at \Tools and Debuggers\.NET Framework Tools\. Curiously, neither this documentation or the /? command say anything about the /adv argument for ILDASM, which makes the tool display more verbose information. To find out about this option, you have to go to a rather small Word file in the Tool Developers Guide documentation, which you can find at [Root directory for Visual Studio .NET installation]\FrameworkSDK\Tool Developers Guide\docs\ILDasmAdvancedOptions.doc. If you run ILDASM with the "/adv" parameter as follows:

Ildasm /adv assemblyname

It will add three new items to the View menu:

  1. COR Header, which displays the header for an assembly. The header contains both the PE header and the CLR header.

  2. Statistics, which allow you to see the sizes of the various portions of the assembly, such as the PE header, the CLR header, the metadata, and the managed code.

  3. Metainfo, which contains several submenu items, the most important of which is the Show! Item, which allows you to view the metadata tables in their "raw" form

The main window of ILDASM is shown in Figure 3–5.

Figure 05Figure 3–5 The main window of ILDASM.


John Robbins has an excellent article in the May, 2001 edition of MSDN magazine that will explain more than you probably want to know about ILDASM and MSIL. See his bugslayer column in that issue.

To view the manifest for the multifile assembly, execute the following command at a Visual Studio .NET command prompt:

ildasm multifile.dll

The manifest of the multifile assembly follows. I have cleaned up the code a little for clarity.

.assembly extern mscorlib
{
 .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
 .hash = (6D A2 04 14 10 73 D7 37 A6 59 AB 
          4A 1B F2 DF 1E AD 04 EC 1D ) 
 .ver 1:0:3300:0
}
.assembly multifile
{
 .custom instance void 
[mscorlib]System.Reflection.AssemblyKeyFileAttribute::.ctor(string) = 
( 01 00 09 6D 79 6B 65 79 2E 73 6E 6B 00 00 )    // ...mykey.snk..
 .custom instance void 
[mscorlib]System.Reflection.AssemblyDelaySignAttribute::.ctor(bool) = 
( 01 00 00 00 00 ) 
 .custom instance void 
[mscorlib]System.Reflection.AssemblyDescriptionAttribute::.ctor(string) = 
( )  // A test multi-file assembly..
 .publickey = ( ) 
 .hash algorithm 0x00008004
 .ver 1:2:3:4
}
.file Employee.mod
  .hash = 
(2D 9E 4C 0A FF 28 9C AA 9D AA 25 66 AA DD C5 18 B5 3F 93 67 ) 
.file Manager.mod
  .hash = 
(E1 14 BA FB 2F 43 B3 FB 86 FB A8 E0 FF 35 E4 EF E0 34 83 FB )
.class extern public AssemblyDemo.TerminationReason
{
 .file Employee.mod
 .class 0x02000002
}
.class extern public AssemblyDemo.Employee
{
 .file Employee.mod
 .class 0x02000003
}
.class extern public AssemblyDemo.Manager
{
 .file Manager.mod
 .class 0x02000002
}
.module multifile.dll
// MVID: {8F84316B-55AC-475D-A0E3-BA1C6941B5BA}
.imagebase 0x00400000
.subsystem 0x00000003
.file alignment 512
.corflags 0x00000009
// Image base: 0x031a0000

The first section of the manifest contains the assembly extern entries that contain information about the only other assemblies that this assembly is dependent on.

.assembly extern mscorlib
{
 .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
 .hash = (6D A2 04 14 10 73 D7 37 A6 59 AB 4A 1B F2 DF 1E AD 04 EC 1D ) 
 .ver 1:0:3300:0
}

This assembly is only dependent on the core system library (mscorlib). If this assembly were dependent on other assemblies besides mscorlib, you would see one entry similar to this for each assembly on which the assembly depends. Notice that the entry contains the public key of the referenced assembly, the hash of the assembly, and the version number of the assembly. The public key and the hash value of the referenced assembly will only be there if the referenced assembly was signed with a strong name. The C# compiler (or any other managed code compiler) will insert the public key token and hash of the referenced assembly into the metadata when you build the assembly.

NOTE

The public key token is a hash of the public key. The token is used in the metadata instead of the public key to conserve space.

When the CLR loads an assembly, it will use the public key token to unsign the digital signature in the referenced assembly. The digital signature in the referenced assembly contains the hash of the contents of the assembly, encrypted with the assembly creator's private key. Therefore, unsigning the assembly will yield the hash of the assembly. The CLR will then compare the unsigned hash from the assembly to the hash in the assembly extern entry, and, if they are the same, it knows that the assembly that it is loading is the same as the assembly that the client was built against (or at least the creator has the same private key). It also knows that the assembly was not tampered with because the hash values match.

The next section of the manifest contains the assembly attributes for the multifile assembly:

.assembly multifile
{
 .custom instance void 
[mscorlib]System.Reflection.AssemblyKeyFileAttribute::.ctor(string) = 
( 01 00 09 6D 79 6B 65 79 2E 73 6E 6B 00 00 )    // ...mykey.snk..
 .custom instance void 
[mscorlib]System.Reflection.AssemblyDelaySignAttribute::.ctor(bool) = 
( 01 00 00 00 00 ) 
 .custom instance void 
[mscorlib]System.Reflection.AssemblyDescriptionAttribute::.ctor(string) = 
( )  // A test multi-file assembly..
 .publickey = ( ) 
 .hash algorithm 0x00008004
 .ver 1:2:3:4
}

You can clearly see the information that was contained in the assembly info file. In this case, I had specified a key file called mykey.snk. The hex numbers are just an encoded version of the string "mykey.snk". I also used the DelaySign attribute and specified false. The hex numbers are just the encoded false Boolean. The AssemblyDescription attribute was quite long, so I elided it. The next few lines show the hex encoding of the public key, which I also elited because it is long. The next line contains an identifier for the hash algorithm that was used to generate the hash values that are stored in the assembly:

.hash algorithm 0x00008004

The final line shows the version number that I specified in the assembly info file with the AssemblyVersion attribute:

.ver 1:2:3:4

The next section of the manifest contains an entry for each module in the assembly that is separate from the file that contains the manifest. The following metadata is the entry for the Employee.mod module.

.file Employee.mod
  .hash = 
(2D 9E 4C 0A FF 28 9C AA 9D AA 25 66 AA DD C5 18 B5 3F 93 67 )

Each of these entries lists the name of the file, without the path, and the hash of the file.

The next section of the manifest contains location information (an index) for all types declared in an external file, that is, a file other than the file that contains the manifest.

.class extern public AssemblyDemo.Employee
{
 .file Employee.mod
 .class 0x02000003
}

Types declared in the same file as the manifest don't require one of these entries because the CLR does not have to load another file in order to find the metadata or implementation for the class. Each of these extern entries contains the name of the file where the class is implemented and the class token, which is 0x2000003 in this case. The entry shown above is for the AssemblyDemo.Employee class; it informs the CLR that this class resides in a module called Employee.mod.

The last section of the manifest contains information that is used by the CLR loader when it loads the assembly into memory and initializes it. This section contains a Module Version Identifier (MVID) and other information as shown here:

.module multifile.dll
// MVID: {8F84316B-55AC-475D-A0E3-BA1C6941B5BA}
.imagebase 0x00400000
.subsystem 0x00000003
.file alignment 512
.corflags 0x00000009
// Image base: 0x031a0000

An MVID is a unique identifier (a GUID) that is associated with each compilation of a module. The CLR uses the MVID when you precompile code, which is just .NET Framework-speak for compiling the MSIL in an assembly into native code ahead of time and caching it before it runs. The usual practice with the .NET Framework is that the CLR will compile code as it executes the code. If you do precompile, the CLR will store the MVID of the MSIL code that it compiles with the native code. Whenever you run the native code, the CLR loader will compare the MVID of the cached native code with the MVID of the MSIL code that the native code was generated from. If they are different, the source assembly has been recompiled, which means that the cached native code will need to be regenerated. The CLR will then revert to the MSIL assembly instead of using the out-of-date native code. The subsystem, imagebase, file alignment, and corflags entries are information used by the CLR to lay out the module in memory after it loads it.

NOTE

You can find out more about precompiling, which is also called pre-JITing, by looking up the Native Image Generator (Ngen.exe) in the .NET Framework tools help.

Although it is vitally important, the manifest is only a small part of the metadata in a .NET assembly. The bulk of the metadata consists of tables that contain descriptive information about every module, type, method, field, property, event, enumeration, and delegate in an assembly. This metadata is spread out through all the modules of an assembly. Each module contains the metadata for the types in that module. This metadata consists of definition or Def metadata tables that describe the types, methods, fields, parameters, and so forth declared in an assembly and reference or Ref metadata tables that describe the external assemblies, modules, types, and members used by an assembly.

There are seven definition (def) metadata tables that describe modules, types, methods, fields, parameters, and events defined in the current assembly:

  • ModuleDef—Contains the name and a unique identifier (a GUID) for each module in the assembly.

  • TypeDef—Contains the name, base type, accessibility (private, public, and so forth) of each type in the assembly. Each entry contains a pointer to the MethodDef, FieldDef, PropertyDef, and Event entries for each method, field, property, or event that is a member of the type.

  • MethodDef—Contains the name, flags (public, private, virtual, abstract, final, and so forth) for each method in the assembly as well as the offset within the assembly where the MSIL code for the method can be found. Each method also contains tokens pointing to entries in the ParamDef table for each parameter of the method.

  • FieldDef—Contains the name, type, and flags (public, private, and so forth) for each field defined in the assembly. A field is just a member variable of a class or structure.

  • ParamDef—Contains the name and flags (in, out, retval, and so forth) for each method parameter defined in the assembly.

  • PropertyDef—Contains the name, flags, type, and underlying field (if used) for each property defined in the assembly.

  • EventDef—Contains the name and flags for each event defined in the assembly. Events in .NET are analogous to connection point events in COM.

There are four main reference (Ref) metadata tables that contain references to "def" entries:

  • AssemblyRef—Contains the name (without path and extension), version number, public key token, and hash for each assembly that the current assembly references.

  • ModuleRef—Contains the file name and extension (without path) for each module that comprises the assembly.

  • TypeRef—Contains the name and location information (either an AssemblyRef token or a ModuleRef token) for each type referenced by the current module.

  • MemberRef—Contains the name, signature, and a TypeRef for each member (field, method, property, and event) referenced by the current assembly.

These Reference metadata tables are one of the main improvements that .NET metadata has over COM type libraries. Type libraries provide fairly detailed information about the type defined within a COM server. However, they do not provide any information about other COM servers or types that the COM server is dependent upon. Reference metadata does that in the .NET Framework.

The metadata for each element (type, method, field, and so forth) resides in a single place—in the module where its enclosing type is implemented. Each element has a token associated with it, which is just an index into the table for that element. The token is used whenever the element is referenced from some other place in the assembly. For instance, earlier I looked at the .extern entries for each class that appears in the manifest. The entry for the Employee class, which is implemented in a module called Employee.mod is as follows:

.class extern public AssemblyDemo.Employee
{
 .file Employee.mod
 .class 0x02000003
}

The value 0x02000003 is the token for the Employee class. With the module name and the token, the CLR can perform a very fast lookup of the metadata for the class. It can go straight to the file where the class is implemented and then do an index lookup into the metadata tables to find the metadata for the class.

Let's look at these metadata tables in the multifile assembly. To do that, I'll use the MSIL Disassembler (ildasm) with the (undocumented) advanced option. Execute the following command at a Visual Studio .NET command prompt:

ildasm /adv manager.mod

After ildasm appears, select View | MetaInfo/Show!. The following text shows the metadata for the Manager class:

TypeDef #3
-------------------------------------------------------
TypDefName: metadatademo.Manager (02000004)
Flags   : [Public] [AutoLayout] [Class] [AnsiClass] (00100001)
Extends  : 02000003 [TypeDef] metadatademo.Employee
Field #1
-------------------------------------------------------
  Field Name: mBonus (04000008)
  Flags   : [Private] (00000001)
  CallCnvntn: [FIELD]
  Field type: ValueClass System.Decimal

Method #1 
-------------------------------------------------------
  MethodName: .ctor (06000009)
  Flags   : [Private] [HideBySig] [ReuseSlot] 
            [SpecialName] [RTSpecialName] 
            [.ctor] (00001881)
  RVA    : 0x00002108
  ImplFlags : [IL] [Managed] (00000000)
  CallCnvntn: [DEFAULT]
  hasThis 
  ReturnType: Void
  4 Arguments
   Argument #1: I4
   Argument #2: String
   Argument #3: ValueClass System.Decimal
   Argument #4: ValueClass System.Decimal
  4 Parameters
   (1) ParamToken : (0800000a) Name : id flags: [none] (00000000)
   (2) ParamToken : (0800000b) Name : name flags: [none] (00000000)
   (3) ParamToken : (0800000c) Name : salary flags: [none] (00000000)
   (4) ParamToken : (0800000d) Name : bonus flags: [none] (00000000)

Method #2 
-------------------------------------------------------
  MethodName: GetSalary (0600000A)
  Flags   : [Public] [Virtual] [HideBySig] [ReuseSlot] (000000c6)
  RVA    : 0x00002128
  ImplFlags : [IL] [Managed] (00000000)
  CallCnvntn: [DEFAULT]
  hasThis 
  ReturnType: ValueClass System.Decimal
  No arguments.

The first section of the metadata, which follows, contains the token for the Manager class, which is just its address in the TypeDef table (02000004). The flags for the class show that it is public, that the CLR should lay out the class in memory as it sees fit (AutoLayout), that the type is a Class (all types whether they are structures or enumerations show "Class" actually), and that strings should be interpreted using Ansi conventions (AnsiClass). The extends entry shows that the Manager class inherits from the class with token 02000003, which is the Employee class.

TypDefName: metadatademo.Manager (02000004) 
Flags   : [Public] [AutoLayout] [Class] [AnsiClass] (00100001)
   Extends  : 02000003 [TypeDef] metadatademo.Employee

The next section of the metadata is for the mBonus field in the Manager class:

  Field Name: mBonus (04000008)
  Flags   : [Private] (00000001)
  CallCnvntn: [FIELD]
  Field type: ValueClass System.Decimal

Notice that this section of the metadata contains the token for the mBonus field (04000008). The flags for this field indicate that it is private and the type of the field is System.Decimal.

The next section of code contains the metadata for the Manager class' constructor:

MethodName: .ctor (06000009)
Flags   : [Private] [HideBySig] [ReuseSlot] [SpecialName] 
          [RTSpecialName] [.ctor] (00001881)
RVA    : 0x00002108
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis ReturnType: Void
4 Arguments
  Argument #1: I4
  Argument #2: String
  Argument #3: ValueClass System.Decimal
  Argument #4: ValueClass System.Decimal
4 Parameters
  (1) ParamToken : (0800000a) Name : id flags: [none] (00000000)
  (2) ParamToken : (0800000b) Name : name flags: [none] (00000000)
  (3) ParamToken : (0800000c) Name : salary flags: [none] (00000000)
  (4) ParamToken : (0800000d) Name : bonus flags: [none] (00000000)

The token for the constructor (its address in the MethodDef table) is 06000009. The location in memory of the MSIL that implements this method, which is called a Relevant Virtual Address (RVA), is 0x00002108. The tokens for the id, name, salary, and bonus parameters, which are the addresses of these parameters in the ParamDef table, are 0800000a, 0800000b, 0800000c and 0800000d, respectively. Notice that the metadata lists the type and name for each of these parameters.

Viewing Metadata Programmatically

You can use two APIs to query and use .NET metadata from your programs. There is an unmanaged API, which is implemented as a set of COM interfaces. The only documentation I have seen on this API is a document called Assembly Metadata Unmanaged API.doc and a file called Metadata Unmanaged API.doc, which you can find in the Tool Developer's guide area of the documentation at [Root directory for Visual Studio.NET installation]\FrameworkSDK\Tool Developers Guide\docs. The documentation in this directory is for compiler vendors or people implementing the Common Language Infrastructure (CLI). There is also an excellent article written by Matt Pietrek in the October, 2000 edition of MSDN magazine. A detailed discussion of this interface is outside the scope of this book. The other API is a managed API called the reflection API. This API is very easy to use. There are two main entry points to this API. One is the GetType method of the Object class in the System Namespace, which is the base type for all other types that you declare in the .NET Framework. The GetType method returns a Type object. The Type object contains a set of methods that you can use to query the metadata associated with a particular type. Another entry point is to use the static Load method in the Assembly class, which can be found in

the System.Reflection namespace. The Assembly class contains methods that you can use to query and view the modules and types in an assembly. The demonstrations for this chapter contain a simple application called the AssemblyViewer that uses the Reflection API to display the metadata for an assembly. To use this application, you type in the name of an assembly. The application will then locate the assembly and display a list of all the types in the assembly. You can then double-click on a type, and it will show a list of the members of the type. I won't show the code for this example here in the text of this chapter, but you can download the code for this book and view the code for yourself at your leisure and see how easy it is to query and use .NET metadata.

  • + Share This
  • 🔖 Save To Your Account

Related Resources