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

Pro CSharp And The .NET 2.0 Platform (2005) [eng]

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

484 C H A P T E R 1 5 U N D E R S TA N D I N G C I L A N D T H E R O L E O F DY N A M I C A S S E M B L I E S

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 so:

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 you are using System.Windows.Forms.dll. To do so, simply 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 11) and locate your version of the System.Windows.Forms.dll assembly, you can simply copy the correct version and public key token value via the assembly’s Properties page.

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:

C H A P T E R 1 5 U N D E R S TA N D I N G C I L A N D T H E R O L E O F DY N A M I C A S S E M B L I E S

485

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

{

.entrypoint

.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:

public 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. Perhaps surprisingly, the CIL compiler has far fewer command-line flags than the C# compiler. Table 15-1 shows the core flags of interest.

Table 15-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 is the same as the name of the first source file.

 

 

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

ilasm /exe HelloProgram.il

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

486 C H A P T E R 1 5 U N D E R S TA N D I N G C I L A N D T H E R O L E O F DY N A M I C A S S E M B L I E S

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

At this point, you can run your new application. Sure enough, rather than pumping a message to the Console window, you will now see a message box displaying your message (see Figure 15-2).

Figure 15-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 combine (via the File New Combine menu option), one of your choices is to create a CIL project workspace. While SharpDevelop does not have IntelliSense support for CIL projects, CIL tokens are color-coded, and you are able to compile and run your application directly within the IDE (rather than running ilasm.exe from a command prompt).

Compiling CIL Code Using ILIDE#

If you’re truly interested in experimenting with the CIL programming language, I also recommend downloading the latest version of a free open source CIL editor named ILIDE#. This tool, like SharpDevelop, provides color-coding, ilasm.exe integration, and various related tools. Unlike SharpDevelop, the latest version of ILIDE# now supports CIL IntelliSense! You can download ILIDE# from http://ilide.aspfreeserver.com/default-en.aspx (of course, this URL is subject to change). Figure 15-3 shows ILIDE# in action.

C H A P T E R 1 5 U N D E R S TA N D I N G C I L A N D T H E R O L E O F DY N A M I C A S S E M B L I E S

487

Figure 15-3. ILIDE# is a free CIL IDE.

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 HelloProgram.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.

Source Code The HelloProgram.il file is included under the Chapter 15 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.

488 C H A P T E R 1 5 U N D E R S TA N D I N G C I L A N D T H E R O L E O F DY N A M I C A S S E M B L I E S

Specifying Externally Referenced Assemblies in CIL

Create a new file named CilTypes.il using your editor of choice. First, you need 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.

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 15-2 lists a few of the more common assembly-level directives.

C H A P T E R 1 5 U N D E R S TA N D I N G C I L A N D T H E R O L E O F DY N A M I C A S S E M B L I E S

489

Table 15-2. Additional Assembly-Centric Directives

Directive

Meaning in Life

.mresources

If your assembly makes use of internal resource (such as bitmaps or string

 

tables), this directive is used to identify the name of the file that contains the

 

resources to be embedded. Chapter 20 examines .NET resources in detail.

.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 application.

 

 

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 an outer namespace. For the sake of argument, assume you wish to create a root namespace named IntertechTraining:

.namespace IntertechTraining

{

.namespace MyNamespace {}

}

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

// Defining a nested namespace.

.namespace IntertechTraining.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 simple public class named MyBaseClass. As in C#, if you do not specify an explicit base class, your type will automatically be derived from System.Object:

.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 {}

}

490 C H A P T E R 1 5 U N D E R S TA N D I N G C I L A N D T H E R O L E O F DY N A M I C A S S E M B L I E S

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

MyBaseClass as so:

// 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 15-3 illustrates some (but not all) of the attributes that may be used in conjunction with the .class directive.

Table 15-3. Various Attributes Used in Conjunction with the .class Directive

Attributes

Meaning in Life

public, private, nested assembly, nested famandassem, nested family, nested famorassem, nested public, nested private

CIL defines various attributes that are used to specify the visibility of a given type. As you can see, raw CIL offers numerous possibilities other than those offered by C#.

abstract

These two attributes may be tacked onto a .class directive

sealed

to define an abstract class or sealed class, respectively.

auto

These attributes are used to instruct the CLR how to lay

sequential

out field data in memory. For class types, the default layout

explicit

flag (auto) is appropriate.

extends

These attributes allow you to define the base class of a type

implements

(via extends) or implement an interface on a type (via

 

implements).

 

 

Defining and Implementing Interfaces in CIL

As odd as it may seem, interface types are defined in CIL using the .class directive. However, when the .class directive is adorned with the interface attribute, the type is realized as a CTS interface type. Once an interface has been defined, it may be bound to a class or structure type using the CIL implements attribute:

.namespace MyNamespace

{

// An interface definition.

.class public interface IMyInterface {}

.class public MyBaseClass {}

// DerivedTestClass now implements IAmAnInterface.

.class public MyDerivedClass extends MyNamespace.MyBaseClass

implements MyNamespace.IMyInterface {}

}

As you recall from Chapter 7, interfaces can function as the base interface to other interface types in order to build interface hierarchies. However, contrary to what you might be thinking, the extends attribute cannot be used to derive interface A from interface B. The extends attribute is used only to qualify a type’s base class. When you wish to extend an interface, you will make use of the implements attribute yet again:

C H A P T E R 1 5 U N D E R S TA N D I N G C I L A N D T H E R O L E O F DY N A M I C A S S E M B L I E S

491

// Extending interfaces in terms of CIL.

.class public interface IMyInterface {}

.class public interface IMyOtherInterface implements MyNamespace.IMyInterface {}

Defining Structures in CIL

The .class directive can be used to define a CTS structure if the type extends System.ValueType. As well, the .class directive is qualified with the sealed attribute (given that structures can never be a base structure to other value types). If you attempt to do otherwise, ilasm.exe will issue a compiler error.

// A structure definition is always sealed.

.class public sealed MyStruct

extends [mscorlib]System.ValueType{}

Do be aware that CIL provides a shorthand notation to define a structure type. If you use the value attribute, the new type will derive the type from [mscorlib]System.ValueType and be marked as sealed automatically. Therefore, you could define MyStruct as so:

// Shorthand notation for declaring a structure.

.class public value MyStruct{}

Defining Enums in CIL

.NET enumerations (as you recall) derive from System.Enum, which is a System.ValueType (and therefore must also be sealed). When you wish to define an enum in terms of CIL, simply extend

[mscorlib]System.Enum:

// An enum.

.class public sealed MyEnum

extends [mscorlib]System.Enum{}

Like a structure definition, enumerations can be defined with a shorthand notation using the enum attribute:

// Enum shorthand.

.class public enum MyEnum{}

Note The other fundamental .NET type, the delegate, also has a specific CIL representation. See Chapter 8 for full details.

Compiling the CILTypes.il file

Even though you have not yet added any members or implementation code to the types you have defined, you are able to compile this *.il file into a .NET DLL assembly (which you must do, as you have not specified a Main() method). Open up a command prompt and enter the following command to ilasm.exe:

ilasm /dll CilTypes.il

Once you have done so, you are able to open your binary into ildasm.exe (see Figure 15-4).

492 C H A P T E R 1 5 U N D E R S TA N D I N G C I L A N D T H E R O L E O F DY N A M I C A S S E M B L I E S

Figure 15-4. The CILTypes.dll assembly

Once you have confirmed the contents of your assembly, run peverify.exe against it. Notice that you are issued a number of errors, given that all your types are completely empty. To understand how to populate a type with content, you first need to examine the fundamental data types of CIL.

Source Code The CilTypes.il file is included under the Chapter 15 subdirectory.

.NET Base Class Library, C#, and CIL Data Type

Mappings

Table 15-4 illustrates how a .NET base class type maps to the corresponding C# keyword, and how each C# keyword maps into raw CIL. As well, Table 15-4 documents the shorthand constant notations used for each CIL type. As you will see in just a moment, these constants are often referenced by numerous CIL opcodes.

Table 15-4. Mapping .NET Base Class Types to C# Keywords, and C# Keywords to CIL

.NET Base Class Type

C# Keyword

CIL Representation

CIL Constant Notation

System.SByte

sbyte

int8

I1

System.Byte

byte

unsigned int8

U1

System.Int16

short

int16

I2

System.UInt16

ushort

unsigned int16

U2

System.Int32

int

int32

I4

System.UInt32

uint

unsigned int32

U4

System.Int64

long

int64

I8

System.UInt64

ulong

unsigned int64

U8

System.Char

char

char

CHAR

System.Single

float

float32

R4

System.Double

double

float64

R8

C H A P T E R 1 5 U N D E R S TA N D I N G C I L A N D T H E R O L E O F DY N A M I C A S S E M B L I E S

493

.NET Base Class Type

C# Keyword

CIL Representation

CIL Constant Notation

System.Boolean

bool

bool

BOOLEAN

System.String

string

string

N/A

System.Object

object

object

N/A

System.Void

void

void

VOID

 

 

 

 

Defining Type Members in CIL

As you are already aware, .NET types may support various members. Enumerations have some set of name/value pairs. Structures and classes may have constructors, fields, methods, properties, static members, and so on. Over the course of this book’s first 14 chapters, you have already seen partial CIL definitions for the items previously mentioned, but nevertheless, here is a quick recap of how various members map to CIL primitives.

Defining Field Data in CIL

Enumerations, structures, and classes can all support field data. In each case, the .field directive will be used. For example, let’s breathe some life into the skeleton MyEnum enumeration and define three name/value pairs (note the values are specified using a parentheses syntax):

.class public auto ansi sealed MyEnum extends [mscorlib]System.Enum

{

.field public static literal valuetype MyNamespace.MyEnum NameOne = int32(0)

.field public static literal valuetype MyNamespace.MyEnum NameTwo = int32(1)

.field public static literal valuetype MyNamespace.MyEnum NameThree = int32(2)

}

Fields that reside within the scope of a .NET System.Enum-derived type are qualified using the static and literal attributes. As you would guess, these attributes set up the field data to be a fixed value accessible from the type itself (e.g., MyEnum.NameOne).

Note The values assigned to an enum value may also be in hexadecimal.

Of course, when you wish to define a point of field data within a class or structure, you are not limited to a point of public static literal data. For example, you could update MyBaseClass to support two points of private, instance-level field data:

.class public MyBaseClass

{

.field private string stringField

.field private int32 intField

}

As in C#, class field data will automatically be assigned to the correct default value. If you wish to allow the object user to supply custom values at the time of creation for each of these points of private field data, you (of course) need to create custom constructors.