WindowsDevCenter.com
oreilly.comSafari Books Online.Conferences.

advertisement


AddThis Social Bookmark Button

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

Dynamic Invocation with InvokeMember( )

The first approach will be to create a class named BruteForceSums dynamically, at runtime. The BruteForceSums class will contain a method, ComputeSum( ), that implements the brute-force approach. You'll write that class to disk, compile it, and then use dynamic invocation to invoke its brute-force method by means of the InvokeMember( ) method of the Type class. The key point is that BruteForceSums.cs won't exist until you run the program. You'll create it when you need it and supply its arguments then.

To accomplish this, you'll create a new class named ReflectionTest. The job of the ReflectionTest class is to create the BruteForceSums class, write it to disk, and compile it. ReflectionTest has only two methods: DoSum and GenerateCode.

ReflectionTest.DoSum is a public method that returns the sum, given a value. That is, if you pass in 10, it returns the sum of 1+2+3+4+5+6+7+8+9+10. It does this by creating the BruteForceSums class and delegating the job to its ComputeSum method.

ReflectionTest has two private fields:

Type theType = null;
object theClass = null;

The first is an object of type Type, which you use to load your class from disk, and the second is an object of type object, which you use to dynamically invoke the ComputeSums( ) method of the BruteForceSums class you'll create.

The driver program instantiates an instance of ReflectionTest and calls its DoSum method, passing in the value. For this version of the program, the value is increased to 200.

The DoSum method checks whether theType is null; if it is, the class has not been created yet. DoSum calls the helper method GenerateCode to generate the code for the BruteForceSums class and the class's ComputeSums method. GenerateCode then writes this newly created code to a .cs file on disk and runs the compiler to turn it into an assembly on disk. Once this is completed, DoSum can call the method using reflection.

Once the class and method are created, you load the assembly from disk and assign the class type information to theType, and DoSum can use that to invoke the method dynamically to get the correct answer.

You begin by creating a constant for the value to which you'll sum:

const int val = 200;

Each time you compute a sum, it will be the sum of the values 1 to 200.

Before you create the dynamic class, you need to go back and re-create MyMath:

MyMath m = new MyMath(  );

Give MyMath a method DoSumLooping, much as you did in the previous example:

public int DoSumLooping (int initialVal)
{
    int result = 0;
    for(int i = 1;i <=initialVal;i++)
    {
        result += i;
    }
    return result;
}

This serves as a benchmark against which you can compare the performance of the brute-force method.

Now you're ready to create the dynamic class and compare its performance with the looping version. First, instantiate an object of type ReflectionTest and invoke the DoSum( ) method on that object:

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

ReflectionTest.DoSum checks to see if its Type field, theType, is null. If it is, you haven't yet created and compiled the BruteForceSums class and must do so now:

if (theType == null)
{
    GenerateCode(theValue);
}

The GenerateCode method takes the value (in this case, 200) as a parameter to know how many values to add.

GenerateCode begins by creating a file on disk. The details of file I/O will be covered in Chapter 21. For now, I'll walk you through this quickly. First, call the static method File.Open, and pass in the filename and a flag indicating that you want to create the file. File.Open returns a Stream object:

string fileName = "BruteForceSums";
Stream s = File.Open(fileName + ".cs", FileMode.Create);

Once you have the Stream, you can create a StreamWriter so that you can write into that file:

StreamWriter wrtr = new StreamWriter(s);

You can now use the WriteLine methods of StreamWriter to write lines of text into the file. Begin the new file with a comment:

wrtr.WriteLine("// Dynamically created BruteForceSums class");

This writes the text:

// Dynamically created BruteForceSums class

to the file you've just created (BruteForceSums.cs). Next, write out the class declaration:

string className = "BruteForceSums";
wrtr.WriteLine("class {0}", className);
wrtr.WriteLine("{");

Within the braces of the class, you create the ComputeSum method:

wrtr.WriteLine("\tpublic double ComputeSum(  )");
wrtr.WriteLine("\t{");
wrtr.WriteLine("\t// Brute force sum method");
wrtr.WriteLine("\t// For value = {0}", theVal);

Now it is time to write out the addition statements. When you are done, you want the file to have this line:

return 0+1+2+3+4+5+6+7+8+9...

continuing up to value (in this case, 200):

wrtr.Write("\treturn 0");
for (int i = 1;i<=theVal;i++)
{
   wrtr.Write("+ {0}",i);
}

Notice how this works. What will be written to the file is:

\treturn 0+ 1+ 2+ 3+...

The initial \t causes the code to be indented in the source file.

When the loop completes, you end the return statement with a semicolon and then close the method and the class:

wrtr.WriteLine(";");
wrtr.WriteLine("\t}");
wrtr.WriteLine("}");

Close the streamWriter and the stream, thus closing the file:

wrtr.Close(  );
s.Close(  );

When this runs, the BruteForceSums.cs file will be written to disk. It will look like this:

// Dynamically created BruteForceSums class
class BruteForceSums
{
    public double ComputeSum(  )
    {
    // Brute force sum method
    // For value = 200
    return 0+ 1+ 2+ 3+ 4+ 5+ 6+ 7+ 8+ 9+ 10+ 
11+ 12+ 13+ 14+ 15+ 16+ 17+ 18+ 19+ 20+ 21+ 
22+ 23+ 24+ 25+ 26+ 27+ 28+ 29+ 30+ 31+ 32+ 
33+ 34+ 35+ 36+ 37+ 38+ 39+ 40+ 41+ 42+ 43+ 
44+ 45+ 46+ 47+ 48+ 49+ 50+ 51+ 52+ 53+ 54+ 
55+ 56+ 57+ 58+ 59+ 60+ 61+ 62+ 63+ 64+ 65+ 
66+ 67+ 68+ 69+ 70+ 71+ 72+ 73+ 74+ 75+ 76+ 
77+ 78+ 79+ 80+ 81+ 82+ 83+ 84+ 85+ 86+ 87+ 
88+ 89+ 90+ 91+ 92+ 93+ 94+ 95+ 96+ 97+ 98+ 
99+ 100+ 101+ 102+ 103+ 104+ 105+ 106+ 107+ 
108+ 109+ 110+ 111+ 112+ 113+ 114+ 115+ 116+ 
117+ 118+ 119+ 120+ 121+ 122+ 123+ 124+ 125+ 
126+ 127+ 128+ 129+ 130+ 131+ 132+ 133+ 134+ 
135+ 136+ 137+ 138+ 139+ 140+ 141+ 142+ 143+ 
144+ 145+ 146+ 147+ 148+ 149+ 150+ 151+ 152+ 
153+ 154+ 155+ 156+ 157+ 158+ 159+ 160+ 161+ 
162+ 163+ 164+ 165+ 166+ 167+ 168+ 169+ 170+ 
171+ 172+ 173+ 174+ 175+ 176+ 177+ 178+ 179+ 
180+ 181+ 182+ 183+ 184+ 185+ 186+ 187+ 188+ 
189+ 190+ 191+ 192+ 193+ 194+ 195+ 196+ 197+ 
198+ 199+ 200;
    }
}

This accomplishes the goal of dynamically creating a class with a method that finds the sum through brute force.

The only remaining task is to build the file and then use the method. To build the file, you must start a new process (processes are explained in some detail in Chapter 20). The best way to launch this process is with a ProcessStartInfo structure that will hold the command line. Instantiate a ProcessStartInfo and set its filename to cmd.exe:

ProcessStartInfo psi = new ProcessStartInfo(  );
 psi.FileName = "cmd.exe";

You need to pass in the string you want to invoke at the command line. The ProcessStartInfo.Arguments property specifies the command-line arguments to use when starting the program. The command-line argument to the cmd.exe program will be /c to tell cmd.exe to exit after it executes the command, and then the command for cmd.exe. The command for cmd.exe is the command-line compile:

string compileString = "/c csc /optimize+ ";
compileString += " /target:library ";
compileString += "{0}.cs > compile.out";

The string compileString will invoke the C# compiler (csc), telling it to optimize the code (after all, you're doing this to gain performance) and to build a dynamic link library (DLL) file (/target:library). You redirect the output of the compile to a file named compile.out so that you can examine it if there are errors.

You combine compileString with the filename, using the static method Format of the string class, and assign the combined string to psi.Arguments:

psi.Arguments = String.Format(compileString, fileName);

The effect of all this is to set the Arguments property of the ProcessStartInfo object psi to:

/c csc /optimize+ /target:library 
BruteForceSums.cs > compile.out

Before invoking cmd.exe, you set the WindowStyle property of psi to Minimized so that when the command executes, the window does not flicker onto and then off of the user's display:

psi.WindowStyle = ProcessWindowStyle.Minimized;

You are now ready to start the cmd.exe process, and you will wait until it finishes before proceeding with the rest of the GenerateCode method:

Process proc = Process.Start(psi);
proc.WaitForExit(  );

Once the process is done, you can get the assembly, and from the assembly, you can get the class you've created. Finally, you can ask that class for its type and assign that to your theType member variable:

Assembly a = Assembly.LoadFrom(fileName + ".dll");
theClass = a.CreateInstance(className);
theType = a.GetType(className);

You can now delete the .cs file you generated:

File.Delete(fileName + ".cs");

You've now filled theType, and you're ready to return to DoSum to invoke the ComputeSum method dynamically. The Type object has a method InvokeMember( ), which can be used to invoke a member of the class described by the Type object. The InvokeMember method is overloaded; the version you'll use takes five arguments:

public object InvokeMember(
   string name,
   BindingFlags invokeAttr,
   Binder binder,
   object target,
   object[] args
);
name
Is the name of the method you wish to invoke.
invokeAttr
Is a bit mask of BindingFlags that specify how the search of the object is conducted. In this case, you'll use the InvokeMethod flag OR'd with the Default flag. These are the standard flags for invoking a method dynamically.
binder
Is used to assist in type conversions. By passing in null, you'll specify that you want the default binder.
target
Is the object on which you'll invoke the method. In this case, you'll pass in theClass, which is the class you just created from the assembly you just built.
args
Is an array of arguments to pass to the method you're invoking.

The complete invocation of InvokeMember looks like this:

object[] arguments = new object[0];
object retVal =
    theType.InvokeMember("ComputeSum",
    BindingFlags.Default | 
    BindingFlags.InvokeMethod,
    null,
    theClass,
    arguments);
return (double) retVal;

The result of invoking this method is assigned to the local variable retVal, which is then returned, as a double, to the driver program. The complete listing is shown in Example 18-9.


Example 18-9: Dynamic invocation with Type and InvokeMethod( )

namespace Programming_CSharp
{
   using System;
   using System.Diagnostics;
   using System.IO;
   using System.Reflection;
 
   // 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;
      }
   }
 
   // responsible for creating the BruteForceSums
   // class and compiling it and invoking the
   // DoSums method dynamically
   public class ReflectionTest
   {
      // the public method called by the driver
      public double DoSum(int theValue)
      {
         // if you don't have a reference
         // to the dynamically created class
         // create it
         if (theType == null)
         {
            GenerateCode(theValue);
         }
 
         // with the reference to the dynamically 
         // created class you can invoke the method 
         object[] arguments = new object[0];
         object retVal =
            theType.InvokeMember("ComputeSum",
            BindingFlags.Default | 
            BindingFlags.InvokeMethod,
            null,
            theClass,
            arguments);
         return (double) retVal;
      }
 
      // generate the code and compile it
      private void GenerateCode(int theVal)
      {
         // open the file for writing
         string fileName = "BruteForceSums";
         Stream s = 
            File.Open(fileName + ".cs", FileMode.Create);
         StreamWriter wrtr = new StreamWriter(s);
         wrtr.WriteLine(
            "// Dynamically created BruteForceSums class");
 
         // create the class
         string className = "BruteForceSums";
         wrtr.WriteLine("class {0}", className);
         WriteLine("{");
 
         // create the method
         wrtr.WriteLine("\tpublic double ComputeSum(  )");
         wrtr.WriteLine("\t{");
         wrtr.WriteLine("\t// Brute force sum method");
         wrtr.WriteLine("\t// For value = {0}", theVal);
 
         // write the brute force additions
         wrtr.Write("\treturn 0");
         for (int i = 1;i<=theVal;i++)
         {
            wrtr.Write("+ {0}",i);
         }
         wrtr.WriteLine(";");    // finish method
         wrtr.WriteLine("\t}");    // end method
         wrtr.WriteLine("}");    // end class
 
         // close the writer and the stream
         wrtr.Close(  );
         s.Close(  );
         // Build the file
         ProcessStartInfo psi = 
            new ProcessStartInfo(  );
         psi.FileName = "cmd.exe";
 
         string compileString = "/c csc /optimize+ ";
         compileString += "/target:library ";
         compileString += "{0}.cs > compile.out";
 
         psi.Arguments = 
            String.Format(compileString, fileName);
         psi.WindowStyle = ProcessWindowStyle.Minimized;
 
         Process proc = Process.Start(psi);
         proc.WaitForExit(  );    // wait at most 2 seconds
 
         // Open the file, and get a 
         // pointer to the method info
         Assembly a = 
            Assembly.LoadFrom(fileName + ".dll");
         theClass = a.CreateInstance(className);
         theType = a.GetType(className);
         // File.Delete(fileName + ".cs");  // clean up
      }
      Type theType = null;
      object theClass = null;
   }
 
   public class TestDriver
   {
      public static void Main(  )
      {
         const int val = 200;  // 1..200
         const int iterations = 100000;
         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 (200) = 20100
Looping. Elapsed milliseconds: 
78.125 for 100000 iterations
Sum of (200) = 20100
Brute Force. Elapsed milliseconds: 
3843.75 for 100000 iterations

Notice that the dynamically invoked method is far slower than the loop. This is not a surprise; writing the file to disk, compiling it, reading it from disk, and invoking the method all bring significant overhead. You accomplished your goal, but it was a pyrrhic victory.

Pages: 1, 2, 3, 4, 5, 6, 7, 8, 9

Next Pagearrow