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

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

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

504 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

// Call Display() and pass in topmost value on stack. call void [CILCars]

CILCars.CILCarInfo::Display( class [CILCars]CILCars.CILCar)

ret

}

}

}

The one opcode that is important to point out is .entrypoint. Recall from the discussion earlier in this chapter that this opcode is used to mark which method of an *.exe functions as the entry point of the module. In fact, given that .entrypoint is how the CLR identifies the initial method to execute, this method can be called anything at all other than Main(). The remainder of the CIL code found in the Main() method is your basic pushing and popping of stack-based values.

Do note, however, that the creation of CILCar involves the use of the .newobj opcode. On a related note, recall that when you wish to invoke a member of a type using raw CIL, you make use of the doublecolon syntax and, as always, make use of the fully qualified name of the type. With this, you can compile your new file with ilasm.exe, verify your assembly with peverify.exe, and execute your program:

ilasm CilCarClient.il peverify CilCarClient.exe CilCarClient.exe

Figure 15-5 shows the end result.

Figure 15-4. Your CILCar in action

That wraps up the CIL primer and the first goal of this chapter. At this point, I hope you feel confident that you can open a particular .NET assembly using ildasm.exe and gain a better understanding of what exactly is occurring behind the scenes.

Understanding Dynamic Assemblies

As you may have gathered, the process of building a complex .NET application in CIL would be quite the labor of love. On the one hand, CIL is an extremely expressive programming language that allows you to interact with all of the programming constructs allowed by the CTS. On the other hand, authoring raw CIL is tedious, error-prone, and painful. While it is true that knowledge is power, you may indeed wonder just how important it is to commit the laws of CIL syntax to memory. The answer is, “It depends.” To be sure, most of your .NET programming endeavors will not require you to view, edit, or author raw CIL code. However, with the CIL primer behind you, you are now ready investigate the world of dynamic assemblies (as opposed to static assemblies) and the role of the System.Reflection.Emit namespace.

The first question you may have is, “What exactly is the difference between static and dynamic assemblies?” By definition, static assemblies are .NET binaries loaded directly from disk storage, meaning they are located somewhere on your hard drive in a physical file (or possibly a set of files in the case of a multifile assembly) at the time the CLR requests them. As you might guess, every time you compile your C# source code, you end up with a static assembly.

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

505

A dynamic assembly, on the other hand, is created in memory on the fly using the types provided by the System.Reflection.Emit namespace. The System.Reflection.Emit namespace makes it possible to create an assembly and its modules, type definitions, and CIL implementation logic at runtime. Once you have done so, you are then free to save your in-memory binary to disk. This, of course, results in a new static assembly. To be sure, the process of building a dynamic assembly using the System.Reflection.Emit namespace does require some level of understanding regarding the nature of CIL opcodes.

Although creating dynamic assemblies is a fairly advanced (and uncommon) programming task, they can be useful under various circumstances:

You are building a .NET programming tool that needs to generate assemblies on demand based on user input.

You are building a program that needs to generate proxies to remote types on the fly based on the obtained metadata.

You wish to load a static assembly and dynamically insert new types into the binary image.

This being said, let’s check out the types within System.Reflection.Emit.

Exploring the System.Reflection.Emit Namespace

Creating a dynamic assembly requires you to have some familiarity with CIL opcodes, but the types of the System.Reflection.Emit namespace hide the complexity of CIL as much as possible. For example, rather than directly specifying the necessary CIL directives and attributes to define a class type, you can simply make use of the TypeBuilder class. Likewise, if you wish to define a new instance-level constructor, you have no need to emit the specialname, rtspecialname, or .ctor tokens; rather, you can make use of the ConstructorBuilder. Table 15-8 documents the key members of the System. Reflection.Emit namespace.

Table 15-8. Select Members of the System.Reflection.Emit Namespace

Members

Meaning in Life

AssemblyBuilder

Used to create an assembly (*.dll or *.exe) at runtime. *.exes

 

must call the ModuleBuilder.SetEntryPoint() method to set the

 

method that is the entry point to the module. If no entry point is

 

specified, a *.dll will be generated.

ModuleBuilder

Used to define the set of modules within the current assembly.

EnumBuilder

Used to create a .NET enumeration type.

TypeBuilder

May be used to create classes, interfaces, structures, and delegates

 

within a module at runtime.

MethodBuilder

Used to create type members (such as methods, local variables,

EventBuilder

properties, constructors, and attributes) at runtime.

LocalBuilder

 

PropertyBuilder

 

FieldBuilder

 

ConstructorBuilder

 

CustomAttributeBuilder

 

ParameterBuilder

 

ILGenerator

Emits CIL opcodes into a given type member.

OpCodes

Provides numerous fields that map to CIL opcodes. This type is

 

used in conjunction with the various members of System.Reflection.

 

Emit.ILGenerator.

 

 

506 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

In general, the types of the System.Reflection.Emit namespace allow you represent raw CIL tokens programmatically during the construction of your dynamic binary. You will see many of these members in the example that follows; however, the ILGenerator type is worth checking out straightaway.

The Role of the System.Reflection.Emit.ILGenerator

As its name implies, the ILGenerator type’s role is to inject CIL opcodes into a given type member. Typically, you will not need to directly create ILGenerator objects, but rather receive a valid reference to the ILGenerator type using the builder-centric types (such as the MethodBuilder and

ConstructorBuilder types), for example:

//Obtain an ILGenerator from a ConstructorBuilder

//object named 'myCtorBuilder'.

ConstructorBuilder myCtorBuilder =

new ConstructorBuilder(/* ...various args... */); ILGenerator myCILGen = myCtorBuilder.GetILGenerator();

Once you have an ILGenerator in your hands, you are then able to emit the raw CIL opcodes using any number of methods. Table 15-9 documents some (but not all) methods of ILGenerator.

Table 15-9. Select Methods of ILGenerator

Method

Meaning in Life

BeginCatchBlock()

Begins a catch block

BeginExceptionBlock()

Begins an exception block for a nonfiltered exception

BeginFinallyBlock()

Begins a finally block

BeginScope()

Begins a lexical scope

DeclareLocal()

Declares a local variable

DefineLabel()

Declares a new label

Emit()

Is overloaded numerous times to allow you to emit CIL opcodes

EmitCall()

Pushes a call or callvirt opcode into the CIL stream

EmitWriteLine()

Emits a call to Console.WriteLine() with different types of values

EndExceptionBlock()

Ends an exception block

EndScope()

Ends a lexical scope

ThrowException()

Emits an instruction to throw an exception

UsingNamespace()

Specifies the namespace to be used in evaluating locals and watches

 

for the current active lexical scope

 

 

The key method of ILGenerator is Emit(), which works in conjunction with the System.Reflection.Emit.OpCodes class type. As mentioned earlier in this chapter, this type exposes a good number of read-only fields that map to raw CIL opcodes. The full set of these members are all documented within online help, and you will see various examples in the pages that follow.

Emitting a Dynamic Assembly

To illustrate the process of defining a .NET assembly at runtime, let’s walk through the process of creating a single-file dynamic assembly named MyAssembly.dll. Within this module is a class named HelloWorld. The HelloWorld type supports a default constructor and a custom constructor that is used to assign the value of a private member variable (theMessage) of type string. In addition,

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

507

HelloWorld supports a public instance method named SayHello(), which prints a greeting to the standard I/O stream, and another instance method named GetMsg(), which returns the internal private string. In effect, you are going to programmatically generate the following class type:

//This class will be created at runtime

//using System.Reflection.Emit.

public class HelloWorld

{

private string theMessage; HelloWorld() {}

HelloWorld(string s) { theMessage = s;} public string GetMsg() { return theMessage;} public void SayHello()

{

System.Console.WriteLine("Hello from the HelloWorld class!");

}

}

Assume you have created a new Visual Studio 2005 console application project workspace named DynAsmBuilder. Rename your initial class as MyAsmBuilder and define a static method named CreateMyAsm(). This single method is in charge of the following:

Defining the characteristics of the dynamic assembly (name, version, etc.)

Implementing the HelloClass type

Saving the in-memory assembly to a physical file

Also note that the CreateMyAsm() method takes as a single parameter a System.AppDomain type, which will be used to obtain access to the AssemblyBuilder type associated with the current application domain (see Chapter 13 for a discussion of .NET application domains). Here is the complete code, with analysis to follow:

// The caller sends in an AppDomain type.

public static void CreateMyAsm(AppDomain curAppDomain)

{

// Establish general assembly characteristics.

AssemblyName assemblyName = new AssemblyName(); assemblyName.Name = "MyAssembly"; assemblyName.Version = new Version("1.0.0.0");

//Create new assembly within the current AppDomain.

AssemblyBuilder assembly = curAppDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save);

//Given that we are building a single-file

//assembly, the name of the module is the same as the assembly.

ModuleBuilder module =

assembly.DefineDynamicModule("MyAssembly", "MyAssembly.dll");

//Define a public class named "HelloWorld".

TypeBuilder helloWorldClass = module.DefineType("MyAssembly.HelloWorld",

TypeAttributes.Public);

// Define a private String member variable named "theMessage".

FieldBuilder msgField =

helloWorldClass.DefineField("theMessage", Type.GetType("System.String"), FieldAttributes.Private);

508 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

// Create the custom ctor.

Type[] constructorArgs = new Type[1]; constructorArgs[0] = typeof(string); ConstructorBuilder constructor =

helloWorldClass.DefineConstructor(MethodAttributes.Public,

CallingConventions.Standard,

constructorArgs);

ILGenerator constructorIL = constructor.GetILGenerator(); constructorIL.Emit(OpCodes.Ldarg_0);

Type objectClass = typeof(object); ConstructorInfo superConstructor =

objectClass.GetConstructor(new Type[0]); constructorIL.Emit(OpCodes.Call, superConstructor); constructorIL.Emit(OpCodes.Ldarg_0); constructorIL.Emit(OpCodes.Ldarg_1); constructorIL.Emit(OpCodes.Stfld, msgField); constructorIL.Emit(OpCodes.Ret);

//Create the default ctor. helloWorldClass.DefineDefaultConstructor(MethodAttributes.Public);

//Now create the GetMsg() method.

MethodBuilder getMsgMethod =

helloWorldClass.DefineMethod("GetMsg", MethodAttributes.Public, typeof(string), null);

ILGenerator methodIL = getMsgMethod.GetILGenerator(); methodIL.Emit(OpCodes.Ldarg_0); methodIL.Emit(OpCodes.Ldfld, msgField); methodIL.Emit(OpCodes.Ret);

//Create the SayHello method.

MethodBuilder sayHiMethod = helloWorldClass.DefineMethod("SayHello", MethodAttributes.Public, null, null);

methodIL = sayHiMethod.GetILGenerator(); methodIL.EmitWriteLine("Hello from the HelloWorld class!"); methodIL.Emit(OpCodes.Ret);

//'Bake' the class HelloWorld.

//(Baking is the formal term for emitting the type) helloWorldClass.CreateType();

//(Optionally) save the assembly to file. assembly.Save("MyAssembly.dll");

}

Emitting the Assembly and Module Set

The method body begins by establishing the minimal set of characteristics about your assembly, using the AssemblyName and Version types (defined in the System.Reflection namespace). Next, you obtain an AssemblyBuilder type via the instance-level AppDomain.DefineDynamicAssembly() method (recall the caller will pass in an AppDomain reference into the CreateMyAsm() method):

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

509

//Establish general assembly characteristics.

//and gain access to the AssemblyBuilder type

public static void CreateMyAsm(AppDomain curAppDomain)

{

AssemblyName assemblyName = new AssemblyName(); assemblyName.Name = "MyAssembly"; assemblyName.Version = new Version("1.0.0.0");

// Create new assembly within the current AppDomain.

AssemblyBuilder assembly = curAppDomain.DefineDynamicAssembly(assemblyName,

AssemblyBuilderAccess.Save);

...

}

As you can see, when calling AppDomain.DefineDynamicAssembly(), you must specify the access mode of the assembly you wish to define, which can be any of the values shown in Table 15-10.

Table 15-10. Values of the AssemblyBuilderAccess Enumeration

Value

Meaning in Life

ReflectionOnly

Represents that a dynamic assembly that can only be reflected over

Run

Represents that a dynamic assembly can be executed in memory but

 

not saved to disk

RunAndSave

Represents that a dynamic assembly can be executed in memory and

 

saved to disk

Save

Represents that a dynamic assembly can be saved to disk but not

 

executed in memory

 

 

The next task is to define the module set for your new assembly. Given that the assembly is a single file unit, you need to define only a single module. If you were to build a multifile assembly using the DefineDynamicModule() method, you would specify an optional second parameter that

represents the name of a given module (e.g., myMod.dotnetmodule). However, when creating a singlefile assembly, the name of the module will be identical to the name of the assembly itself. In any case, once the DefineDynamicModule() method has returned, you are provided with a reference to a valid ModuleBuilder type:

// The single-file assembly.

ModuleBuilder module =

assembly.DefineDynamicModule("MyAssembly", "MyAssembly.dll");

The Role of the ModuleBuilder Type

ModuleBuilder is key type used during the development of dynamic assemblies. As you would expect, ModuleBuilder supports a number of members that allow you to define the set of types contained within a given module (classes, interfaces, structures, etc.) as well as the set of embedded resources (string tables, images, etc.) contained within (the .NET resource format will be examined in Chapter 15). Table 15-11 describes a few of the creation-centric methods. (Do note that each method will return to you a related type that represents the type you wish to construct.)

510 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

Table 15-11. Select Members of the ModuleBuilder Type

Method

Meaning in Life

DefineEnum()

Used to emit a .NET enum definition

DefineResource()

Defines a managed embedded resource to be stored in this module

DefineType()

Constructs a TypeBuilder, which allows you to define value types,

 

interfaces, and class types (including delegates)

 

 

The key member of the ModuleBuilder class to be aware of is DefineType(). In addition to specifying the name of the type (via a simple string), you will also make use of the System.Reflection. TypeAttributes enum to describe the format of the type itself. Table 15-12 lists some (but not all) of the key members the TypeAttributes enumeration.

Table 15-12. Select Members of the TypeAttributes Enumeration

Member

Meaning in Life

Abstract

Specifies that the type is abstract

Class

Specifies that the type is a class

Interface

Specifies that the type is an interface

NestedAssembly

Specifies that the class is nested with assembly visibility and is thus

 

accessible only by methods within its assembly

NestedFamAndAssem

Specifies that the class is nested with assembly and family visibility, and

 

is thus accessible only by methods lying in the intersection of its family

 

and assembly

NestedFamily

Specifies that the class is nested with family visibility and is thus

 

accessible only by methods within its own type and any subtypes

NestedFamORAssem

Specifies that the class is nested with family or assembly visibility, and

 

is thus accessible only by methods lying in the union of its family and

 

assembly

NestedPrivate

Specifies that the class is nested with private visibility

NestedPublic

Specifies that the class is nested with public visibility

NotPublic

Specifies that the class is not public

Public

Specifies that the class is public

Sealed

Specifies that the class is concrete and cannot be extended

Serializable

Specifies that the class can be serialized

 

 

Emitting the HelloClass Type and the String Member Variable

Now that you have a better understanding of the role of the ModuleBuilder.CreateType() method, let’s examine how you can emit the public HelloWorld class type and the private string variable:

// Define a public class named "MyAssembly.HelloWorld".

TypeBuilder helloWorldClass = module.DefineType("MyAssembly.HelloWorld", TypeAttributes.Public);

// Define a private String member variable named "theMessage".

FieldBuilder msgField = helloWorldClass.DefineField("theMessage",

typeof(string),

FieldAttributes.Private);

// Assign msgField. // Return.

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

511

Notice how the TypeBuilder.DefineField() method provides access to a FieldBuilder type. The TypeBuilder class also defines other methods that provide access to other “builder” types. For example,

DefineConstructor() returns a ConstructorBuilder, DefineProperty() returns a PropertyBuilder, and so forth.

Emitting the Constructors

As mentioned earlier, the TypeBuilder.DefineConstructor() method can be used to define a constructor for the current type. However, when it comes to implementing the constructor of HelloClass, you need to inject raw CIL code into the constructor body, which is responsible for assigning the incoming parameter to the internal private string. To obtain an ILGenerator type, you call the GetILGenerator() method from the respective “builder” type you have reference to (in this case, the ConstructorBuilder type).

The Emit() method of the ILGenerator class is the entity in charge of placing CIL into a member implementation. Emit() itself makes frequent use of the OpCodes class type, which exposes the opcode set of CIL using read-only fields. For example, OpCodes.Ret signals the return of a method call. OpCodes. Stfld makes an assignment to a member variable. OpCodes.Call is used to call a given method (in this case, the base class constructor). That said, ponder the following constructor logic:

//Create the custom constructor taking

//a single System.String argument.

Type[] constructorArgs = new Type[1]; constructorArgs[0] = typeof(string); ConstructorBuilder constructor =

helloWorldClass.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, constructorArgs);

//Now emit the necessary CIL into the ctor.

ILGenerator constructorIL = constructor.GetILGenerator();

 

constructorIL.Emit(OpCodes.Ldarg_0);

 

 

Type objectClass = typeof(object);

 

 

ConstructorInfo superConstructor

= objectClass.GetConstructor(new Type[0]);

constructorIL.Emit(OpCodes.Call,

superConstructor);

//

Call base class ctor.

//Load the object's 'this' pointer on the stack. constructorIL.Emit(OpCodes.Ldarg_0);

//load incoming argument on virtual stack and store in msgField. constructorIL.Emit(OpCodes.Ldarg_1);

constructorIL.Emit(OpCodes.Stfld, msgField); constructorIL.Emit(OpCodes.Ret);

Now, as you are well aware, as soon as you define a custom constructor for a type, the default constructor is silently removed. To redefine the no-argument constructor, simply call the DefineDefaultConstructor() method of the TypeBuilder type as so:

// Reinsert the default ctor. helloWorldClass.DefineDefaultConstructor(MethodAttributes.Public);

This single call emits the standard CIL code used to define a default constructor:

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

{

.maxstack 1 ldarg.0

512 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

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

}

Emitting the HelloWorld() Method

Last but not least, let’s examine the process of emitting the SayHello() method. The first task is to obtain a MethodBuilder type from the helloWorldClass variable. Once you do this, you define the method and obtain the underlying ILGenerator to inject the CIL instructions:

//Create the SayHello method.

MethodBuilder sayHiMethod = helloWorldClass.DefineMethod("SayHello", MethodAttributes.Public, null, null);

methodIL = sayHiMethod.GetILGenerator();

//Write a line to the Console. methodIL.EmitWriteLine("Hello there!"); methodIL.Emit(OpCodes.Ret);

Here you have established a public method (MethodAttributes.Public) that takes no parameters and returns nothing (marked by the null entries contained in the DefineMethod() call). Also note the EmitWriteLine() call. This helper member of the ILGenerator class automatically writes a line to the standard output with minimal fuss and bother.

Using the Dynamically Generated Assembly

Now that you have the logic in place to create and save your assembly, all that’s needed is a class to trigger the logic. To come full circle, assume your current project defines a second class named AsmReader. The logic in Main() obtains the current AppDomain via the Thread.GetDoMain() method that will be used to host the assembly you will dynamically create. Once you have a reference, you are able to call the CreateMyAsm() method.

To make things a bit more interesting, once the call to CreateMyAsm() returns, you will exercise some late binding (see Chapter 12) to load your newly created assembly into memory and interact with the members of the HelloWorld class:

using System;

using System.Reflection.Emit; using System.Reflection; using System.Threading;

...

public class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** The Amazing Dynamic Assembly Builder App *****");

//Get the application domain for the current thread.

AppDomain curAppDomain = Thread.GetDomain();

//Create the dynamic assembly using our helper f(x).

CreateMyAsm(curAppDomain);

Console.WriteLine("-> Finished creating MyAssembly.dll.");

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

513

//Now load the new assembly from file.

Console.WriteLine("-> Loading MyAssembly.dll from file."); Assembly a = Assembly.Load("MyAssembly");

//Get the HelloWorld type.

Type hello = a.GetType("MyAssembly.HelloWorld");

//Create HelloWorld object and call the correct ctor.

Console.Write("-> Enter message to pass HelloWorld class: "); string msg = Console.ReadLine();

object[] ctorArgs = new object[1]; ctorArgs[0] = msg;

object obj = Activator.CreateInstance(hello, ctorArgs);

//Call SayHello and show returned string.

Console.WriteLine("-> Calling SayHello() via late binding."); MethodInfo mi = hello.GetMethod("SayHello");

mi.Invoke(obj, null);

//Trigger GetMsg(). Invoke() returns an object that

//holds the method's return value.

mi = hello.GetMethod("GetMsg"); Console.WriteLine(mi.Invoke(obj, null));

}

}

In effect, you have just created a .NET assembly that is able to create .NET assemblies at runtime. That wraps up our examination of CIL and the role of dynamic assemblies. I hope this chapter

has deepened your understanding of the .NET type system and the syntax and semantics of CIL.

Note Be sure to load your dynamically created assembly into ildasm.exe to connect the dots between raw CIL code and the functionality within the System.Reflection.Emit namespace.

Source Code The DynAsmBuilder application is included under the Chapter 15 subdirectory.

A Brief Word Regarding System.CodeDOM

Now that you have seen how to build dynamic assemblies using System.Reflection.Emit and various CIL tokens, I must confess there is another (often easier) alternative. The .NET platform provides a technology termed code DOM that allows you to represent the structure of a .NET type in languageagnostic terms via a related object graph. Once this graph has been established using members of the System.CodeDOM namespace, you are able to dynamically persist its contents in language-specific code files (C#, Visual Basic .NET, or any third-party language that supports a code DOM provider). As well, the System.CodeDOM.Compiler namespace (and related namespaces) can be used to compile an in-memory (or persisted) object graph into a valid static .NET assembly.

Alas, I don’t have the space to include information regarding code DOM technology in this edition of the text. If you require more information, look up the topic “CodeDOM, quick reference” within the .NET Framework 2.0 SDK documentation.