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

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

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

652CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

//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):

//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 19-10.

CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

653

Table 19-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 the 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. Table 19-11 describes a few of the creationcentric methods. (Do note that each method will return to you a related type that represents the type you wish to construct.)

Table 19-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 19-12 lists some (but not all) of the key members of the TypeAttributes enumeration.

654 CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

Table 19-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);

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

CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

655

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); // Assign msgField.

constructorIL.Emit(OpCodes.Ret);

// Return.

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

// 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

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

}

Emitting the SayHello() 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:

656 CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

//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 16) to load your newly created assembly into memory and interact with the members of the HelloWorld class. Update your Main() method as follows:

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.");

//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);

CHAPTER 19 UNDERSTANDING CIL AND THE ROLE OF DYNAMIC ASSEMBLIES

657

// Invoke method.

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

}

In effect, you have just created a .NET assembly that is able to create and execute .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 DynamicAsmBuilder project is included under the Chapter 19 subdirectory.

Summary

This chapter provided an overview of the syntax and semantics of CIL. Unlike higher-level managed languages such as C#, CIL does not simply define a set of keywords, but provides directives (used to define the structure of an assembly and its types), attributes (which further qualify a given directive), and opcodes (which are used to implement type members). You were introduced to the CIL compiler (ilasm.exe) and learned how to alter the contents of a .NET assembly with new CIL code and also the basic process of building a .NET assembly using raw CIL.

The latter half of this chapter introduced you to the System.Reflection.Emit namespace. Using these types, you are able to emit a .NET assembly on the fly to memory. As well, if you so choose, you may persist this in-memory image to a physical file. Recall that many types of System. Reflection.Emit will automatically generate the correct CIL directives and attributes using friendly types such as ConstructorBuilder, TypeBuilder, and so forth. The ILGenerator type can be used to inject the necessary CIL opcodes into a given member. While we do have a number of helper types that attempt to make the process of programming with the CIL opcode set more palatable, you must have an understanding of CIL when programming with dynamic assemblies.

P A R T 5

Introducing the .NET Base

Class Libraries

C H A P T E R 2 0

File I/O and Isolated Storage

When you are creating full-blown desktop applications, the ability to save information between user sessions is imperative. This chapter examines a number of I/O-related topics as seen through the eyes of the .NET Framework. The first order of business is to explore the core types defined in the System.IO namespace and come to understand how to programmatically modify a machine’s directory and file structure. The next task is to explore various ways to read from and write to character-based, binary-based, string-based, and memory-based data stores.

Once you have learned to manipulate files and directories using the core I/O types, you will then be introduced to the topic of isolated storage (via the System.IO.IsolatedStorage namespace). This approach to persisting user and application data allows applications that are running under a restricted security environment to perform limited file I/O in a safe manner. To illustrate this API in action, you will examine a security-centric aspect of the .NET platform termed Code Access Security (CAS), which is commonly used in conjunction with isolated storage.

Exploring the System.IO Namespace

In the framework of .NET, the System.IO namespace is the region of the base class libraries devoted to file-based (and memory-based) input and output (I/O) services. Like any namespace, System.IO defines a set of classes, interfaces, enumerations, structures, and delegates, most of which are contained in mscorlib.dll. In addition to the types contained within mscorlib.dll, the System.dll assembly defines additional members of the System.IO namespace (given that all Visual Studio 2008 projects automatically set a reference to both assemblies, you should be ready to go).

Many of the types within the System.IO namespace focus on the programmatic manipulation of physical directories and files. However, additional types provide support to read data from and write data to string buffers as well as raw memory locations. To give you a road map of the functionality in System.IO, Table 20-1 outlines the core (nonabstract) classes.

Table 20-1. Key Members of the System.IO Namespace

Nonabstract I/O Class Type

Meaning in Life

BinaryReader, BinaryWriter

These types allow you to store and retrieve primitive data types

 

(integers, Booleans, strings, and whatnot) as a binary value.

BufferedStream

This type provides temporary storage for a stream of bytes that

 

may be committed to storage at a later time.

Directory, DirectoryInfo

These types are used to manipulate a machine’s directory

 

structure. The Directory type exposes functionality using static

 

members. The DirectoryInfo type exposes similar functionality

 

from a valid object reference.

 

Continued

661