Previous section   Next section

18.2 Reflection

For the attributes in the metadata to be useful, you need a way to access them, ideally during runtime. The classes in the Reflection namespace, along with the System.Type and System.TypedReference classes, provide support for examining and interacting with the metadata.

Reflection is generally used for any of four tasks:

Viewing metadata

This might be used by tools and utilities that wish to display metadata.

Performing type discovery

This allows you to examine the types in an assembly and interact with or instantiate those types. This can be useful in creating custom scripts. For example, you might want to allow your users to interact with your program using a script language, such as JavaScript, or a scripting language you create yourself.

Late binding to methods and properties

This allows the programmer to invoke properties and methods on objects dynamically instantiated based on type discovery. This is also known as dynamic invocation .

Creating types at runtime (reflection emit)

The ultimate use of reflection is to create new types at runtime and then to use those types to perform tasks. You might do this when a custom class, created at runtime, will run significantly faster than more generic code created at compile time. An example is offered later in this chapter.

18.2.1 Viewing MetaData

In this section, you will use the Visual Basic .NET Reflection support to read the metadata in the MyMath class.

Start by initializing an object of the type MemberInfo. This object, in the System. Reflection namespace, is provided to discover the attributes of a member and to provide access to the metadata:

Dim inf As System.Reflection.MemberInfo = GetType(MyMath)

Call the GetType operator on the MyMath type, which returns an object of type Type, which derives from MemberInfo.

The Type class is the root of the reflection classes. Type encapsulates a representation of the type of an object. The Type class is the primary way to access metadata. Type derives from MemberInfo and encapsulates information about the members of a class (e.g., methods, properties, fields, events, etc.).

The next step is to call GetCustomAttributes on this MemberInfo object, passing in the type of the attribute you want to find. You get back an array of objects, each of type BugFixAttribute:

Dim attributes( ) As Attribute
attributes = inf.GetCustomAttributes(GetType(BugFixAttribute), False)

You can now iterate through this array, printing out the properties of the BugFixAttribute object. Example 18-2 replaces the Tester class from Example 18-1.

Example 18-2. Using reflection
Public Class Tester

    Public Shared Sub Main( )
        Dim mm As New MyMath( )
        Console.WriteLine("Calling DoFunc(7). Result: {0}", mm.DoFunc1(7))

        Dim inf As System.Reflection.MemberInfo = GetType(MyMath)

        Dim attributes( ) As Attribute
        attributes = _
           inf.GetCustomAttributes(GetType(BugFixAttribute), False)

        ' iterate through the attributes, retrieving the 
        ' properties
        Dim attribute As [Object]
        For Each attribute In attributes
            Dim bfa As BugFixAttribute = CType(attribute, BugFixAttribute)
            Console.WriteLine(ControlChars.Lf + "BugID: {0}", bfa.BugID)
            Console.WriteLine("Programmer: {0}", bfa.Programmer)
            Console.WriteLine("Date: {0}", bfa.theDate)
            Console.WriteLine("Comment: {0}", bfa.Comment)
        Next attribute
    End Sub 'Main
End Class 'Tester

When you put this replacement code into Example 18-1 and run it, you can see the metadata printed as you'd expect:

Output:
Calling DoFunc(7). Result: 9.33333333333333

BugID: 107
Programmer: Jesse Liberty
Date: 01/04/05
Comment: Fixed off by one errors

BugID: 121
Programmer: Jesse Liberty
Date: 01/03/05
Comment:

18.2.2 Type Discovery

You can use reflection to explore and examine the contents of an assembly. You can find the types associated with a module; the methods, fields, properties, and events associated with a type, as well as the signatures of each of the type's methods; the interfaces supported by the type; and the type's base class.

To start, load an assembly dynamically with the Assembly.Load( ) static method. The Assembly class encapsulates the actual assembly itself, for purposes of reflection. The signature for the Load( ) method is:

Overloads Public Shared Function Load(AssemblyName) As Assembly

For the next example, pass in the Core Library to the Load( ) method. MsCorLib.dll has the core classes of the .NET Framework. Because Assembly is both a keyword and a class name, you'll need to enclose the class name in square brackets:

Dim a As [Assembly] = [Assembly].Load("Mscorlib.dll")

Once the assembly is loaded, you can call GetTypes( ) to return an array of Type objects. The Type object is the heart of reflection. Type represents type declarations (classes, interfaces, arrays, values, and enumerations):

Dim theTypes As Type( ) = a.GetTypes( )

The assembly returns an array of types that you can display in a For Each loop, as shown in Example 18-3. Because this listing uses the Type class, you will want to add an Imports statement for the System.Reflection namespace.

Example 18-3. Reflecting on an assembly
Option Strict On
Imports System
Imports System.Reflection

Public Class Tester

    Public Shared Sub Main( )
        ' what is in the assembly
        Dim a As [Assembly] = [Assembly].Load("Mscorlib.dll")
        Dim theTypes As Type( ) = a.GetTypes( )
        Dim t As Type
        For Each t In theTypes
            Console.WriteLine("Type is {0}", t)
        Next t
        Console.WriteLine("{0} types found", theTypes.Length)
    End Sub 'Main
End Class 'Tester

The output from this would fill many pages. Here is a short excerpt:

Type is System.TypeCode
Type is System.Security.Util.StringExpressionSet
Type is System.Runtime.InteropServices.COMException
Type is System.Runtime.InteropServices.SEHException
Type is System.Reflection.TargetParameterCountException
Type is System.Text.UTF7Encoding
Type is System.Text.UTF7Encoding+Decoder
Type is System.Text.UTF7Encoding+Encoder
Type is System.ArgIterator
1430 types found

This example obtained an array filled with the types from the Core Library and printed them one by one. The array contained 1,430 entries on my machine.

18.2.3 Reflecting on a Type

You can reflect on a single type in the mscorlib assembly as well. To do so, extract a type from the assembly with GetType( ) method, as shown in Example 18-4.

Example 18-4. Reflecting on a type
Option Strict On
Imports System
Imports System.Reflection

Public Class Tester
    Public Shared Sub Main( )
        Dim t As Type = Type.GetType("System.Reflection.Assembly")
        Console.WriteLine("Single type is {0}", t)
    End Sub 'Main
End Class 'Tester
18.2.3.1 Finding all type members

You can ask the Assembly type for all its members using the GetMembers( ) method of the Type class, which lists all the methods, properties, and fields, as shown in Example 18-5.

Example 18-5. Reflecting on the members of a type
Option Strict On
Imports System
Imports System.Reflection

Public Class Tester
    Public Shared Sub Main( )
        Dim t As Type = Type.GetType("System.Reflection.Assembly")
        Console.WriteLine("Single type is {0}", t)
        Dim mbrInfoArray As MemberInfo( ) = t.GetMembers( )
        Dim inf As MemberInfo
        For Each inf In mbrInfoArray
            Console.WriteLine("{0} is a {1}", _
                inf, inf.MemberType)
        Next
    End Sub 'Main
End Class 'Tester

Once again the output is quite lengthy, but within the output you see fields, methods, constructors, and properties, as shown in this excerpt:

Boolean IsDefined(System.Type, Boolean) is a Method
System.Object<> GetCustomAttributes(Boolean) is a Method
System.Object<> GetCustomAttributes(System.Type, Boolean) is a Method
System.Security.Policy.Evidence get_Evidence( ) is a Method
System.String get_Location( ) is a Method
18.2.3.2 Finding type methods

You might want to focus on methods only, excluding the fields, properties, and so forth. To do so, remove the call to GetMembers( ):

Dim mbrInfoArray As MemberInfo( ) = t.GetMembers( )

and add a call to GetMethods( ):

Dim mbrInfoArray As MemberInfo( ) = t.GetMethods( )

The output now is nothing but the methods:

Output (excerpt):
Boolean Equals(System.Object) is a Method
System.String ToString( ) is a Method
System.String CreateQualifiedName(
      System.String, System.String) is a Method
Boolean get_GlobalAssemblyCache( ) is a Method
18.2.3.3 Finding particular type members

Finally, to narrow it down even further, you can use the FindMembers method to find particular members of the type. For example, you can narrow your search to methods whose names begin with "Get".

To narrow the search, use the FindMembers method, which takes four parameters: MemberTypes, BindingFlags, MemberFilter, and object.

MemberTypes

A MemberTypes object that indicates the type of the member to search for. These include All, Constructor, Custom, Event, Field, Method, NestedType, Property, and TypeInfo. You will also use the MemberTypes method to find a method.

BindingFlags

An enumeration that controls the way searches are conducted by reflection. There are a great many BindingFlag values, including IgnoreCase, Instance, Public, Static, and so forth.

MemberFilter

A delegate (see Chapter 12) that is used to filter the list of members in the MemberInfo array of objects. The filter you'll use is Type.FilterName, a field of the Type class used for filtering on a name.

Object

A string value that will be used by the filter. In this case you'll pass in "Get*" to match only those methods that begin with "Get".

The complete listing for filtering on these methods is shown in Example 18-6.

Example 18-6. Finding particular methods
Option Strict On
Imports System
Imports System.Reflection

Public Class Tester
    Public Shared Sub Main( )
        Dim t As Type = Type.GetType("System.Reflection.Assembly")

        Dim mbrInfoArray As MemberInfo( ) = t.FindMembers( _
            MemberTypes.Method, _
            BindingFlags.Public Or _
            BindingFlags.Static Or _
            BindingFlags.NonPublic Or _
            BindingFlags.Instance Or _
            BindingFlags.DeclaredOnly, _
            Type.FilterName, "Get*")

        Dim inf As MemberInfo
        For Each inf In mbrInfoArray
            Console.WriteLine("{0} is a {1}", _
                inf, inf.MemberType)
        Next
    End Sub 'Main
End Class 'Tester

Here is an excerpt of the output:

System.Object[] GetCustomAttributes(Boolean) is a Method
System.Object[] GetCustomAttributes(System.Type, Boolean) is a Method
System.IO.Stream GetManifestResourceStream(System.String, System.Threading.
StackCrawlMark ByRef, Boolean) is a Method
GetManifestResourceInfo(System.String) is a Method
System.String[] GetManifestResourceNames( ) is a Method
System.IO.FileStream[] GetFiles(Boolean) is a Method
System.IO.FileStream[] GetFiles( ) is a Method
System.IO.FileStream GetFile(System.String) is a Method
System.String GetFullName( ) is a Method
Byte* GetResource(System.String, UInt64 ByRef, System.Threading.StackCrawlMark ByRef, 
Boolean) is a Method

18.2.4 Late Binding

Once you have discovered a method, it's possible to invoke it using reflection. For example, you might like to invoke the Cos( ) method of System.Math, which returns the cosine of an angle.

You could, of course, call Cos( ) in the normal course of your code, but reflection allows you to bind to that method at runtime. This is called late-binding and offers the flexibility of choosing at runtime which object you will bind to and invoking it programmatically. This can be useful when creating a custom script to be run by the user or when working with objects that might not be available at compile time. For example, by using late-binding, your program can interact with the spellchecker or other components of a running commercial word processing program such as Microsoft Word.

To invoke Cos( ), you will first get the Type information for the System.Math class:

Dim theMathType As Type = Type.GetType("System.Math")

With that type information, you could dynamically load an instance of a class by using a static method of the Activator class. Since Cos( ) is static, you don't need to construct an instance of System.Math (and you can't, since System.Math has no public constructor).

The Activator class contains four methods, all static, which you can use to create objects locally or remotely, or to obtain references to existing objects. The four methods are CreateComInstanceFrom, CreateInstanceFrom, GetObject, and CreateInstance:

CreateComInstanceFrom

Used to create instances of COM objects.

CreateInstanceFrom

Used to create a reference to an object from a particular assembly and type name.

GetObject

Used when marshaling objects. Marshaling is discussed in detail in Chapter 19.

CreateInstance

Used to create local or remote instances of an object.

For example:

Dim theObj As Object = Activator.CreateInstance(someType)

Back to the Cos( ) example, you now have one object in hand: a Type object named theMathType, which you created by calling GetType.

Before you can invoke a method on the object, you must get the method you need from the Type object, theMathType. To do so, you'll call GetMethod( ), and you'll pass in the signature of the Cos method.

The signature, you will remember, is the name of the method (Cos) and its parameter types. In the case of Cos( ), there is only one parameter: a double. However, Type.GetMethod takes two parameters. The first represents the name of the method you want, and the second represents the parameters. The name is passed as a string; the parameters are passed as an array of types:

Dim ConsineInfo As MethodInfo = _
    theMathType.GetMethod("Cos", paramTypes)

Before calling GetMethod, you must prepare the array of types:

Dim paramTypes(0) As Type
paramTypes(0) = Type.GetType("System.Double")

This code declares the array of Type objects and then fills the first element (paramTypes(0)) with a Type representing a Double. Obtain the type representing a Double by calling the static method Type.GetType( ), and passing in the string "System.Double".

You now have an object of type MethodInfo on which you can invoke the method. To do so, you must pass in the object to invoke the method on and the actual value of the parameters, again in an array. Since this is a static method, pass in theMathType. (If Cos( ) was an instance method, you could use theObj instead of theMathType.)

Dim parameters(0) As Object
parameters(0) = 45 * (Math.PI / 180) '45 degrees in radians
Dim returnVal As Object = _
    ConsineInfo.Invoke(theMathType, parameters)

Note that you've created two arrays. The first, paramTypes, holds the type of the parameters. The second, parameters, holds the actual value. If the method had taken two arguments, you'd have declared these arrays to hold two values.

Example 18-7 illustrates dynamically calling the Cos( ) method.

Example 18-7. Dynamically invoking a method
Option Strict On
Imports System
Imports System.Reflection

Public Class Tester
    Public Shared Sub Main( )

        ' Sine System.Math has no public constructor you can not call
        ' Activator.CreateInstance(theMathType)
        Dim theMathType As Type = Type.GetType("System.Math")

        Dim paramTypes(0) As Type
        paramTypes(0) = Type.GetType("System.Double")

        Dim ConsineInfo As MethodInfo = _
            theMathType.GetMethod("Cos", paramTypes)

        Dim parameters(0) As Object
        parameters(0) = 45 * (Math.PI / 180) '45 degrees in radians
        Dim returnVal As Object

        returnVal = ConsineInfo.Invoke(theMathType, parameters)
        Console.WriteLine("The cosine of a 45 degree angle is {0}", _
            returnVal)

    End Sub 'Main
End Class 'Tester

That was a lot of work just to invoke a single method. The power, however, is that you can use reflection to discover an assembly on the user's machine, to query what methods are available, and to invoke one of those members dynamically!

18.2.5 Reflection Emit

So far we've seen reflection used for three purposes: viewing metadata, type discovery, and dynamic invocation. You might use these techniques when building tools (such as a development environment) or when processing scripts. The most powerful use of reflection, however, is with reflection emit.

Reflection emit supports the dynamic creation of new types at runtime. You can define an assembly to run dynamically or to save itself to disk, and you can define modules and new types with methods that you can then invoke.

The use of dynamic invocation and reflection emit should be considered an advanced topic. Most developers will never have need to use reflection emit. This demonstration is based on an example provided at a Microsoft Author's Summit.

To understand the power of reflection emit, you must first consider a slightly more complicated example of dynamic invocation.

Problems can have general solutions that are relatively slow and specific solutions that are fast. To keep things manageably simple, consider a DoSum( ) method, which provides the sum of a string of integers from 1 to n, where n will be supplied by the user.

Thus, DoSum(3) is equal to 1+2+3, or 6. DoSum(10) is 55. Writing this in Visual Basic .NET is very simple:

Public Function DoSum1(n As Integer) As Integer
   Dim result As Integer = 0
   Dim i As Integer
   For i = 1 To n
      result += i
   Next i
   Return result
End Function 'DoSum1

The method simply loops, adding the requisite number. If you pass in 3, the method adds 1 + 2 + 3 and returns an answer of 6.

With large numbers, and when run many times, this might be a bit slow. Given the value 20, this method would be considerably faster if you removed the loop:

Public Function DoSum2( ) As Integer
   Return 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + _
    15 + 16 + 17 + 18 + 19 + 20
End Function 'DoSum2

DoSum2 runs more quickly than DoSum1 does. How much more quickly? To find out, you'll need to put a timer on both methods. To do so, use a DateTime object to mark the start time and a TimeSpan object to compute the elapsed time.

For this experiment, you need to create two DoSum( ) methods; the first will use the loop and the second will not. Call each 1,000,000 times. (Computers are very fast, so to see a difference you have to work hard!) Then compare the times. Example 18-8 illustrates the entire test program.

Example 18-8. Comparing loop to brute force
Option Strict On
Imports System
Imports System.Diagnostics
Imports System.Threading

Public Class MyMath

    ' sum numbers with a loop
    Public Function DoSum(ByVal n As Integer) As Integer
        Dim result As Integer = 0
        Dim i As Integer
        For i = 1 To n
            result += i
        Next i
        Return result
    End Function 'DoSum

    ' brute force by hand
    Public Function DoSum2( ) As Integer
        Return 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 _
        + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20
    End Function 'DoSum2
End Class 'MyMath

Public Class TestDriver

    Public Shared Sub Main( )

        Const val As Integer = 20 ' val to sum
        ' 1,000,000 iterations
        Const iterations As Integer = 1000000

        ' hold the answer
        Dim result As Integer = 0

        Dim m As New MyMath( )

        ' mark the start time   
        Dim startTime As DateTime = DateTime.Now

        ' run the experiment
        Dim i As Integer
        For i = 0 To iterations - 1
            result = m.DoSum(val)
        Next i
        
        ' mark the elapsed time
        Dim elapsed As TimeSpan = DateTime.Now.Subtract(startTime)

        ' display the results
        Console.WriteLine("Loop: Sum of ({0}) = {1}", val, result)
        Console.WriteLine(("The elapsed time in milliseconds is: " _
          & elapsed.TotalMilliseconds.ToString( )))

        ' mark a new start time
        startTime = DateTime.Now

        ' run the experiment
        Dim j As Integer
        For j = 0 To iterations - 1
            result = m.DoSum2( )
        Next j

        ' mark the new elapsed time
        elapsed = DateTime.Now.Subtract(startTime)

        ' display the results
        Console.WriteLine( _
          "Brute Force: Sum of ({0}) = {1}", val, result)
        Console.WriteLine("The elapsed time in milliseconds is: " _
          & elapsed.TotalMilliseconds.ToString( ))
    End Sub 'Main
End Class 'TestDriver

As you can see, both methods returned the same answer (one million times!), but the brute-force method was six times faster.

Is there a way to avoid the loop and still provide a general solution? In traditional programming, the answer would be no, but with reflection you do have one other option. You can, at runtime, take the value the user wants (20, in this case) and write out to disk a class that implements the brute-force solution. You can then use dynamic invocation to invoke that method.

There are at least three ways to achieve this result, each increasingly elegant. The third, reflection emit, is the best, but a close look at two other techniques is instructive. If you are pressed for time, you might wish to jump ahead to the section entitled Section 18.2.5.3.

18.2.5.1 Dynamic invocation with InvokeMember( )

The first approach will be to dynamically create a class named BruteForceSums 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.vb 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:

Private theType As Type = Nothing
Private theClass As Object = Nothing

The first is an object of type Type, which you use to load your class from disk; 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 nothing; 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 .vb 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, load the assembly from disk and assign the class type information to theType�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 val As Integer = 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:

Dim m As New MyMath( )

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

Public Class MyMath

    ' sum numbers with a loop
    Public Function DoSumLooping(ByVal initialVal As Integer) As Integer
        Dim result As Integer = 0
        Dim i As Integer
        For i = 1 To initialVal
            result += i
        Next i
        Return result
    End Function 'DoSumLooping
End Class 'MyMath

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:

Dim t As New ReflectionTest( )
result = t.DoSum(val)

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

If theType Is Nothing Then
    GenerateCode(theValue)
End If

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

Dim fileName As String = "BruteForceSums"
Dim s As Stream = File.Open(fileName & ".vb", FileMode.Create)

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

Dim wrtr As 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 comment:

' Dynamically created BruteForceSums class

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

Dim className As String = "BruteForceSums"
wrtr.WriteLine("class {0}", className)

Within the definition of the class, create the ComputeSum method:

wrtr.WriteLine(ControlChars.Tab & _
  "public Function ComputeSum( ) as Integer")
wrtr.WriteLine(ControlChars.Tab & "' Brute force sum method")
wrtr.WriteLine(ControlChars.Tab & "' 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(ControlChars.Tab + "return 0")
Dim i As Integer
For i = 1 To theVal
    wrtr.Write("+ {0}", i)
Next i

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

ControlChars.Tab return 0+ 1+ 2+ 3+...

The initial ControlChars.Tab causes the code to be indented in the source file.

When the loop completes, end the return statement and end the method and then end the class:

wrtr.WriteLine("")
wrtr.WriteLine(ControlChars.Tab & "End Function") ' end method
wrtr.WriteLine("End Class") ' end class

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

wrtr.Close( )
s.Close( )

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

' Dynamically created BruteForceSums class
class BruteForceSums
    public Function ComputeSum( ) as Integer
    ' 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
    End Function
End Class

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 class that will hold the command line. Instantiate a ProcessStartInfo and set its filename to cmd.exe:

Dim psi As 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. The command for cmd.exe is the command-line compiler:

Dim compileString As String = "/c {0}vbc /optimize+ "
compileString += "/target:library "
compileString += "{1}.vb > compile.out"

The string compileString will invoke the Visual Basic .NET compiler (vbc), 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). Redirect the output of the compile to a file named compile.out so that you can examine it if there are errors.

Combine compileString with the filename, using the static method Format of the string class, and assign the combined string to psi.Arguments. The first placeholder, {0}, holds the location of the compiler (%SystemRoot%\Microsoft.NET\Framework\<version>), and the second placeholder, {1}, holds the source code filename:

Dim frameworkDir As String = RuntimeEnvironment.GetRuntimeDirectory( )
psi.Arguments = String.Format(compileString, frameworkDir, fileName)

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

/c vbc /optimize+ /target:library 
BruteForceSums.vb > compile.out

Before invoking cmd.exe, 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�wait until it finishes before proceeding with the rest of the GenerateCode method:

Dim proc As Process = Process.Start(psi)
proc.WaitForExit( )

Once the process is done, you can get the assembly; 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:

Dim a As [Assembly] = [Assembly].LoadFrom((fileName & ".dll"))
theClass = a.CreateInstance(className)
theType = a.GetType(className)

You can now delete the .vb file you generated:

File.Delete(fileName & ".vb")

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:

Overloads Public Function InvokeMember( _
   ByVal name As String, _
   ByVal invokeAttr As BindingFlags, _
   ByVal binder As Binder, _
   ByVal target As Object, _
   ByVal args( ) As Object _
) As Object
name

The name of the method you wish to invoke.

invokeAttr

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 flag. These are the standard flags for invoking a method dynamically.

binder

Used to assist in type conversions. By passing in nothing, you'll specify that you want the default binder.

target

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

An array of arguments to pass to the method you're invoking.

The complete invocation of InvokeMember looks like this:

Dim arguments( ) As Object
Dim retVal As Object = _
 theType.InvokeMember( _
    "ComputeSum", _
    BindingFlags.Default Or BindingFlags.InvokeMethod, _
    Nothing, theClass, arguments)

    Return CDbl(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( )
Option Strict On
Imports System
Imports System.Diagnostics
Imports System.IO
Imports System.Reflection
Imports System.Runtime.InteropServices ' provides RuntimeEnvironment

' benchmark the looping approach
Public Class MyMath

    ' sum numbers with a loop
    Public Function DoSumLooping(ByVal initialVal As Integer) As Integer
        Dim result As Integer = 0
        Dim i As Integer
        For i = 1 To initialVal
            result += i
        Next i
        Return result
    End Function 'DoSumLooping
End Class 'MyMath

' responsible for creating the BruteForceSums
' class and compiling it and invoking the
' DoSums method dynamically
Public Class ReflectionTest

    Private theType As Type = Nothing
    Private theClass As Object = Nothing

    ' the public method called by the driver
    Public Function DoSum(ByVal theValue As Integer) As Double
        ' if you don't have a reference
        ' to the dynamically created class
        ' create it
        If theType Is Nothing Then
            GenerateCode(theValue)
        End If

        ' with the reference to the dynamically 
        ' created class you can invoke the method 
        Dim arguments( ) As Object
        Dim retVal As Object = _
         theType.InvokeMember( _
            "ComputeSum", _
            BindingFlags.Default Or BindingFlags.InvokeMethod, _
            Nothing, theClass, arguments)
        Return CDbl(retVal)
    End Function 'DoSum


    ' generate the code and compile it
    Private Sub GenerateCode(ByVal theVal As Integer)
        ' open the file for writing
        Dim fileName As String = "BruteForceSums"
        Dim s As Stream = File.Open(fileName & ".vb", FileMode.Create)
        Dim wrtr As New StreamWriter(s)
        wrtr.WriteLine(" ' Dynamically created BruteForceSums class")

        ' create the class
        Dim className As String = "BruteForceSums"
        wrtr.WriteLine("class {0}", className)

        ' create the method
        wrtr.WriteLine(ControlChars.Tab & _
          "public Function ComputeSum( ) as Integer")
        wrtr.WriteLine(ControlChars.Tab & "' Brute force sum method")
        wrtr.WriteLine(ControlChars.Tab & "' For value = {0}", theVal)

        ' write the brute force additions
        wrtr.Write(ControlChars.Tab & "return 0")
        Dim i As Integer
        For i = 1 To theVal
            wrtr.Write("+ {0}", i)
        Next i
        wrtr.WriteLine("")
        wrtr.WriteLine(ControlChars.Tab & "End Function") ' end method
        wrtr.WriteLine("End Class") ' end class
        ' close the writer and the stream
        wrtr.Close( )
        s.Close( )

        ' Build the file
        Dim psi As New ProcessStartInfo( )
        psi.FileName = "cmd.exe"

        Dim compileString As String = "/c {0}vbc /optimize+ "
        compileString += "/target:library "
        compileString += "{1}.vb > compile.out"

        Dim frameworkDir As String = _
        RuntimeEnvironment.GetRuntimeDirectory( )
        psi.Arguments = [String].Format(compileString, _
            frameworkDir, fileName)
        psi.WindowStyle = ProcessWindowStyle.Minimized

        Dim proc As Process = Process.Start(psi)
        proc.WaitForExit( )

        ' Open the file, and get a 
        ' pointer to the method info
        Dim a As [Assembly] = [Assembly].LoadFrom((fileName & ".dll"))
        theClass = a.CreateInstance(className)
        theType = a.GetType(className)
        ' File.Delete(fileName + ".vb")
    End Sub 'GenerateCode 
End Class 'ReflectionTest

Public Class TestDriver

    Public Shared Sub Main( )
        Const val As Integer = 200 ' 1..200
        Const iterations As Integer = 100000
        Dim result As Double = 0

        ' run the benchmark
        Dim m As New MyMath( )
        Dim startTime As DateTime = DateTime.Now
        Dim i As Integer
        For i = 0 To iterations - 1
            result = m.DoSumLooping(val)
        Next i
        Dim elapsed As TimeSpan = DateTime.Now.Subtract(startTime)
        Console.WriteLine("Sum of ({0}) = {1}", val, result)
        Console.WriteLine("Looping. Elapsed milliseconds: " & _
        elapsed.TotalMilliseconds.ToString( ) _
          & " for {0} iterations", iterations)

        ' run our reflection alternative
        Dim t As New ReflectionTest( )

        startTime = DateTime.Now
        For i = 0 To iterations - 1
            result = t.DoSum(val)
        Next i

        elapsed = DateTime.Now.Subtract(startTime)
        Console.WriteLine("Sum of ({0}) = {1}", val, result)
        Console.WriteLine("Brute Force. Elapsed milliseconds: " & _
          elapsed.TotalMilliseconds.ToString( ) & _
        " for {0} iterations", iterations)
    End Sub 'Main
End Class 'TestDriver

Output:
Sum of (200) = 20100
Looping. Elapsed milliseconds: 46.875 for 100000 iterations
Sum of (200) = 20100
Brute Force. Elapsed milliseconds: 1406.25 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.

18.2.5.2 Dynamic invocation with interfaces

It turns out that dynamic invocation is particularly slow. You want to maintain the general approach of writing the class at runtime and compiling it on the fly. But rather than using dynamic invocation, you'd just like to call the method. One way to speed things up is to use an interface to call the ComputeSums( ) method directly.

To accomplish this, you need to change ReflectionTest.DoSum( ) from:

Public Function DoSum(ByVal theValue As Integer) As Double
    ' if you don't have a reference
    ' to the dynamically created class
    ' create it
    If theType Is Nothing Then
        GenerateCode(theValue)
    End If

    ' with the reference to the dynamically 
    ' created class you can invoke the method 
    Dim arguments( ) As Object
    Dim retVal As Object = _
     theType.InvokeMember( _
        "ComputeSum", _
        BindingFlags.Default Or BindingFlags.InvokeMethod, _
        Nothing, theClass, arguments)
    Return CDbl(retVal)
End Function 'DoSum

to the following:

Public Function DoSum(ByVal theValue As Integer) As Double
     If theComputer Is Nothing Then
         GenerateCode(theValue)
     End If

     Return theComputer.ComputeSum( )
 End Function 'DoSum

In this example, theComputer is an interface to an object of type BruteForceSums. It must be an interface and not an object because when you compile this program, theComputer won't yet exist; you'll create it dynamically.

Remove the declarations for theType and theClass and replace them with:

Private theComputer As IComputer = Nothing

This declares theComputer to be an IComputer interface. At the top of your program, declare the interface:

Public Interface IComputer
     Function ComputeSum( ) As Integer
 End Interface

When you create the BruteForceSum class, you must make it implement IComputer:

Dim className As String = "BruteForceSums"
wrtr.WriteLine("class {0}", className)
wrtr.WriteLine( _
    "Implements ReflectionEmit2.Programming_VBNET.IComputer")

You must also modify the implementation of ComputeSums to indicate that it implements the method from the interface:

wrtr.WriteLine( _
  ControlChars.Tab & _
  "Public Function ComputeSum( ) as Integer _ ")
wrtr.WriteLine( _
"Implements ReflectionEmit2.Programming_VBNET.IComputer.ComputeSum ")

Save your program in a project file named ReflectionEmit2, and modify compileString in GenerateCode as follows:

Dim compileString As String = "/c {0}vbc /optimize+ "
compileString += "/r:ReflectionEmit2.exe "
compileString += "/target:library "
compileString += "{1}.vb > compile.out"

The compile string will need to reference the ReflectionEmit2 program itself (ReflectionEmit2.exe) so that the dynamically called compiler will know where to find the declaration of IComputer.

After you build the assembly, you will no longer assign the instance to theClass and then get the type for theType, as these variables are gone. Instead, you will assign the instance to the interface IComputer:

theComputer = a.CreateInstance(className)

Use the interface to invoke the method directly in DoSum:

Return theComputer.ComputeSum( )

Example 18-10 is the complete source code.

Example 18-10. Dynamic invocation with interfaces
Option Strict On
Imports System
Imports System.Diagnostics
Imports System.IO
Imports System.Reflection
Imports System.Runtime.InteropServices ' provides RuntimeEnvironment

Namespace Programming_VBNET

    Public Interface IComputer
        Function ComputeSum( ) As Integer
    End Interface

    ' benchmark the looping approach
    Public Class MyMath

        ' sum numbers with a loop
        Public Function DoSumLooping(ByVal initialVal As Integer) _
            As Integer
            Dim result As Integer = 0
            Dim i As Integer
            For i = 1 To initialVal
                result += i
            Next i
            Return result
        End Function 'DoSumLooping
    End Class 'MyMath


    ' responsible for creating the BruteForceSums
    ' class and compiling it and invoking the
    ' DoSums method dynamically
    Public Class ReflectionTest

        Private theComputer As IComputer = Nothing

        ' the public method called by the driver
        Public Function DoSum(ByVal theValue As Integer) As Double
            If theComputer Is Nothing Then
                GenerateCode(theValue)
            End If

            Return theComputer.ComputeSum( )
        End Function 'DoSum

        ' generate the code and compile it
        Private Sub GenerateCode(ByVal theVal As Integer)
            ' open the file for writing
            Dim fileName As String = "BruteForceSums"
            Dim s As Stream = File.Open(fileName & ".vb", FileMode.Create)
            Dim wrtr As New StreamWriter(s)
            wrtr.WriteLine(" ' Dynamically created BruteForceSums class")

            ' create the class
            Dim className As String = "BruteForceSums"
            wrtr.WriteLine("class {0}", className)
            wrtr.WriteLine( _
                "Implements ReflectionEmit2.Programming_VBNET.IComputer")


            ' create the method
            wrtr.WriteLine( _
                ControlChars.Tab + _
             "Public Function ComputeSum( ) as Integer _ ")
            wrtr.WriteLine( _
      "Implements ReflectionEmit2.Programming_VBNET.IComputer.ComputeSum ")
            wrtr.WriteLine( _
                ControlChars.Tab & "' Brute force sum method")
            wrtr.WriteLine( _
                ControlChars.Tab & "' For value = {0}", theVal)

            ' write the brute force additions
            wrtr.Write(ControlChars.Tab & "return 0")
            Dim i As Integer
            For i = 1 To theVal
                wrtr.Write("+ {0}", i)
            Next i
            wrtr.WriteLine("")
            wrtr.WriteLine(ControlChars.Tab & "End Function") ' end method
            wrtr.WriteLine("End Class") ' end class
            ' close the writer and the stream
            wrtr.Close( )
            s.Close( )

            ' Build the file
            Dim psi As New ProcessStartInfo( )
            psi.FileName = "cmd.exe"

            Dim compileString As String = "/c {0}vbc /optimize+ "
            compileString += "/r:ReflectionEmit2.exe "
            compileString += "/target:library "
            compileString += "{1}.vb > compile.out"

            Dim frameworkDir As String = _
            RuntimeEnvironment.GetRuntimeDirectory( )
            Dim args As String = _
            [String].Format(compileString, frameworkDir, fileName)
            psi.Arguments = args
            psi.WindowStyle = ProcessWindowStyle.Minimized

            Dim proc As Process = Process.Start(psi)
            proc.WaitForExit( )

            ' Open the file, and get a 
            ' pointer to the method info
            Dim a As [Assembly] = [Assembly].LoadFrom((fileName & ".dll"))
            theComputer = a.CreateInstance(className)
            ' File.Delete(fileName & ".vb")
        End Sub 'GenerateCode 
    End Class 'ReflectionTest

    Public Class TestDriver

        Public Shared Sub Main( )
            Const val As Integer = 200 ' 1..200
            Const iterations As Integer = 100000
            Dim result As Double = 0

            ' run the benchmark
            Dim m As New MyMath( )
            Dim startTime As DateTime = DateTime.Now
            Dim i As Integer
            For i = 0 To iterations - 1
                result = m.DoSumLooping(val)
            Next i
            Dim elapsed As TimeSpan = DateTime.Now.Subtract(startTime)
            Console.WriteLine("Sum of ({0}) = {1}", val, result)
            Console.WriteLine("Looping. Elapsed milliseconds: " & _
                elapsed.TotalMilliseconds.ToString( ) _
              & " for {0} iterations", iterations)

            ' run our reflection alternative
            Dim t As New ReflectionTest( )

            startTime = DateTime.Now
            For i = 0 To iterations - 1
                result = t.DoSum(val)
            Next i

            elapsed = DateTime.Now.Subtract(startTime)
            Console.WriteLine("Sum of ({0}) = {1}", val, result)
            Console.WriteLine("Brute Force. Elapsed milliseconds: " & _
              elapsed.TotalMilliseconds.ToString( ) & _
                " for {0} iterations", iterations)
        End Sub 'Main
    End Class 'TestDriver
End Namespace

Output:
Sum of (200) = 20100
Looping. Elapsed milliseconds: 46.875 for 100000 iterations
Sum of (200) = 20100
Brute Force. Elapsed milliseconds: 375 for 100000 iterations

This output is much more satisfying; our dynamically created brute-force method now runs a bit faster, but still not as fast as the loop does. The solution to this is to use reflection emit.

18.2.5.3 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; when you're done compiling, you have IL (Intermediate Language) op codes on disk that 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 Visual Basic .NET 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. 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:

Dim t As New ReflectionTest( )
result = t.DoSum(val)

DoSum itself is virtually unchanged:

Public Function DoSum(ByVal theValue As Integer) As Double
    If theComputer Is Nothing Then
        GenerateCode(theValue)
    End If

    ' call the method through the interface
    Return theComputer.ComputeSum( )
End Function 'DoSum

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 Sub GenerateCode(ByVal theValue As Integer)
    Dim theAssembly As [Assembly] = EmitAssembly(theValue)
    theComputer = _
        CType(theAssembly.CreateInstance("BruteForceSums"), _
        IComputer)
End Sub 'GenerateCode

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

Private Function EmitAssembly(ByVal theValue As Integer) As [Assembly]

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

Dim assemblyName As New AssemblyName( )
assemblyName.Name = "DoSumAssembly"

An AssemblyName is an object that fully describes an assembly's unique identity. As discussed in Chapter 17, 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, call DefineDynamicAssembly on the current domain, which is done 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:

Dim newAssembly As AssemblyBuilder = _
    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. Call the DefineDynamicModule method, passing in the name of the method you want to create:

Dim newModule As ModuleBuilder = _
    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:

Dim myType As TypeBuilder = _
    newModule.DefineType("BruteForceSums", TypeAttributes.Public)

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

myType.AddInterfaceImplementation(GetType(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, create an array of zero length:

Dim paramTypes( ) As Type

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

Dim returnType As Type = GetType(Integer)

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:

Dim simpleMethod As MethodBuilder = _
    myType.DefineMethod("ComputeSum", MethodAttributes.Public _
    Or MethodAttributes.Virtual, returnType, paramTypes)

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

Then use the MethodBuilder object you created to get an ILGenerator object:

Dim generator As ILGenerator = _
    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 Visual Basic .NET compiler would have created. (In fact, the best way to get the op codes is to write a small Visual Basic .NET 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)
Dim i As Integer
For i = 1 To theValue
    generator.Emit(OpCodes.Ldc_I4, i)
    generator.Emit(OpCodes.Add)
Next i

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:

Dim computeSumInfo As MethodInfo = _
   GetType(IComputer).GetMethod("ComputeSum")

Now you must specify the implementation that will implement the method. 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. Example 18-11 is the full source code.

Example 18-11. Dynamic invocation with reflection emit
Option Strict On
Imports System
Imports System.Diagnostics
Imports System.IO
Imports System.Reflection
Imports System.Reflection.Emit
Imports System.Runtime.InteropServices ' provides RuntimeEnvironment
Imports System.Threading

Namespace Programming_VBNET

    Public Interface IComputer
        Function ComputeSum( ) As Integer
    End Interface

    ' benchmark the looping approach
    Public Class MyMath
        ' sum numbers with a loop
        Public Function DoSumLooping(ByVal initialVal As Integer) _
            As Integer
            Dim result As Integer = 0
            Dim i As Integer
            For i = 1 To initialVal
                result += i
            Next i
            Return result
        End Function 'DoSumLooping
    End Class 'MyMath

    ' responsible for creating the BruteForceSums
    ' class and compiling it and invoking the
    ' DoSums method dynamically
    Public Class ReflectionTest

        Private theComputer As IComputer = Nothing
        ' the private method which emits the assembly
        ' using op codes
        Private Function EmitAssembly( _
        ByVal theValue As Integer) As [Assembly]
            ' Create an assembly name
            Dim assemblyName As New AssemblyName( )
            assemblyName.Name = "DoSumAssembly"

            ' Create a new assembly with one module
            Dim newAssembly As AssemblyBuilder = _
                Thread.GetDomain( ).DefineDynamicAssembly( _
                    assemblyName, AssemblyBuilderAccess.Run)
            Dim newModule As ModuleBuilder = _
                newAssembly.DefineDynamicModule("Sum")

            '  Define a public class named "BruteForceSums " 
            '  in the assembly.
            Dim myType As TypeBuilder = _
                newModule.DefineType( _
            "BruteForceSums", TypeAttributes.Public)

            ' Mark the class as implementing IComputer.
            myType.AddInterfaceImplementation(GetType(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.
            Dim paramTypes( ) As Type
            Dim returnType As Type = GetType(Integer)
            Dim simpleMethod As MethodBuilder = _
                myType.DefineMethod("ComputeSum", MethodAttributes.Public _
                Or MethodAttributes.Virtual, returnType, paramTypes)

            ' Get an ILGenerator. This is used
            ' to emit the IL that you want.
            Dim generator As ILGenerator = _
                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)
            Dim i As Integer
            For i = 1 To theValue
                generator.Emit(OpCodes.Ldc_I4, i)
                generator.Emit(OpCodes.Add)
            Next i

            ' return the value
            generator.Emit(OpCodes.Ret)

            'Encapsulate information about the method and
            'provide access to the method's metadata
            Dim computeSumInfo As MethodInfo = _
                GetType(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
        End Function 'EmitAssembly

        ' check if the interface is nothing
        ' if so, call Setup.
        Public Function DoSum(ByVal theValue As Integer) As Double
            If theComputer Is Nothing Then
                GenerateCode(theValue)
            End If

            ' call the method through the interface
            Return theComputer.ComputeSum( )
        End Function 'DoSum

        ' emit the assembly, create an instance 
        ' and get the interface
        Public Sub GenerateCode(ByVal theValue As Integer)
            Dim theAssembly As [Assembly] = EmitAssembly(theValue)
            theComputer = _
                CType(theAssembly.CreateInstance("BruteForceSums"), _
                IComputer)
        End Sub 'GenerateCode

        ' private member data

    End Class 'ReflectionTest

    Public Class TestDriver

        Public Shared Sub Main( )
            Const val As Integer = 200 ' 1..200
            Const iterations As Integer = 100000
            Dim result As Double = 0

            ' run the benchmark
            Dim m As New MyMath( )
            Dim startTime As DateTime = DateTime.Now
            Dim i As Integer
            For i = 0 To iterations - 1
                result = m.DoSumLooping(val)
            Next i
            Dim elapsed As TimeSpan = DateTime.Now.Subtract(startTime)
            Console.WriteLine("Sum of ({0}) = {1}", val, result)
            Console.WriteLine("Looping. Elapsed milliseconds: " & _
                elapsed.TotalMilliseconds.ToString( ) & _
                  " for {0} iterations", iterations)

            ' run our reflection alternative
            Dim t As New ReflectionTest( )
            startTime = DateTime.Now
            For i = 0 To iterations - 1
                result = t.DoSum(val)
            Next i
            elapsed = DateTime.Now.Subtract(startTime)
            Console.WriteLine("Sum of ({0}) = {1}", val, result)
            Console.WriteLine("Brute Force. Elapsed milliseconds: " & _
              elapsed.TotalMilliseconds.ToString( ) & _
                " for {0} iterations", iterations)
        End Sub 'Main
    End Class 'TestDriver
End Namespace

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.


  Previous section   Next section
Top