Microsoft & .NETSpeed Up Your Reflection Processes

Speed Up Your Reflection Processes

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Introduction

I write about real problems that real programmers have, and in so doing I try to show you, the reader, alternate solutions that provide you with options. Hearing back from you that the article and solution helped makes it worth writing.

All of us use Reflection implicitly when objects are serialized. Many use Reflection explicitly to do things like deep object copies or dumping object state. The upside of Reflection is it that it supports writing a general purpose algorithm, albeit an underperforming one. In this article, you will learn to write a general purpose emitter that you need write only one time, and the emitter outputs a specific-purpose algorithm. In this example, the algorithm emits code that dumps the object state for a specific list of objects. Pass in the list of objects and the emitter writes the equivalent of a property-by-property state dump. A side benefit of working at this level, besides writing highly performant code, is that you can learn a little more about how .NET works under the hood.

Here Is the Sample Main Subroutine…

The sample Main subroutine is listed here without much fanfare. It is very simple but you need it to try the emitter. Listing 2 contains the definition of the Customer class.

Listing 1: A sample Main loop that invokes and tests the emitter.

Module Module1

   Sub Main()

      ' Create customer list
      Dim customers As List(Of Customer) = New List(Of Customer)
      For i As Integer = 0 To 50
         customers.Add(New Customer(i.ToString()))
      Next

      DynamicAssemblyCreator.CreateAssembly(customers)
      Console.ReadLine()

   End Sub
End Module

Listing 2: The Customer class represents a custom coded entity that mirrors the Northwind.Customers table.

Public Class Customer

   ''' <summary>
   ''' Initializes a new instance of the Customer class.
   ''' </summary>
   ''' <param name="customerID"></param>
   Public Sub New(ByVal customerID As String)
      FCustomerID = customerID
      FCompanyName = "Company " & customerID.ToString()
      FContactName = "George Contact"
      FContactTitle = "Dr."
   End Sub

   Private FCustomerID As String = ""
   Public Property CustomerID() As String
      Get
         Return FCustomerID
      End Get
      Set(ByVal Value As String)
         FCustomerID = Value
      End Set
   End Property

   Private FCompanyName As String
   Public Property CompanyName() As String
      Get
         Return FCompanyName
      End Get
      Set(ByVal Value As String)
         FCompanyName = Value
      End Set
   End Property

   Private FContactName As String = ""
   Public Property ContactName() As String
      Get
         Return FContactName
      End Get
      Set(ByVal Value As String)
         FContactName = Value
      End Set
   End Property

   Private FContactTitle As String = ""
   Public Property ContactTitle() As String
      Get
         Return FContactTitle
      End Get
      Set(ByVal Value As String)
         FContactTitle = Value
      End Set
   End Property

   Private FAddress As String = ""
   Public Property Address() As String
      Get
         Return FAddress
      End Get
      Set(ByVal Value As String)
         FAddress = Value
      End Set
   End Property

   Private FCity As String = ""
   Public Property City() As >String
      Get
         Return FCity
      End Get
      Set(ByVal Value As String)
         FCity = Value
      End Set
   End Property

   Private FRegion As String = ""
   Public Property Region() As String
      Get
         Return FRegion
      End Get
      Set(ByVal Value As String)
         FRegion = Value
      End Set
   End Property

   Private FPostalCode As String = ""
   Public Property PostalCode() As String
      Get
         Return FPostalCode
      End Get
      Set(ByVal Value As String)
         FPostalCode = Value
      End Set
   End Property

   Private FCountry As String = ""
   Public Property Country() As String
      Get
         Return FCountry
      End Get
      Set(ByVal Value As String)
         FCountry = Value
      End Set
   End Property

   Private FPhone As String = ""
   Public Property Phone() As String
      Get
         Return FPhone
      End Get
      Set(ByVal Value As String)
         FPhone = Value
      End Set
   End Property

   Private FFax As String = ""
   Public Property Fax() As String
      Get
         Return FFax
      End Get
      Set(ByVal Value As String)
         FFax = Value
      End Set
   End Property
End Class

You will need Main and the Customer class to try the demo. You can just copy and paste them, but if you are just getting started writing the code yourself will provide you with some good practice.

Implementing a Dynamic Emitter Assembly

The problem is defined as not wanting to write a ToString override for every object where you manually write the code that dumps and object’s state. Further, if your code uses Reflection to dump an object’s state, it’s an improvement because you only have to write it one time, but if it’s called a lot your application’s performance will suffer. Our solution is to write an emitter that acts like the fast hard-coded version but you only have to write it one time because you are writing an emitter that generates the equivalent of the hard-coded version.

Listing 3 contains the complete codification of the emitter. Generally, long methods are broken up into pieces, but in the case of emitters and code generators they are easier to follow if they are listed consecutively.

Listing 3: An emitter that generates a dynamic assembly that loops through a list and dumps the state of each object.

Imports System.Reflection
Imports System.IO
Imports System.Runtime.CompilerServices
Imports System.Reflection.Emit

Module DynamicAssemblyCreator


   Public Sub CreateAssembly(Of T)(ByVal list As IEnumerable(Of T))

   Dim FunctionName As String = "Dump" + GetType(T).Name

   Dim name As AssemblyName = New AssemblyName()
   name.Name = GetType(T).Name + "Dumper"
   Dim domain As AppDomain = System.Threading.Thread.GetDomain()
   Dim builder As AssemblyBuilder = _
      domain.DefineDynamicAssembly(name, _
      AssemblyBuilderAccess.RunAndSave)
   Dim moduleBuilder As ModuleBuilder = _
      builder.DefineDynamicModule(GetType(T).Name + _
      "Dumper.dll", True)
   Dim typeBuilder As TypeBuilder = _
      moduleBuilder.DefineType("MyType", _
      TypeAttributes.Public Or TypeAttributes.Class)

   Dim methodBuilder As MethodBuilder = _
      typeBuilder.DefineMethod(FunctionName, _
      MethodAttributes.Static Or MethodAttributes.Public)


   methodBuilder.SetParameters(GetType(IEnumerable(Of T)))
   Dim parmBuilder As ParameterBuilder = _
      methodBuilder.DefineParameter( _
      1, ParameterAttributes.In, "list")

   ' Some methods
   Dim write As MethodInfo = _
      GetType(Console).GetMethod("WriteLine", _
      New Type() {GetType(String)})

   Dim concat As MethodInfo = _
      GetType(System.String).GetMethod("Concat", _
      New Type() {GetType(String), GetType(String)})

   Dim MoveNext As MethodInfo = _
      GetType(System.Collections.IEnumerator).GetMethod("MoveNext", _
      New Type() {})

   Dim enumer As MethodInfo = _
      GetType(IEnumerable(Of T)).GetMethod("GetEnumerator", _
      New Type() {})

   Dim get_Current As MethodInfo = _
      GetType(System.Collections.IEnumerator).GetMethod("get_Current")

      ' get the properties and use them to build writeline statements
      Dim properties() As PropertyInfo = GetType(T).GetProperties()
      Dim generator As ILGenerator = methodBuilder.GetILGenerator()

      ' loc 1
      Dim localT As LocalBuilder = generator.DeclareLocal(GetType(T))
      ' loc 2
      Dim localI As LocalBuilder = _
         generator.DeclareLocal(GetType(IEnumerator(Of T)))
      'loc 3
      Dim localB As LocalBuilder = _
         generator.DeclareLocal(GetType(Boolean))

      generator.Emit(OpCodes.Nop)
      generator.Emit(OpCodes.Nop)
      ' Try
      generator.BeginExceptionBlock()
      generator.Emit(OpCodes.Ldarg_0)
      generator.EmitCall(OpCodes.Callvirt, enumer, Nothing)
      generator.Emit(OpCodes.Stloc_1)
      Dim IL_000b As Label = generator.DefineLabel()
      Dim IL_003f As Label = generator.DefineLabel()
      generator.Emit(OpCodes.Br, IL_003f)    ' BR_S is wrong
      generator.MarkLabel(IL_000b)
      generator.Emit(OpCodes.Ldloc_1)
      generator.EmitCall(OpCodes.Callvirt, get_Current, Nothing)
      ' added this
      generator.Emit(OpCodes.Castclass, GetType(T))
   generator.Emit(OpCodes.Stloc_0)

      ' write each property
      For Each prop As PropertyInfo In properties
         generator.Emit(OpCodes.Ldstr, prop.Name + "=")
         generator.Emit(OpCodes.Ldloc_0)
         Dim get_Prop As MethodInfo = _
         GetType(T).GetMethod("get_" + prop.Name, New Type() {})
         generator.EmitCall(OpCodes.Callvirt, get_Prop, Nothing)

         ' Handle value types that need to be boxed for ToString
         If (prop.PropertyType.IsValueType) Then
            generator.Emit(OpCodes.Box, prop.PropertyType)
         End If

         generator.Emit(OpCodes.Callvirt, _
            GetType(Object).GetMethod("ToString", New Type() {}))
         generator.Emit(OpCodes.Call, concat)
         generator.Emit(OpCodes.Call, write)
         generator.Emit(OpCodes.Nop)
      Next

      generator.Emit(OpCodes.Nop)
      generator.MarkLabel(IL_003f)
      generator.Emit(OpCodes.Ldloc_1)
      generator.EmitCall(OpCodes.Callvirt, MoveNext, Nothing)
      generator.Emit(OpCodes.Stloc_2)
      generator.Emit(OpCodes.Ldloc_2)
      ' Brtrue_S is wrong
      generator.Emit(OpCodes.Brtrue, IL_000b)
      generator.Emit(OpCodes.Nop)
      Dim IL_0060 As Label = generator.DefineLabel()
      generator.Emit(OpCodes.Leave_S, IL_0060)
      ' begin finally
      generator.BeginFinallyBlock()
      generator.Emit(OpCodes.Ldloc_1)
      generator.Emit(OpCodes.Ldnull)
      generator.Emit(OpCodes.Ceq)
      generator.Emit(OpCodes.Ldc_I4_0)
      generator.Emit(OpCodes.Ceq)
      generator.Emit(OpCodes.Stloc_2)
      generator.Emit(OpCodes.Ldloc_2)
      Dim IL_005e As Label = generator.DefineLabel
      generator.Emit(OpCodes.Brfalse_S, IL_005e)
      generator.Emit(OpCodes.Ldloc_1)
      Dim dispose As MethodInfo = _
         GetType(System.IDisposable).GetMethod("Dispose", New Type() {})
      generator.EmitCall(OpCodes.Callvirt, dispose, Nothing)
      generator.Emit(OpCodes.Nop)
      generator.MarkLabel(IL_005e)
      generator.Emit(OpCodes.Nop)
      generator.EndExceptionBlock()
      generator.MarkLabel(IL_0060)
      generator.Emit(OpCodes.Nop)
      generator.Emit(OpCodes.Ret)

      Dim test As Type = typeBuilder.CreateType()

      builder.Save(GetType(T).Name + "Dumper.dll")
      test.GetMethod(FunctionName).Invoke(Nothing, New Object() {list})

   End Sub

EndModule

In the remaining sections of the article, the emitter is deeconstructed, permitting you to understand what is going on and write your own emitters for new solutions to existing problems.

Defining the Dynamic Assembly

When you write an emitter, you have to include all of the elements, using Reflection.Emit, that you would include if you were writing the code in VB. This means you need an assembly, module, and methods with lines of code. Working from the big piece—the assembly—down to the little pieces—the lines of code.

The emitter in Listing 1 includes imports for System.IO, System.Reflection, System.Runtime.CompilerServices, and System.Reflection.Emit re-listed here with the first few lines of the emitter.

Listing 4: Starting your emitter.

Imports System.Reflection
Imports System.IO
Imports System.Runtime.CompilerServices
Imports System.Reflection.Emit

Module DynamicAssemblyCreator


   Public Sub CreateAssembly(Of T)(ByVal list As IEnumerable(Of T))

      Dim FunctionName As String = "Dump" + GetType(T).Name

      Dim name As AssemblyName = New AssemblyName()
      name.Name = GetType(T).Name + "Dumper"
      Dim domain As AppDomain = System.Threading.Thread.GetDomain()
      Dim builder As AssemblyBuilder = _
         domain.DefineDynamicAssembly(name, _
         AssemblyBuilderAccess.RunAndSave)
      Dim moduleBuilder As ModuleBuilder = _
         builder.DefineDynamicModule(GetType(T).Name + _
         "Dumper.dll", True)
      Dim typeBuilder As TypeBuilder = _
         moduleBuilder.DefineType("MyType", _
         TypeAttributes.Public Or TypeAttributes.Class)

...

CreateAssembly accepts a List(Of T). When you see (Of T) or something similar, you know you are using generics. Generics means the algorithm includes everything but the type, with T representing the type. The caller provides the type.

The first step defines a unique function name. You will need only to call this method once for each type. Calling it with a list of Customer objects means you will get a unique method named DumpCustomer.

Next, an AssemblyName object is needed and the AssemblyName.Name property is defined. Assemblies run within the context of an AppDomain and use the current AppDomain for the dyanmic assembly. The AssemblyBuilder is the root class used to aggregate all of the elements of a dynamic assembly. The ModuleBuilder represents a module—not in the VB sense of a Midule—but as an item containing code elements. Within modules, you add things like literal Modules or Classes. Your TypeBuilder will contain a Public Class named MyType.

Defining the Method

Within modules and classes, you add methods (among other things). Methods are emitted using the MethodBuilder class. Are you beginning to see the pattern? To define a dynamic assembly, you add all of the traditional elements using a class; generally, the emitter class is the name of the thing you want to add with the suffix Builder.

Just as with writing VB code, you need to define modifiers like Public and Shared and parameters. Listing 5 contains the fragment that defines the dumper method.

Listing 5: Use a MethodBuilder as the locus for your lines of code—with Reflection.Emit, the lines of code are IL opcodes.

Dim methodBuilder As MethodBuilder = _
   typeBuilder.DefineMethod(FunctionName, _
   MethodAttributes.Static Or MethodAttributes.Public)


methodBuilder.SetParameters(GetType(IEnumerable(Of T)))
Dim parmBuilder As ParameterBuilder = _
   methodBuilder.DefineParameter( _
   1, ParameterAttributes.In, "list")

Listing 5 defines a method with the named defined by FunctionName as a Public Shared method with an IEnumerable(OF T) parameter.

Getting References to Methods the Emitter Uses

As is true with writing VB code, you will want to leverage the framework in your emitter. The way to do this is get the MethodInfo object using Reflection for the methods you emitter will need. Listing 6 contains local variable MethodInfo references to Console.WriteLine, System.String.Concat, IEnumerator.MoveNext, IEnumerable.GetEnumerator, and IEnumerator.get_Current. Current is actually a property, but at the MSIL level properties are actually methods with a get_ and set_ prefix.

Listing 6: Obtaining handles to methods the emitter will use.

' Some methods
Dim write As MethodInfo = _
   GetType(Console).GetMethod("WriteLine", _
   New Type() {GetType(String)})

Dim concat As MethodInfo = _
   GetType(System.String).GetMethod("Concat", _
   New Type() {GetType(String), GetType(String)})

Dim MoveNext As MethodInfo = _
   GetType(System.Collections.IEnumerator).GetMethod("MoveNext", _
   New Type() {})

Dim enumer As MethodInfo = _
   GetType(IEnumerable(Of T)).GetMethod("GetEnumerator", _
   New Type() {})

   Dim get_Current As MethodInfo = _
      GetType(System.Collections.IEnumerator).GetMethod("get_Current")

Emitting Lines of Code

Basically what the emitter has to do is emit an algorithm that reads roughly like the following pseudocode:

For each item in list

   Write property1’s name and property1’s value

   Write property2’s name and property2’s value

Next item

Of course, this is very easy with VB code, but if you did that you’d have to revert to pure reflection or write the lines of code yourself. The code in Listing 7 writes that code for you but in MSIL (Microsoft Intermediate Language, which is an implementation of the standard CIL (Common Intermediate Language). MSIL is like assembly language, so you have to do it in little itty bitty pieces. The lines of code are numbered for reference only to assist with the long listing.

Listing 7: Some code that iterates over each property and writes the lines of MSIL to display the property name and value.

 1. ' get the properties and use them to build writeline statements
 2. Dim properties() As PropertyInfo = GetType(T).GetProperties()
 3. Dim generator As ILGenerator = methodBuilder.GetILGenerator()

 4. Dim localT As LocalBuilder = generator.DeclareLocal(GetType(T))
   ' loc 1
 5. Dim localI As LocalBuilder = _
    generator.DeclareLocal(GetType(IEnumerator(Of T)))
    ' loc 2
 6. Dim localB As LocalBuilder = _
    generator.DeclareLocal(GetType(Boolean))
    'loc 3

 7. generator.Emit(OpCodes.Nop)
 8. generator.Emit(OpCodes.Nop)
 9. ' Try
10. generator.BeginExceptionBlock()
11. generator.Emit(OpCodes.Ldarg_0)
12. generator.EmitCall(OpCodes.Callvirt, enumer, Nothing)
13. generator.Emit(OpCodes.Stloc_1)
14. Dim IL_000b As Label = generator.DefineLabel()
15. Dim IL_003f As Label = generator.DefineLabel()
16. generator.Emit(OpCodes.Br, IL_003f)    ' BR_S is wrong
17. generator.MarkLabel(IL_000b)
18. generator.Emit(OpCodes.Ldloc_1)
19. generator.EmitCall(OpCodes.Callvirt, get_Current, Nothing)
20. ' added this
21. generator.Emit(OpCodes.Castclass, GetType(T))
22. generator.Emit(OpCodes.Stloc_0)

23. ' write each property
24. For Each prop As PropertyInfo In properties
25. generator.Emit(OpCodes.Ldstr, prop.Name + "=")
26. generator.Emit(OpCodes.Ldloc_0)
27. Dim get_Prop As MethodInfo = _
    GetType(T).GetMethod("get_" + prop.Name, New Type() {})
28. generator.EmitCall(OpCodes.Callvirt, get_Prop, Nothing)

29. ' Handle value types that need to be boxed for ToString
30. If (prop.PropertyType.IsValueType) Then
    a. generator.Emit(OpCodes.Box, prop.PropertyType)
31. End If

32. generator.Emit(OpCodes.Callvirt, _
    GetType(Object).GetMethod("ToString", New Type() {}))
33. generator.Emit(OpCodes.Call, concat)
34. generator.Emit(OpCodes.Call, write)
35. generator.Emit(OpCodes.Nop)
36. Next

37. generator.Emit(OpCodes.Nop)
38. generator.MarkLabel(IL_003f)
39. generator.Emit(OpCodes.Ldloc_1)
40. generator.EmitCall(OpCodes.Callvirt, MoveNext, Nothing)
41. generator.Emit(OpCodes.Stloc_2)
42. generator.Emit(OpCodes.Ldloc_2)
43. ' Brtrue_S is wrong
44. generator.Emit(OpCodes.Brtrue, IL_000b)
45. generator.Emit(OpCodes.Nop)
46. Dim IL_0060 As Label = generator.DefineLabel()
47. generator.Emit(OpCodes.Leave_S, IL_0060)
48. ' begin finally
49. generator.BeginFinallyBlock()
50. generator.Emit(OpCodes.Ldloc_1)
51. generator.Emit(OpCodes.Ldnull)
52. generator.Emit(OpCodes.Ceq)
53. generator.Emit(OpCodes.Ldc_I4_0)
54. generator.Emit(OpCodes.Ceq)
55. generator.Emit(OpCodes.Stloc_2)
56. generator.Emit(OpCodes.Ldloc_2)
57. Dim IL_005e As Label = generator.DefineLabel
58. generator.Emit(OpCodes.Brfalse_S, IL_005e)
59. generator.Emit(OpCodes.Ldloc_1)
60. Dim dispose As MethodInfo = _
    GetType(System.IDisposable).GetMethod("Dispose", New Type() {})
61. generator.EmitCall(OpCodes.Callvirt, dispose, Nothing)
62. generator.Emit(OpCodes.Nop)
63. generator.MarkLabel(IL_005e)
64. generator.Emit(OpCodes.Nop)
65. generator.EndExceptionBlock()
66. generator.MarkLabel(IL_0060)
67. generator.Emit(OpCodes.Nop)
68. generator.Emit(OpCodes.Ret)

Line 2 gets all of the properties for type T, which in the example is a Customer. Line 3 requests an ILGenerator from the MethodBuilder. The ILGenerator will convert the symbolic OpCodes to actual MSIL.

Lines 4, 5, and 6 define local variables for the dynamic method. There is the local variable representing a Customer object, an instance of IEnumerator(Of T), and a Boolean.

Lines 7 and 8 generate a Nop (pronounced no-op). These placeholders do nothing other than add one byte padding. I believe, although on this I could be wrong (sorry), their purpose is to add a little padding so instructions break on even 32-bit boundaries, which helps with moving the instruction pointer and other registers to locations that match the size of microprocessor registers.

Line 10 opens an exception handling block, emitting a try. Line 11 is the opcode that translates to loading the methods first (or zero-th) argument. Line 12 calls the virtual method referenced by the MethodInfo variable enumer, which from Listing 6 you know is GetEnumerator. The code is setting up code that is roughly a while loop iterating over the list of Customers. Line 13 stores the IEnumerator returned by GetEnumerator into the local variable emitted on Line 5.

Lines 14 and 15 define labels for branching statements. Yes, GOTO is a love while in MSIL and assembly language. This is how things like break an dcontinue are implemented. Line 16 is an unconditional branch to the location of label IL_003f. (The labels were named based on approximations and by looking at how Visual Studio compilers VB into MSIL. You can use any name you’d like.)

The MarkLabel command on Line 17 demonstrates how to place a label. OpCodes.LdLoc_1 (or whatever number suffix) are used to load the local parameters onto the evaluation stack. Lines 18 and 19 use the enumerator to get the Current object—the Customer. Because get_Current returns an object type, Line 18 casts the type of the current object to the type of T and stires it in the first parameter.

Lines 23 to 35 use basic Reflection and repeat emitted code that emits the property name and the property value. The if-condition starting on Line 30 checks to see whether the type is a a value type. Value types are things like integers that you wouldn’t normally think of as classes, but by boxing them .NET permits you to treat them like classes. In this case, both referenced classes and value types are treated like object so that ToString can be called on every property value regardless of whether it’s a reference type or value type.

The rest of the code moves the enumerator and supports ending the loop. Line 49 contains the finally block that cleans up the enumerator by calling Dispose and contains the return statement.

Loading and Calling the Dynamic Code

The final three lines create an instance of the type contained in the typeBuilder. The second to the last line saves the assembly with a unique name, and the remaining line uses Reflection to invoke the behavior.

For a generally useful dynamic assembly, you will want to return a reference to the TypeBuilder or just know when your code has created a dynamic assembly. The key here is that, once you create a dynamic assembly and load it using Reflection.Emit, it stays in memory in your AppDomain—and is available but never unloaded—until your application stops running. Figure 1 shows the CustomerDumper.dll—the dynamic assembly created by the code from Listing 3—as one of your applications loaded modules.

Figure 1: One challenge with dynamically emitted assemblies is that they remain loaded as long as your application is running.

It’s worth noting that you don’t have to become an MSIL expert to get some benefit from Reflection.Emit. A handy trick is to write the code you ultimately want to emit in VB and then use ILDASM to see what kind of MSIL you need to emit. Use sparingly and in situations where you need to eke out some performance; emitting MSIL dynamically can help you get a lot of mileage out of your solutions.

Summary

Reflection.Emit lets you emit MSIL—.NET’s equivalent of assembly language—at runtime. Use this technique when you have a problem that is best solved by a single, general algorithm, but pure Reflection is too slow. Instead, write a single, general emitter that dynamically emits the hard-code equivalent for you. You get the best of both worlds: You write a single algorithm and .NET emits code as if you had written a specialized algorithm for each need.

About the Author

Paul Kimmel is the VB Today columnist for www.codeguru.com and has written several books on object-oriented programming and .NET. Check out his upcoming book LINQ Unleashed for C# now available on Amazon.com and fine bookstores everywhere. Look for his upcoming book Teach Yourself the ADO.NET Entity Framework in 24 Hours. You may contact him for technology questions at pkimmel@softconcepts.com.

Copyright © 2008 by Paul T. Kimmel. All Rights Reserved.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories