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

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

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

622 CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

You are building COM interoperability assemblies and wish to account for some IDL attributes that have been lost during the conversion process (such as the COM [helpstring] attribute).

To illustrate the process of round-tripping, begin by creating a new C# code file (HelloProgram.cs) using a simple text editor, and define the following class type (you are free to use Visual Studio 2008’s Console Application project if you wish; however, be sure to delete the AssemblyInfo.cs file to decrease the amount of generated CIL code):

// A simple C# console app. using System;

class Program

{

static void Main(string[] args)

{

Console.WriteLine("Hello CIL code!"); Console.ReadLine();

}

}

Save your file to a convenient location (for example, C:\HelloCilCode) and compile your program using csc.exe:

csc HelloProgram.cs

Now, open HelloProgram.exe with ildasm.exe and, using the File Dump menu option, save the raw CIL code to a new *.il file (HelloProgram.il) in the same folder containing your compiled assembly (all of the default values of the resulting dialog box are fine as is).

Note ildasm.exe will also generate a *.res file when dumping the contents of an assembly to file. These resource files can be ignored (and deleted) throughout this chapter, as we will not be making use of them.

Now you are able to view this file using your text editor of choice. Here is the (slightly reformatted and annotated) result:

//Referenced Assemblies.

.assembly extern mscorlib

{

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

.ver 2:0:0:0

}

//Our assembly.

.assembly HelloProgram

{

.hash algorithm 0x00008004

.ver 0:0:0:0

}

.module HelloProgram.exe

.imagebase 0x00400000

.file alignment 0x00000200

.stackreserve 0x00100000

.subsystem 0x0003

.corflags 0x00000001

CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

623

// Definition of Program class.

.class private auto ansi beforefieldinit Program extends [mscorlib]System.Object

{

.method private hidebysig static void Main(string[] args) cil managed

{

//Marks this method as the entry point of the

//executable.

.entrypoint

.maxstack 8 IL_0000: nop

IL_0001: ldstr "Hello CIL code!"

IL_0006: call void [mscorlib]System.Console::WriteLine(string) IL_000b: nop

IL_000c: call string [mscorlib]System.Console::ReadLine() IL_0011: pop

IL_0012: ret

}

// The default constructor.

.method public hidebysig specialname rtspecialname instance void .ctor() cil managed

{

.maxstack 8 IL_0000: ldarg.0

IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: ret

}

}

First, notice that the *.il file opens by declaring each externally referenced assembly the current assembly is compiled against. Here, you can see a single .assembly extern token set for the always present mscorlib.dll. Of course, if your class library made use of types within other referenced assemblies, you would find additional .assembly extern directives.

Next, you find the formal definition of your HelloProgram.exe assembly, which has been assigned a default version of 0.0.0.0 (given that you did not specify a value using the [AssemblyVersion] attribute). The assembly is further described using various CIL directives (such as .module, .imagebase, and so forth).

After documenting the externally referenced assemblies and defining the current assembly, you find a definition of the Program type. Note that the .class directive has various attributes (many of which are actually optional) such as extends, which marks the base class of the type:

.class private auto ansi beforefieldinit Program extends [mscorlib]System.Object

{ ... }

The bulk of the CIL code represents the implementation of the class’s default constructor and the Main() method, both of which are defined (in part) with the .method directive. Once the members have been defined using the correct directives and attributes, they are implemented using various opcodes.

It is critical to understand that when interacting with .NET types (such as System.Console) in CIL, you will always need to use the type’s fully qualified name. Furthermore, the type’s fully qualified name must always be prefixed with the friendly name of the defining assembly (in square brackets). Consider the CIL implementation of Main():

624CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

.method private hidebysig static void Main(string[] args) cil managed

{

.entrypoint

.maxstack 8 IL_0000: nop

IL_0001: ldstr "Hello CIL code!"

IL_0006: call void [mscorlib]System.Console::WriteLine(string)

IL_000b: nop

IL_000c: call string [mscorlib]System.Console::ReadLine()

IL_0011: pop IL_0012: ret

}

The implementation of the default constructor in terms of CIL code makes use of yet another “load-centric” instruction (ldarg.0). In this case, the value loaded onto the stack is not a custom variable specified by us, but the current object reference (more details on this later). Also note that the default constructor explicitly makes a call to the base class constructor:

.method public hidebysig specialname rtspecialname instance void .ctor() cil managed

{

.maxstack 8 IL_0000: ldarg.0

IL_0001: call instance void [mscorlib]System.Object::.ctor() IL_0006: ret

}

The Role of CIL Code Labels

One thing you certainly have noticed is that each line of implementation code is prefixed with a token of the form IL_XXX: (e.g., IL_0000:, IL_0001:, and so on). These tokens are called code labels and may be named in any manner you choose (provided they are not duplicated within the same member scope). When you dump an assembly to file using ildasm.exe, it will automatically generate code labels that follow an IL_XXX: naming convention. However, you may change them to reflect a more descriptive marker:

.method private hidebysig static void Main(string[] args) cil managed

{

.entrypoint

.maxstack 8

Nothing_1: nop

Load_String: ldstr "Hello CIL code!"

PrintToConsole: call void [mscorlib]System.Console::WriteLine(string) Nothing_2: nop

WaitFor_KeyPress: call string [mscorlib]System.Console::ReadLine() RemoveValueFromStack: pop

Leave_Function: ret

}

The truth of the matter is that most code labels are completely optional. The only time code labels are truly mandatory is when you are authoring CIL code that makes use of various branching or looping constructs, as you can specify where to direct the flow of logic via these code labels. For our current example, you can remove these autogenerated labels altogether with no ill effect:

.method private hidebysig static void Main(string[] args) cil managed

{

.entrypoint

CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

625

.maxstack 8 nop

ldstr "Hello CIL code!"

call void [mscorlib]System.Console::WriteLine(string) nop

call string [mscorlib]System.Console::ReadLine() pop

ret

}

Interacting with CIL: Modifying an *.il File

Now that you have a better understanding of how a basic CIL file is composed, let’s complete our round-tripping experiment. The goal here is to update the CIL within the existing *.il file as follows:

Add a reference to the System.Windows.Forms.dll assembly.

Load a local string within Main().

Call the System.Windows.Forms.MessageBox.Show() method using the local string variable as an argument.

The first step is to add a new .assembly directive (qualified with the extern attribute) that specifies your assembly requires the System.Windows.Forms.dll assembly. To do so, update the *.il file with the following logic after the external reference to mscorlib:

.assembly extern System.Windows.Forms

{

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

.ver 2:0:0:0

}

Be aware that the value assigned to the .ver directive may differ depending on which version of the .NET platform you have installed on your development machine. Here, you see that System.Windows.Forms.dll version 2.0.0.0 is used and has the public key token of B77A5C561934E089. If you open the GAC (see Chapter 15) and locate your version of the System. Windows.Forms.dll assembly, you can simply copy the correct version and public key token value through the assembly’s Properties page (via a right-click of your mouse).

Next, you need to alter the current implementation of the Main() method. Locate this method within the *.il file and remove the current implementation code (the .maxstack and .entrypoint directives should remain intact; I’ll describe them later):

.method private hidebysig static void Main(string[] args) cil managed

{

.entrypoint

.maxstack 8

// ToDo: Write new CIL code!

}

Again, the goal is to push a new string onto the stack and call the MessageBox.Show() method (rather than the Console.WriteLine() method). Recall that when you specify the name of an external type, you must make use of the type’s fully qualified name (in conjunction with the friendly name of the assembly). Keeping this in mind, update the Main() method as follows:

.method private hidebysig static void Main(string[] args) cil managed

{

.entrypoint

626 CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

.maxstack 8

ldstr "CIL is way cool"

call valuetype [System.Windows.Forms] System.Windows.Forms.DialogResult [System.Windows.Forms] System.Windows.Forms.MessageBox::Show(string)

pop ret

}

In effect, you have just updated the CIL code to correspond to the following C# class definition:

class Program

{

static void Main(string[] args)

{

System.Windows.Forms.MessageBox.Show("CIL is way cool");

}

}

Compiling CIL Code Using ilasm.exe

Assuming you have saved this modified *.il file, you can compile a new .NET assembly using the ilasm.exe (CIL compiler) utility. While the CIL compiler has numerous command-line options (all of which can be seen by specifying the -? option), Table 19-1 shows the core flags of interest.

Table 19-1. Common ilasm.exe Command-Line Flags

Flag

Meaning in Life

/debug

Includes debug information (such as local variable and argument names, as

 

well as line numbers).

/dll

Produces a *.dll file as output.

/exe

Produces an *.exe file as output. This is the default setting and may be omitted.

/key

Compiles the assembly with a strong name using a given *.snk file.

/noautoinherit

Prevents class types from automatically inheriting from System.Object when a

 

specific base class is not defined.

/output

Specifies the output file name and extension. If you do not make use of the

 

/output flag, the resulting file name (minus the file extension) is the same as

 

the name of the first source file.

 

 

To compile your updated HelloProgram.il file into a new .NET *.exe, you can issue the following command within a Visual Studio 2008 command prompt:

ilasm /exe HelloProgram.il /output=NewAssembly.exe

Assuming things have worked successfully, you will see the report shown in Figure 19-1.

CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

627

Figure 19-1. Compiling *.il files using ilasm.exe

At this point, you can run your new application. Sure enough, rather than showing a message within the console window, you will now see a message box displaying your message (see

Figure 19-2).

Figure 19-2. The result of the round-trip

Compiling CIL Code Using SharpDevelop

When working with *.il files, you may wish to make use of the freely available SharpDevelop IDE (see Chapter 2). When you create a new solution (via the File New Solution menu option), one of your choices is to create a CIL project workspace (see Figure 19-3).

628 CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

Figure 19-3. The SharpDevelop CIL project template

While SharpDevelop does not have IntelliSense support for CIL projects, CIL tokens are colorcoded, and you are able to compile and run your application directly within the IDE (rather than running ilasm.exe from a command prompt). Consider Figure 19-4.

Figure 19-4. Authoring CIL code within SharpDevelop

CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

629

The Role of peverify.exe

When you are building or modifying assemblies using CIL code, it is always advisable to verify that the compiled binary image is a well-formed .NET image using the peverify.exe command-line tool:

peverify MyNewAssembly.exe

This tool will examine all opcodes within the specified assembly for valid CIL code. For example, in terms of CIL code, the evaluation stack must always be empty before exiting a function. If you forget to pop off any remaining values, the ilasm.exe compiler will still generate a valid assembly (given that compilers are concerned only with syntax). peverify.exe, on the other hand, is concerned with semantics. If you did forget to clear the stack before exiting a given function, peverify.exe will let you know before you try running your code base.

Source Code The HelloCilCode example is included under the Chapter 19 subdirectory.

Understanding CIL Directives and Attributes

Now that you have seen how ildasm.exe and ilasm.exe can be used to perform a round-trip, we can get down to the business of checking out the syntax and semantics of CIL itself. The next sections will walk you through the process of authoring a custom namespace containing a set of types. However, to keep things simple, these types will not contain any implementation logic for their members. Once you understand how to create empty types, you can then turn your attention to the process of providing “real” members using CIL opcodes.

Specifying Externally Referenced Assemblies in CIL

Create a new file named CilTypes.il using your editor of choice. The first task a CIL project will require is to list the set of external assemblies used by the current assembly. For this example, you will only make use of types found within mscorlib.dll. To do so, the .assembly directive will be qualified using the external attribute. When you are referencing a strongly named assembly, such as mscorlib.dll, you’ll want to specify the .publickeytoken and .ver directives as well:

.assembly extern mscorlib

{

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

.ver 2:0:0:0

}

Note Strictly speaking, you are not required to explicitly reference mscorlib.dll as an external reference, as ilasm.exe will do so automatically.

630 CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

Defining the Current Assembly in CIL

The next order of business is to define the assembly you are interested in building using the

.assembly directive. At the simplest level, an assembly can be defined by specifying the friendly name of the binary:

// Our assembly.

.assembly CILTypes { }

While this indeed defines a new .NET assembly, you will typically place additional directives within the scope of the assembly declaration. For this example, update your assembly definition to include a version number of 1.0.0.0 using the .ver directive (note that each numerical identifier is separated by colons, not the C#-centric dot notation):

// Our assembly.

.assembly CILTypes

{

.ver 1:0:0:0

}

Given that the CILTypes assembly is a single-file assembly, you will finish up the assembly definition using a single .module directive, which marks the official name of your .NET binary,

CILTypes.dll:

.assembly CILTypes

{

.ver 1:0:0:0

}

// The module of our single-file assembly.

.module CILTypes.dll

In addition to .assembly and .module are CIL directives that further qualify the overall structure of the .NET binary you are composing. Table 19-2 lists a few of the more common assembly-level directives.

Table 19-2. Additional Assembly-Centric Directives

Directive

Meaning in Life

.mresources If your assembly makes use of internal resources (such as bitmaps or string tables), this directive is used to identify the name of the file that contains the resources to be embedded.

.subsystem This CIL directive is used to establish the preferred UI that the assembly wishes to execute within. For example, a value of 2 signifies that the assembly should run within a Forms-based GUI, whereas a value of 3 denotes a console executable.

Defining Namespaces in CIL

Now that you have defined the look and feel of your assembly (and the required external references), you can create a .NET namespace (MyNamespace) using the .namespace directive:

// Our assembly has a single namespace.

.namespace MyNamespace {}

Like C#, CIL namespace definitions can be nested within further namespaces. We have no need to define a root namespace here; however, for the sake of argument, assume you wish to create a root namespace named Intertech:

CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

631

.namespace Intertech

{

.namespace MyNamespace {}

}

Like C#, CIL allows you to define a nested namespace as follows:

// Defining a nested namespace.

.namespace Intertech.MyNamespace{}

Defining Class Types in CIL

Empty namespaces are not very interesting, so let’s now check out the process of defining a class type using CIL. Not surprisingly, the .class directive is used to define a new class type. However, this simple directive can be adorned with numerous additional attributes, to further qualify the nature of the type. To illustrate, add a public class to your namespace named MyBaseClass. As in C#, if you do not specify an explicit base class, your type will automatically be derived from System. Object, unless you compile the *.il code by specifying the /noautoinherit option of ilasm.exe.

.namespace MyNamespace

{

// System.Object base class assumed.

.class public MyBaseClass {}

}

When you are building a class type that derives from any class other than System.Object, you make use of the extends attribute. Whenever you need to reference a type defined within the same assembly, CIL demands that you also make use of the fully qualified name (however, if the base type is within the same assembly, you can omit the assembly’s friendly name prefix). Therefore, the following attempt to extend MyBaseClass results in a compiler error:

// This will not compile!

.namespace MyNamespace

{

.class public MyBaseClass {}

.class public MyDerivedClass extends MyBaseClass {}

}

To correctly define the parent class of MyDerivedClass, you must specify the full name of

MyBaseClass as follows:

// Better!

.namespace MyNamespace

{

.class public MyBaseClass {}

.class public MyDerivedClass extends MyNamespace.MyBaseClass {}

}

In addition to the public and extends attributes, a CIL class definition may take numerous additional qualifiers that control the type’s visibility, field layout, and so on. Table 19-3 illustrates some (but not all) of the attributes that may be used in conjunction with the .class directive.