23.9 Creating Dynamic Assemblies

 <  Day Day Up  >  

You want to programmatically generate an executable assembly.


Technique

Dynamic assembly generation is the act of programmatically creating a .NET assembly at runtime from within your application. The System.Reflection.Emit namespace contains all the classes you need to create an assembly. This recipe looks at a particular application of dynamic assembly generation called the Animal Builder. In essence, the application asks the user a series of questions about a particular animal she wants to create. Once the questions are finished, the application proceeds to generate a class library containing a new class with two fields, a constructor and a public method that can then be used within another application. The initial steps for the application simply entail getting the answers from the user and setting properties within the AnimalBuilder class.

Listing 23.5 Programmatically Creating Assemblies
 using System; using System.Reflection; using System.Reflection.Emit; using System.Threading; namespace _9_DynamicAssembly {     class Class1     {         [STAThread]         static void Main(string[] args)         {             string input;             AnimalBuilder builder = new AnimalBuilder();             Console.WriteLine( "Welcome to the .NET Animal Builder!" );             Console.WriteLine( "This program lets you build an animal and " +                                " save it as an assembly" );             input = GetInput( "What type of animal do you want?" );             builder.AnimalType = input;             input = GetInput( "What sound does the animal make?" );             builder.AnimalSound = input;             input = GetInput( "How many sounds will it make before it gets" +                                 " hoarse (no pun intended)?" );             builder.SoundsBeforeHoarse = Int32.Parse(input);             input = GetInput( "Where would you like to save this animal?" );             builder.BuildAndSaveAnimal( input );         }         static string GetInput( string question )         {             Console.Write( question + ": " );             return Console.ReadLine();         }         public class AnimalBuilder         {             private string animalType;             private string animalSound;             private int sounds;             public string AnimalType             {                 get                 {                     return animalType;                 }                 set                 {                     animalType = value;                 }             }             public string AnimalSound             {                 get                 {                     return animalSound;                 }                 set                 {                     animalSound = value;                 }             }             public int SoundsBeforeHoarse             {                 get                 {                     return sounds;                 }                 set                 {                     sounds = value;                 }             }             public void BuildAndSaveAnimal( string assemblyPath )             {             }         }     } } 

As you can see, the BuildAndSaveAnimal method is blank. It is the dynamic assembly generation method that will take all the property values within the class along with a location to save the final assembly.

The first step in creating an assembly is creating an assembly name by creating an AssemblyName object and setting its Name parameter. Note that it is not the same as the filename used for the assembly. Creating assemblies involves the use of an application domain. Because every .NET assembly runs within the confines of an application domain, you already know that your current application has an application domain that you can use. Calling the Thread.GetDomain method returns the active application domain for your current application, as shown in the following code:

 
 public void BuildAndSaveAnimal( string assemblyPath ) {     // create an assembly name     AssemblyName asmName = new AssemblyName();     asmName.Name = animalType;     // get current appdomain     AppDomain domain = Thread.GetDomain(); 

Now that you have a name for the assembly and an application domain to create it in, you are ready to generate the assembly in memory. Call the DefineDynamicAssembly method. For this example, we are creating a class library, so the second parameter to this method is AssemblyBuilderAccess.Save . If it were an executable assembly, you could set the value to Run or SaveAndRun to utilize the dynamic invocation techniques described in the next recipe. The DefineDynamicAssembly method returns an AssemblyBuilder object, which is the first stop in the assembly hierarchy.

 
 // get an assembly builder from the appdomain AssemblyBuilder asmBuilder = domain.DefineDynamicAssembly(  asmName, AssemblyBuilderAccess.Save ); 

Earlier recipes mentioned that assemblies contain a hierarchical organization for constructs defined in them. At the top level is the assembly, followed by one or more modules, each of which defines zero or more types. Each type can also define zero or more members . Therefore, the next step after creating the assembly is to build a module. Call the DefineDynamicModule method defined in the AssemblyBuilder class created in the previous step:

 
 // get a module builder from the assembly builder ModuleBuilder modBuilder = asmBuilder.DefineDynamicModule(  animalType, assemblyPath ); 

At this point, you are ready to start defining the types that make up the module. For this application, the only module type defined is a class. To create a new class, call the DefineType method from the ModuleBuilder object created earlier. The first parameter is the name of the class, and the second parameter is a bit field containing any necessary TypeAttributes . For this class, the only necessary attribute is TypeAttributes.Public . The method returns a TypeBuilder object, which will be used to define two member fields, a constructor and a public method:

 
 // create a new public class TypeBuilder typeBuilder = modBuilder.DefineType( animalType,  TypeAttributes.Public ); 

The next step is to define the two fields that are used. If you recall from the beginning of this recipe, the AnimalBuilder class uses two properties relating to the sound the animal makes and how many times that sound is emitted before the animal's voice becomes hoarse. Therefore, create two fields by using the DefineField method of the TypeBuilder , specifying the variable name, the variable type, and any necessary FieldAttributes , as shown in the following code. These two fields will be referenced later when the constructor body is created, so save each of the returned FieldBuilder objects:

 
 // add a new field FieldBuilder soundFld = typeBuilder.DefineField( "Sound", typeof(string),                          FieldAttributes.Private ); FieldBuilder countFld = typeBuilder.DefineField( "Count", typeof(int),                          FieldAttributes.Private ); 

So far, the steps in creating the dynamic assembly haven't been too difficult. Unfortunately , it doesn't stay that way. When you create methods within a dynamic assembly, there is no way to assign C# code to the method bodies. Instead, you have to use an ILGenerator object to emit actual intermediate language (IL) code within the body. Fortunately, the ILDasm tool is able to generate IL code given an assembly. With that knowledge, you can create the code, compile it, and then view the IL to help you with this next step. Before you do so, however, you must first create the constructor. The code that follows creates a constructor using the DefineConstructor method from the TypeBuilder object created earlier. Next, an ILGenerator object is retrieved by using the GetILGenerator method defined in the ConstructorBuilder class. Finally, the IL is emitted using the ILGenerator . If you look at the Emit statements, you can see that the OpCodes class contains several properties corresponding to the available IL opcodes. In most cases, the conversion from IL to an OpCodes property is straightforward. Within the code, also take special note of the load string ( ldstr ), store field ( stfld ), and load integer constant ( ldc.i4 ) opcodes. The second parameter for this emitted opcodes references properties within the AnimalBuilder class to retrieve their current values and the FieldBuilder object created earlier with the calls to DefineField :

 
 // build the constructor ConstructorBuilder constructor = typeBuilder.DefineConstructor(     MethodAttributes.Public, CallingConventions.Standard, null ); ILGenerator gen = constructor.GetILGenerator(); gen.Emit( OpCodes.Ldarg_0 ); gen.Emit( OpCodes.Call, typeof(Object).GetConstructor(new Type[]{})); gen.Emit( OpCodes.Ldarg_0 ); gen.Emit( OpCodes.Ldstr, animalSound ); gen.Emit( OpCodes.Stfld, soundFld ); gen.Emit( OpCodes.Ldarg_0 ); gen.Emit( OpCodes.Ldc_I4, sounds ); gen.Emit( OpCodes.Stfld, countFld ); gen.Emit( OpCodes.Ret ); 

The constructor method body just shown simply stores the values of the AnimalBuilder class in the dynamically generated class variables of the dynamic assembly using assignment statements. The final step in creating the dynamic assembly is to create the public method of the class. This step unfortunately is a little more involved than the constructor body created earlier. The procedure to actually create the method, however, is similar to the procedure used to create the constructor. To create a new method, call the DefineMethod function defined in the TypeBuilder , passing the name of the method; any MethodAttributes ; the CallingConvention , which can simply be set to null if no special convention is needed; and an array of Type object corresponding to the parameter list of the method. Because this method does not use parameters, the array is empty.

After the method is created, the IL code is emitted. The example shown demonstrates how to emit labels within IL code. First, two labels correspond to the branching that occurs as a result of a for loop. Whenever a label needs to be set within IL, call the MarkLabel method defined in the ILGenerator class, passing one of the labels created. One thing to note is that the labels aren't similar to labels used within common programming languages in that no special name is used. The ILGenerator generates the correct label, which incidentally corresponds to the memory location of an instruction, whenever it is used. The branch ( br ) and branch-if- less-than ( blt.s ) instructions use the labels created and cause a branch to occur at the point where the MarkLabel method is called. To compare the ILGenerator code with the actual IL code, the IL code is placed within comments following the Emit recipe.

Listing 23.6 Emitting IL Code
 // add the hoarse method  MethodBuilder hoarseMethod = typeBuilder.DefineMethod( "MakeHoarse",      MethodAttributes.Public, null, new Type[]{} );  ILGenerator gen = hoarseMethod.GetILGenerator();  // create labels for branching  Label endLoop = gen.DefineLabel();  Label beginLoop = gen.DefineLabel();  // create Console.WriteLine MethodInfo  MethodInfo writeLine = typeof(Console).GetMethod( "WriteLine",                          new Type[]{typeof(string)});  gen.DeclareLocal( typeof(int) );  gen.Emit( OpCodes.Ldc_I4_0 );  gen.Emit( OpCodes.Stloc_0 );  gen.Emit( OpCodes.Br_S, endLoop );  gen.MarkLabel( beginLoop );  gen.Emit( OpCodes.Ldarg_0 );  gen.Emit( OpCodes.Ldfld, soundFld );  gen.EmitCall( OpCodes.Call, writeLine, null );  gen.Emit( OpCodes.Ldloc_0 );  gen.Emit( OpCodes.Ldc_I4_1 );  gen.Emit( OpCodes.Add );  gen.Emit( OpCodes.Stloc_0 );  gen.MarkLabel( endLoop );  gen.Emit( OpCodes.Ldloc_0 );  gen.Emit( OpCodes.Ldarg_0 );  gen.Emit( OpCodes.Ldfld, countFld );  gen.Emit( OpCodes.Blt_S, beginLoop );  gen.EmitWriteLine("...a muffled sound emanates..." );  gen.Emit( OpCodes.Ret ); // MakeHoarse IL Code /*     IL_0000:  ldc.i4.0     IL_0001:  stloc.0     IL_0002:  br.s       IL_0013     IL_0004:  ldarg.0     IL_0005:  ldfld      string ConsoleApplication1.Horse::Sound     IL_000a:  call       void [mscorlib]System.Console::WriteLine(string)     IL_000f:  ldloc.0     IL_0010:  ldc.i4.1     IL_0011:  add     IL_0012:  stloc.0     IL_0013:  ldloc.0     IL_0014:  ldarg.0     IL_0015:  ldfld      int32 ConsoleApplication1.Horse::Count     IL_001a:  blt.s      IL_0004     IL_001c:  ldstr      "...a muffled sound emanates from the animal."     IL_0021:  call       void [mscorlib]System.Console::WriteLine(string)     IL_0026:  ret     */ 

The MakeHoarse method simply uses a for loop to output the animal sound the specified number of times. When the count has been exceeded, a message states that the animal can no longer speak and the method exits. At this point, the necessary information for the assembly has been created, and the final step is to generate it. For each Type that is defined, call the CreateType method. This application creates only a single class so this is done once. Finally, you call the Save method defined in the AssemblyBuilder class, which generates and writes the assembly to a file ready to be referenced by an application:

 
 typeBuilder.CreateType();     // save final assembly     asmBuilder.Save( assemblyPath ); } 

Comments

Dynamic assembly generation might seem odd at first, and like type discovery, using reflection might be difficult in figuring where an application can use it. However, you can see evidence of its use within the .NET Framework. For example, the RegEx class is used for regular expressions. One of its methods is named CompileToAssembly , which takes a currently loaded RegEx object and compiles its current state to an assembly. This step allows portability using regular expressions in that a regular expression that matches URLs, for instance, can be compiled to an assembly for reference in other applications. Therefore, rather than manually add the regular expression for a URL to a project, which is a rather lengthy expression, you can simply add a reference to the generated assembly in your new project.

As another example, you can create an application that utilizes various image resources. Rather than simply store these images within a directory where any user can edit or view them, you can automatically generate an assembly containing the image resources. As resources are added or removed, the assembly is regenerated, providing a single file that dynamically provides data file support. In the future, you will start to see some interesting applications with dynamic assembly generation.

 <  Day Day Up  >  


Microsoft Visual C# .Net 2003
Microsoft Visual C *. NET 2003 development skills Daquan
ISBN: 7508427505
EAN: 2147483647
Year: 2003
Pages: 440

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net