AddThis Social Bookmark Button

Print

Programming C#: Attributes and Reflection
Pages: 1, 2, 3, 4, 5, 6, 7, 8, 9

Dynamic Invocation with Reflection Emit

So far you've created an assembly on the fly by writing its source code to disk and then compiling that source code. You then dynamically invoked the method you wanted to use from that assembly, which was compiled on disk. That brings a lot of overhead, and what have you accomplished? When you're done with writing the file to disk, you have source code you can compile, and when you're done compiling, you have IL (Intermediate Language) op codes on disk you can ask the .NET Framework to run.

Reflection emit allows you to skip a few steps and just "emit" the op codes directly. This is writing assembly code directly from your C# program and then invoking the result. It just doesn't get any cooler than that.

You start much as you did in the previous examples. You create a constant for the number to add to (200) and the number of iterations (1,000,000). You then re-create the myMath class as a benchmark.

Once again you have a ReflectionTest class, and once again you call DoSum, passing in the value:

ReflectionTest t = new ReflectionTest(  );
result = t.DoSum(val);

DoSum itself is virtually unchanged:

public double DoSum(int theValue)
{
   if (theComputer == null)
   {
      GenerateCode(theValue);
   }
 
   // call the method through the interface
   return (theComputer.ComputeSum(  ));
}

As you can see, you will use an interface again, but this time you are not going to write a file to disk.

GenerateCode is quite different now. You no longer write the file to disk and compile it; instead you call the helper method EmitAssembly and get back an assembly. You then create an instance from that assembly and cast that instance to your interface.

public void GenerateCode(int theValue)
{
   Assembly theAssembly = EmitAssembly(theValue);
   theComputer = (IComputer) 
      theAssembly.CreateInstance("BruteForceSums");
}

As you might have guessed, the magic is stashed away in the EmitAssembly method:

private Assembly EmitAssembly(int theValue)

The value you pass in is the sum you want to compute. To see the power of reflection emit, you'll increase that value from 200 to 2,000.

The first thing to do in EmitAssembly is to create an object of type AssemblyName and give that AssemblyName object the name "DoSumAssembly":

AssemblyName assemblyName = new AssemblyName(  );
assemblyName.Name = "DoSumAssembly";

An AssemblyName is an object that fully describes an assembly's unique identity. As discussed in Chapter 13, an assembly's identity consists of a simple name (DoSumAssembly), a version number, a cryptographic key pair, and a supported culture.

With this object in hand, you can create a new AssemblyBuilder object. To do so, you call DefineDynamicAssembly on the current domain, which you get by calling the static GetDomain( ) method of the Thread object. Domains are discussed in detail in Chapter 19.

The parameters to the GetDomain( )method are the AssemblyName object you just created and an AssemblyBuilderAccess enumeration value (one of Run, RunandSave, or Save). You'll use Run in this case to indicate that the assembly can be run but not saved:

AssemblyBuilder newAssembly =
  Thread.GetDomain(  ).DefineDynamicAssembly(assemblyName,
    AssemblyBuilderAccess.Run);

With this newly created AssemblyBuilder object, you are ready to create a ModuleBuilder object. The job of the ModuleBuilder, not surprisingly, is to build a module dynamically. Modules are discussed in Chapter 17. You call the DefineDynamicModule method, passing in the name of the method you want to create:

ModuleBuilder newModule = 
newAssembly.DefineDynamicModule("Sum");

Now, given that module, you can define a public class and get back a TypeBuilder object. TypeBuilder is the root class used to control the dynamic creation of classes. With a TypeBuilder object, you can define classes and add methods and fields:

TypeBuilder myType = newModule.DefineType("BruteForceSums", TypeAttributes.Public);

You are now ready to mark the new class as implementing the IComputer interface:

myType.AddInterfaceImplementation(typeof(IComputer));

You're almost ready to create the ComputeSum method, but first you must set up the array of parameters. Because you have no parameters at all, you create an array of zero length:

Type[] paramTypes = new Type[0];

You then create a Type object to hold the return type for your method:

Type returnType = typeof(int);

You're ready to create the method. The DefineMethod( ) method of TypeBuilder will both create the method and return an object of type MethodBuilder, which you will use to generate the IL code:

MethodBuilder simpleMethod =
myType.DefineMethod("ComputeSum",
               MethodAttributes.Public | 
                  MethodAttributes.Virtual,
               returnType,
               paramTypes);

You pass in the name of the method, the flags you want (public and virtual), the return type (int), and the paramTypes (the zero length array).

You then use the MethodBuilder object you created to get an ILGenerator object:

ILGenerator generator = simpleMethod.GetILGenerator( );

With your precious ILGenerator object in hand, you are ready to emit the op codes. These are the very op codes that the C# compiler would have created. (In fact, the best way to get the op codes is to write a small C# program, compile it, and then examine the op codes in ILDasm!)

First emit the value 0 to the stack. Then loop through the number values you want to add (1 through 200), adding each to the stack in turn, adding the previous sum to the new number and leaving the result on the stack:

generator.Emit(OpCodes.Ldc_I4, 0);
for (int i = 1; i <= theValue;i++)
{
    generator.Emit(OpCodes.Ldc_I4, i);
    generator.Emit(OpCodes.Add);
}

The value that remains on the stack is the sum you want, so you'll return it:

generator.Emit(OpCodes.Ret);

You're ready now to create a MethodInfo object that will describe the method:

MethodInfo computeSumInfo =
    typeof(IComputer).GetMethod("ComputeSum");

Now you must specify the implementation that will implement the method. You call DefineMethodOverride on the TypeBuilder object you created earlier, passing in the MethodBuilder you created, along with the MethodInfo object you just created:

myType.DefineMethodOverride(simpleMethod, computeSumInfo);

You're just about done; create the class and return the assembly:

myType.CreateType(  );
return newAssembly;

OK, I didn't say it was easy, but it is really cool, and the resulting code runs very fast. The normal loop runs 1,000,000 iterations in 11.5 seconds, but the emitted code runs in .4 second! A full 3,000% faster. Example 18-11 is the full source code.

Example 18-11: Dynamic invocation with reflection emit

namespace Programming_CSharp
{
 using System;
 using System.Diagnostics;
 using System.IO;
 using System.Reflection;
 using System.Reflection.Emit;
 using System.Threading;
 
 // used to benchmark the looping approach
 public class MyMath
 {
   // sum numbers with a loop
   public int DoSumLooping(int initialVal)
   {
     int result = 0;
     for(int i = 1;i <=initialVal;i++)
     {
      result += i;
     }
     return result;
   }
 }
 
 // declare the interface
 public interface IComputer
 {
   int ComputeSum( );
 }
 
 public class ReflectionTest
 {
   // the private method which emits the assembly
   // using op codes
   private Assembly EmitAssembly(int theValue)
   {
     // Create an assembly name
     AssemblyName assemblyName =
      new AssemblyName( );
     assemblyName.Name = "DoSumAssembly";
 
     // Create a new assembly with one module
     AssemblyBuilder newAssembly =
      Thread.GetDomain( ).DefineDynamicAssembly(
      assemblyName, AssemblyBuilderAccess.Run);
     ModuleBuilder newModule =
      newAssembly.DefineDynamicModule("Sum");
 
     // Define a public class named "BruteForceSums "
     // in the assembly.
     TypeBuilder myType =
      newModule.DefineType(
      "BruteForceSums", TypeAttributes.Public);
 
     // Mark the class as implementing IComputer.
     myType.AddInterfaceImplementation(
      typeof(IComputer));
 
     // Define a method on the type to call. Pass an
     // array that defines the types of the parameters,
     // the type of the return type, the name of the
     // method, and the method attributes.
     Type[] paramTypes = new Type[0];
     Type returnType = typeof(int);
     MethodBuilder simpleMethod =
      myType.DefineMethod(
      "ComputeSum",
      MethodAttributes.Public |
      MethodAttributes.Virtual,
      returnType,
      paramTypes);
 
     // Get an ILGenerator. This is used
     // to emit the IL that you want.
     ILGenerator generator =
      simpleMethod.GetILGenerator( );
 
     // Emit the IL that you'd get if you
     // compiled the code example
     // and then ran ILDasm on the output.
 
     // Push zero onto the stack. For each 'i'
     // less than 'theValue',
     // push 'i' onto the stack as a constant
     // add the two values at the top of the stack.
     // The sum is left on the stack.
     generator.Emit(OpCodes.Ldc_I4, 0);
     for (int i = 1; i <= theValue;i++)
     {
      generator.Emit(OpCodes.Ldc_I4, i);
      generator.Emit(OpCodes.Add);
 
     }
 
     // return the value
     generator.Emit(OpCodes.Ret);
 
     //Encapsulate information about the method and
     //provide access to the method's metadata
     MethodInfo computeSumInfo =
      typeof(IComputer).GetMethod("ComputeSum");
 
     // specify the method implementation.
     // Pass in the MethodBuilder that was returned
     // by calling DefineMethod and the methodInfo
     // just created
     myType.DefineMethodOverride(simpleMethod, computeSumInfo);
 
     // Create the type.
     myType.CreateType( );
     return newAssembly;
   }
 
   // check if the interface is null
   // if so, call Setup.
   public double DoSum(int theValue)
   {
     if (theComputer == null)
     {
      GenerateCode(theValue);
     }
 
     // call the method through the interface
     return (theComputer.ComputeSum( ));
   }
 
   // emit the assembly, create an instance
   // and get the interface
   public void GenerateCode(int theValue)
   {
     Assembly theAssembly = EmitAssembly(theValue);
     theComputer = (IComputer)
      theAssembly.CreateInstance("BruteForceSums");
   }
 
   // private member data
   IComputer theComputer = null;
 
 }
 
 public class TestDriver
 {
   public static void Main( )
   {
     const int val = 2000; // Note 2,000
 
     // 1 million iterations!
     const int iterations = 1000000;
     double result = 0;
 
     // run the benchmark
     MyMath m = new MyMath( );
     DateTime startTime = DateTime.Now;      
     for (int i = 0;i < iterations;i++)
      result = m.DoSumLooping(val);
     }
     TimeSpan elapsed =
      DateTime.Now - startTime;
     Console.WriteLine(
      "Sum of ({0}) = {1}",val, result);
     Console.WriteLine(
      "Looping. Elapsed milliseconds: " +
      elapsed.TotalMilliseconds +
      " for {0} iterations", iterations);
 
     // run our reflection alternative
     ReflectionTest t = new ReflectionTest( );
 
     startTime = DateTime.Now;
     for (int i = 0;i < iterations;i++)
     {
      result = t.DoSum(val);
     }
 
     elapsed = DateTime.Now - startTime;
     Console.WriteLine(
      "Sum of ({0}) = {1}",val, result);
     Console.WriteLine(
      "Brute Force. Elapsed milliseconds: " +
      elapsed.TotalMilliseconds +
      " for {0} iterations", iterations);
   }
 }
}

Output:

Sum of (2000) = 2001000
Looping. Elapsed milliseconds: 
11468.75 for 1000000 iterations
Sum of (2000) = 2001000
Brute Force. Elapsed milliseconds: 
406.25 for 1000000 iterations

Reflection emit is a powerful technique for emitting op codes. Although today's compilers are very fast and today's machines have lots of memory and processing speed, it is comforting to know that when you must, you can get right down to the virtual metal.


Return to the .NET DevCenter.