By far, the most interesting part of the project was how we dynamically generated a class to implement an interface. In the name of brevity for this post, I'll focus on the most basic case: implementing an interface with only methods that return primitives (equivalently in WCF, Service Contracts with only Operation Contracts). C# finds surprising strength in it's usability and power of reflection. To briefly explain the system from a birds-eye-view, you pass in the name of the interface you wish to implement as well as the assembly you it is defined in, it takes it, and goes through implementing each method by defining a custom Method Builder that contains information on calling parameters and the return type. It then uses OpCodes to put the return value on the stack, then assigns the return value of that method to the value you just pushed. Once it has finished with all methods, the Type of the newly generated class is returned to the caller function who can instantiate a new instance of the class and handle it like any other reference type.
To begin, let's define an interface that we wish to implement dynamically. Say an ICalculator with a string method as well. It doesn't really matter what goes here as long as we have only methods that return certain primitives including strings (I will get into that later). Let's say the interface definition is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 | using System; public interface ICalculator { double Add(double arg1, double arg2); double Subtract(double arg1, double arg2); double Multiply(double arg1, double arg2); string SomeOtherMethod(int arg1); } |
Easy enough! Notice that we're never going to make a class that implements this interface.
Next, we can define a ClassBuilder object that has only a constructor and a single public method, CreateClass. This method will take two parameters, a Type object that is the type of interface that we want to implement and an AssemblyName which is the name of the assembly that contains the interface definition. The second parameter is more there as proof that the interface doesn't need to be in the same assembly as this functionality, but we will just pass it the executing assembly and ignore it for all intents and purposes. We will need to include System.Reflection and System.Reflection.Emit. So far, we've got:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | using System; using System.Reflection; using System.Reflection.Emit; public class ClassBuilder { public ClassBuilder() { } public Type CreateClass(Type interfaceType, AssemblyName assemblyName) { } } |
Now that we've got everything we're going to need set up, we can get into the meat and bones of this project by implementing the CreateClass method. We're first going to want to define an AssemblyBuilder object for the assembly passed to this method. We can then define a ModuleBuilder to build a dynamic module. Then we define a TypeBuilder object that we will use to create a type that will implement the passed interface. You can play around with the parameters of this object, but everything we need for the TypeBuilder is listed below. We will also add an interface implementation to the TypeBuilder object to indicate that we intend to implement the interface. So now we have:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public Type CreateClass(Type interfaceType, AssemblyName assemblyName) { AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain. DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); ModuleBuilder moduleBuilder = assemblyBuilder. DefineDynamicModule("dynamicModule"); TypeBuilder typeBuilder = moduleBuilder.DefineType( String.Format("Autogenerated.{0}", interfaceType.ToString()), TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass, typeof(System.Object)); typeBuilder.AddInterfaceImplementation(interfaceType); return null; } |
I'm returning null at the end just for now so Visual Studio doesn't throw any compiler errors. Additionally, I apologize for the line wrapping; Blogger has a tough time with code formatting.
We've now got a TypeBuilder defined that targets the assembly we correct interface and assembly. Now, we need to iterate through each method defined in the interface and generate a dynamic implementation of it. We will do this by skipping method bodies entirely and just returning a dummy value (that's is why this process is very similar to unit test mocking). For each method the interface defines, we will create a MethodBuilder object with information about the method, including the name of the method, the protection level, if it's virtual, the return type an the parameters the method takes. To get the parameters, we will need to define a private method in our ClassBuilder class as follows:
1 2 3 4 5 6 7 8 9 10 11 12 | private Type[] GetMethodArguments(MethodInfo methodInfo) { Type[] argumentArray = new Type[methodInfo.GetParameters().Length]; for (int argumentIndex = argumentArray.Length - 1; argumentIndex >= 0; --argumentIndex) { argumentArray[argumentIndex] = methodInfo. GetParameters()[argumentIndex].ParameterType; } return argumentArray; } |
It simply iterates through each parameter and sticks it's type in an array, then returns that to the method builder. After making the MethodBuilder object, we will get the MethodBuilder's IL generator so we can use IL to create the return values of the methods and then return the Type that we're generating when we're done. We will also define a method override for the method we're currently generating. So now we're at:
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 | using System; using System.Reflection; using System.Reflection.Emit; public class ClassBuilder { public ClassBuilder() { } public Type CreateClass(Type interfaceType, AssemblyName assemblyName) { AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain. DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); ModuleBuilder moduleBuilder = assemblyBuilder. DefineDynamicModule("dynamicModule"); TypeBuilder typeBuilder = moduleBuilder.DefineType( String.Format("Autogenerated.{0}", interfaceType.ToString()), TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass, typeof(System.Object)); typeBuilder.AddInterfaceImplementation(interfaceType); foreach (MethodInfo methodInfo in interfaceType.GetMethods()) { MethodBuilder methodBuilder = typeBuilder.DefineMethod( methodInfo.Name, MethodAttributes.Public | MethodAttributes.Virtual, methodInfo.ReturnType, this.GetMethodArguments(methodInfo)); ILGenerator il = methodBuilder.GetILGenerator(); typeBuilder.DefineMethodOverride(methodBuilder, methodInfo); } return null; } private Type[] GetMethodArguments(MethodInfo methodInfo) { Type[] argumentArray = new Type[methodInfo.GetParameters().Length]; for (int argumentIndex = argumentArray.Length - 1; argumentIndex >= 0; --argumentIndex) { argumentArray[argumentIndex] = methodInfo. GetParameters()[argumentIndex].ParameterType; } return argumentArray; } } |
Great! The next step is to actually create the return value for the method. We will encapsulate this in a new private method for readability's sake:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | private void CreateReturnValue(ILGenerator il, Type parameterType) { if (parameterType == typeof(void)) { return; } else if (parameterType.IsPrimitive || parameterType == typeof(string)) { this.EmitPrimitive(il, parameterType); } else { throw new ArgumentException("Parameter was reference value or null"); } } |
So this method will error if you pass a reference type. It is absolutely possible to generate a dummy reference type, but it's slightly outside the scope of this post. If the method is void, nothing needs to be done, so it returns. The EmitPrimitive method is interesting however, as it basically acts as a switch on the type of argument it is passed, and if it finds it, it pushes a new instance of that primitive onto the stack which will be used as the return value. For the sake of this example, I put in some ridiculous dummy values to push onto the stack to show that it actually works.
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 | private void EmitPrimitive(ILGenerator il, Type parameterType) { if (parameterType == typeof(bool)) { // pushes a 1 onto the stack, represents true il.Emit(OpCodes.Ldc_I4_1); } else if (parameterType == typeof(char)) { il.Emit(OpCodes.Ldc_I4, 'a'); il.Emit(OpCodes.Conv_I2); } else if (parameterType == typeof(int)) { il.Emit(OpCodes.Ldc_I4, Convert.ToInt32(42)); } else if (parameterType == typeof(long)) { il.Emit(OpCodes.Ldc_I8, Convert.ToInt64(12)); } else if (parameterType == typeof(double)) { il.Emit(OpCodes.Ldc_R8, Convert.ToDouble(301)); } else if (parameterType == typeof(string)) { il.Emit(OpCodes.Ldstr, "Custom class generation worked!"); } // you can add additional primitive types ad nauseum, // unsigned, bytes, shorts, floats, decimals, etc. else { throw new ArgumentException(String.Format( "Unsupported parameter type : {0}", parameterType)); } } |
Almost there! Now let's add calls to CreateReturnValue in our method loop. We will also need to return. This will finish our implementation of the ClassBuilder class!
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 | using System; using System.Reflection; using System.Reflection.Emit; public class ClassBuilder { public ClassBuilder() { } public Type CreateClass(Type interfaceType, AssemblyName assemblyName) { AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain. DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); ModuleBuilder moduleBuilder = assemblyBuilder. DefineDynamicModule("dynamicModule"); TypeBuilder typeBuilder = moduleBuilder.DefineType( String.Format("Autogenerated.{0}", interfaceType.ToString()), TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass, typeof(System.Object)); typeBuilder.AddInterfaceImplementation(interfaceType); foreach (MethodInfo methodInfo in interfaceType.GetMethods()) { MethodBuilder methodBuilder = typeBuilder.DefineMethod( methodInfo.Name, MethodAttributes.Public | MethodAttributes.Virtual, methodInfo.ReturnType, this.GetMethodArguments(methodInfo)); ILGenerator il = methodBuilder.GetILGenerator(); typeBuilder.DefineMethodOverride(methodBuilder, methodInfo); this.CreateReturnValue(il, methodInfo.ReturnType); il.Emit(OpCodes.Ret); } return typeBuilder.CreateType(); } private Type[] GetMethodArguments(MethodInfo methodInfo) { Type[] argumentArray = new Type[methodInfo.GetParameters().Length]; for (int argumentIndex = argumentArray.Length - 1; argumentIndex >= 0; --argumentIndex) { argumentArray[argumentIndex] = methodInfo. GetParameters()[argumentIndex].ParameterType; } return argumentArray; } private void CreateReturnValue(ILGenerator il, Type parameterType) { if (parameterType == typeof(void)) { return; } else if (parameterType.IsPrimitive || parameterType == typeof(string)) { this.EmitPrimitive(il, parameterType); } else { throw new ArgumentException("Parameter was reference value or null"); } } private void EmitPrimitive(ILGenerator il, Type parameterType) { if (parameterType == typeof(bool)) { // pushes a 1 onto the stack, represents true il.Emit(OpCodes.Ldc_I4_1); } else if (parameterType == typeof(char)) { il.Emit(OpCodes.Ldc_I4, 'a'); il.Emit(OpCodes.Conv_I2); } else if (parameterType == typeof(int)) { il.Emit(OpCodes.Ldc_I4, Convert.ToInt32(42)); } else if (parameterType == typeof(long)) { il.Emit(OpCodes.Ldc_I8, Convert.ToInt64(12)); } else if (parameterType == typeof(double)) { il.Emit(OpCodes.Ldc_R8, Convert.ToDouble(301)); } else if (parameterType == typeof(string)) { il.Emit(OpCodes.Ldstr, "Custom class generation worked!"); } // you can add additional primitive types ad nauseum, // unsigned, bytes, shorts, floats, decimals, etc. else { throw new ArgumentException(String.Format( "Unsupported parameter type : {0}", parameterType)); } } } |
Now let's call if from Main to prove it works:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using System; using System.Collections.Generic; using System.Reflection; public class MainClass { public static void Main(string[] args) { ClassBuilder classBuilder = new ClassBuilder(); Type dynamicType = classBuilder.CreateClass(typeof(ICalculator), Assembly.GetEntryAssembly().GetName()); var dynamicObject = Activator.CreateInstance(dynamicType) as ICalculator; Console.WriteLine(dynamicObject.Add(1.0, 99.0)); Console.WriteLine(dynamicObject.Subtract(1.0, 99.0)); Console.WriteLine(dynamicObject.Multiply(1.0, 99.0)); Console.WriteLine(dynamicObject.SomeOtherMethod(1001)); Console.WriteLine("\nDone"); Console.ReadKey(); } } |
Which will produce:
1 2 3 4 5 6 | 301 301 301 Custom class generation works! Done |
And that's it! We've now got a way to generate a dynamic implementation of any method-only interface that returns only primitives! It's not super useful yet, but with a little more work to make it more versatile, it actually ends up being a really interesting project in reflection and a great programming exercise. A common use case for this could be if you're waiting on an implantation of an interface from a co-worker, you could use their interface in a mocked environment without actually needing their implementation. I think that's pretty interesting, as it breaks long-held programming workflows, especially in projects where someone's implementation gets pushed back for extended periods of time. You could define your usage of their interface and simply plug their implementation in afterwards. It is particularly useful within the context of WCF as you don't need an implementation to launch a web service. You can go ahead and use these custom-generated classes in their place. This project has several applications that are edge-case scenarios, however it provides a very interesting and versatile solution.
No comments:
Post a Comment