The System.Reflection.Emit
namespace provides objects for generating assemblies, types, and type
members at runtime. Basically you need to perform the following
operations sequentially:
1. | Create an in-memory assembly within the current application domain with an instance of the AssemblyBuilder class.
|
2. | Create a module for containing types via an instance of the ModuleBuilder class.
|
3. | Create types with instances of the TypeBuilder class.
|
4. | Add members to the TypeBuilder via XBuilder objects, such as MethodBuilder, FieldBuilder, and PropertyBuilder.
|
5. | Save the assembly to disk if required.
|
The code in Listing 1 demonstrates how to create dynamically a simple implementation of the Person class with one property and one method.
Listing 1. Generating Code at Runtime
Imports System.Reflection Imports System.Reflection.Emit
Module CreatingCode
Sub CreateAssembly()
'Creates assembly name and properties Dim asmName As New AssemblyName("People") asmName.Version = New Version("1.0.0") asmName.CultureInfo = New Globalization.CultureInfo("en-US")
'Gets the current application domain Dim currentAppDomain As AppDomain = AppDomain.CurrentDomain
'Creates a new in-memory assembly in the current application domain 'providing execution and saving capabilities Dim asmBuilder As AssemblyBuilder = currentAppDomain. DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndSave)
'Creates a module for containing types Dim modBuilder As ModuleBuilder = _ asmBuilder.DefineDynamicModule("PersonModule", "People.dll")
'Creates a type, specifically a Public Class Dim tyBuilder As TypeBuilder = _ modBuilder.DefineType("Person", TypeAttributes.Public _ Or TypeAttributes.Class) 'Defines a default empty constructor Dim ctorBuilder As ConstructorBuilder = _ tyBuilder.DefineDefaultConstructor(MethodAttributes.Public)
'Defines a field for storing a property value Dim fldBuilder As FieldBuilder = _ tyBuilder.DefineField("_lastName", GetType(String), FieldAttributes.Private)
'Defines a property of type String Dim propBuilder As PropertyBuilder = _ tyBuilder.DefineProperty("LastName", PropertyAttributes.None, GetType(String), Type.EmptyTypes)
'Defines a series of attributes for both getter and setter Dim propMethodAttributes As MethodAttributes = _ MethodAttributes.Public Or MethodAttributes.SpecialName Or MethodAttributes.HideBySig
'Defines the getter method for the property Dim propGetMethod As MethodBuilder = _ tyBuilder.DefineMethod("get_LastName", propMethodAttributes, GetType(String), Type.EmptyTypes) 'Generates IL code for returning the field value Dim propGetMethodIL As ILGenerator = propGetMethod.GetILGenerator propGetMethodIL.Emit(OpCodes.Ldarg_0) propGetMethodIL.Emit(OpCodes.Ldfld, fldBuilder) propGetMethodIL.Emit(OpCodes.Ret)
'Defines the setter method for the property Dim propSetMethod As MethodBuilder = _ tyBuilder.DefineMethod("set_LastName", propMethodAttributes, GetType(String), Type.EmptyTypes)
'Generates the IL code for setting the field value Dim propSetMethodIL As ILGenerator = propSetMethod.GetILGenerator propSetMethodIL.Emit(OpCodes.Ldarg_0) propSetMethodIL.Emit(OpCodes.Ldarg_1) propSetMethodIL.Emit(OpCodes.Stfld, fldBuilder) propSetMethodIL.Emit(OpCodes.Ret)
'Assigns getter and setter to the property propBuilder.SetGetMethod(propGetMethod) propBuilder.SetSetMethod(propSetMethod)
'Defines a public method that returns String Dim methBuilder As MethodBuilder = _ tyBuilder.DefineMethod("BuildFullName", MethodAttributes.Public, GetType(String), Type.EmptyTypes)
'Method body cannot be empty, so just return Dim methodILGen As ILGenerator = methBuilder.GetILGenerator methodILGen.EmitWriteLine("Method implementation needed") methodILGen.Emit(OpCodes.Ret)
'Creates an instance of the type Dim pers As Type = tyBuilder.CreateType
'Enumerates members for demo purposes For Each member In pers.GetMembers Console.WriteLine("Member name: {0}", member.Name) Next
'Saves the assembly to disk asmBuilder.Save("People.dll") Console.ReadLine() End Sub End Module
|
After you create an AssemblyName for assigning assembly properties and get the instance of the current application domain, you use the AppDomain.DefineDynamicAssembly method to generate an in-memory assembly. The method returns an instance of the AssemblyBuilder class and receives the AssemblyName instance and a value from the AssemblyBuilderAccess enumeration that establishes the access level for Reflection. RunAndSave enables executing and saving the assembly, but you can also limit Reflection with the ReflectionOnly value. The next step is creating an instance of the ModuleBuilder class that can act as a container of types. This is accomplished by invoking the AssemblyBuilder.DefineDynamicModule method that requires you to specify the module name and the filename. (This one should be the same as for AssemblyName
if you want metadata to be merged into a single assembly.) When you
have a module, you can put your types into it. For each type you need to
create an instance of the TypeBuilder class, which you accomplish by invoking the ModuleBuilder.DefineType method that receives the type name and qualifiers as arguments. Qualifiers are one or more values from the TypeAttributes enumeration; in the current example, Public and Class values are assigned to the new type to create a new class with public visibility. The TypeBuilder
class provides lots of methods for adding members, such as
constructors, field, properties, and methods. For constructors, the code
demonstrates how to add a public, empty, and default constructor
invoking the TypeBuilder.DefineDefaultConstructor, but you can supply constructor overloads via the DefineConstructor method. To implement properties, you first need to supply fields. These are implemented via the TypeBuilder.DefineField method that requires three arguments: the field name, the type (retrieved via GetType), and qualifiers, determined with values from the FieldAttributes enumeration. Similarly you implement properties invoking the TypeBuilder.DefineProperty
method, but this is not enough because you also need to explicitly
generate the getter and setter methods for each property. These are
special methods that require providing some properties defined within
the propMethodAttributes variable that takes values from the MethodAttributes enumeration. When you establish method attributes, you create two MethodBuilder
instances. Such a class generates each kind of method, including
special ones. You just supply the method name, attributes, the return
type, and an array of type parameters. The actual problem is how you
implement method bodies. As a general rule, methods implemented via
Reflection cannot have an empty method body, so you must provide some
Intermediate Language code to populate the method body. This is
accomplished by invoking methods from the ILGenerator class that enable injecting IL code to the method. Consider the following snippet, excerpted from Listing 1:
'Generates IL code for returning the field value
Dim propGetMethodIL As ILGenerator = propGetMethod.GetILGenerator
propGetMethodIL.Emit(OpCodes.Ldarg_0)
propGetMethodIL.Emit(OpCodes.Ldfld, fldBuilder)
propGetMethodIL.Emit(OpCodes.Ret)
The MethodBuilder.GetILGenerator method returns an instance of the ILGenerator class. Then you invoke the Emit method to execute IL code. In the preceding snippet, the IL code simply returns the value of the fldBuilder
variable and pushes the value onto the stack and then returns. Actions
to execute via the IL are taken via shared fields from the OpCodes class, each related to an IL instruction.
When you provide the method body for getters and setters, you add them to the related properties via the PropertyBuilder.SetGetMethod and PropertyBuilder.SetSetMethod
methods. Similarly you implement any other method, and the sample code
demonstrates this by providing a simple method body that invokes EmitWriteLine,
a method that sends to the assembly the appropriate IL code for writing
a message to the Console window. Finally you simply invoke AssemblyBuilder.Save
to save the assembly to disk. More than running the code, you can
ensure if everything works by inspecting the assembly with a Reflection
tool such as Microsoft IL Disassembler. Figure 1 shows how the assembly looks if opened with ILDasm, demonstrating the correct result of our work.
Typically you will prefer
code generators instead of Reflection to generate code on-the-fly
because in that case you do not need to know about Intermediate
Language.
Late Binding Concepts
Late binding is a
particular programming technique that you use to resolve types at
runtime and for types dynamic loading that is accomplished by assigning
objects to variable of type Object.
For a better understanding, consider its counterpart, the early binding.
This happens at compile time where the compiler checks that argument
types utilized to invoke methods match their signatures. An example is
the background compiler that provides real-time check for types used in
code, thanks to early binding. On the contrary, late binding requires
you to specify the function signatures; moreover you must ensure that
the code uses the correct types. Basically this means that binding
requirements, such as binary files to load or methods to invoke, is long
delayed, in many cases until before the method is invoked. Reflection
greatly uses late binding because in many cases you work with objects of
type Object, and this requires late
resolution for invoking appropriate members. The following example,
although not related to Reflection, demonstrates how to invoke members
from objects declared as Object that are instead of different types, but this is determined late at runtime:
' This code creates an instance of Microsoft Excel and adds a new WorkBook.
' Requires Option Strict Off
Sub LateBindingDemo()
Dim xlsApp As Object
Dim xlsBook As Object
xlsApp = CreateObject("Excel.Application")
xlsBook = xlsApp.Workbooks.Add
End Sub
Because in lots of situations turning Option Strict to Off
can be very dangerous, if you need to work with late-binding you should
consider moving the code that requires such a technique to a separate
code file and just mark this code file with Option Strict Off, instead of setting it Off at the project level.
|
As you can see, invoking members from Object
in late binding is different because the compiler cannot predetermine
if members exist, and you don’t have IntelliSense support. But if the
actual type defines members that you are attempting to invoke, they will
be correctly bound at runtime. Just remember that late binding requires
an Option Strict Off directive and that should be used carefully.