In the last post, we were emitting a new dynamic type into an assembly. Now it’s time to add some data members to this new type. We’re going to start with Microsoft’s implementation, and then improve it.
First, let’s decide what we want to be able to store. In my implementation, I chose to deal with basic types (int, float, string, bool), object references, enums, and dynamic arrays (represented as List<T>s). I also wanted to support fixed arrays of value types. In order to do this, I made a slight concession for convenience: scalar values are fixed arrays of size 1. This helps to reduce the amount of divergent code needed. So what we’ll need is a class to represent our reflected properties, and we’ll need a method to emit their definitions into the ReflectedClass’s TypeBuilder. Let’s break down how ReflectedProperty.Emit(TypeBuilder Builder) needs to work:
Since we’re going to be adding these data members as properties on our new type (so that PropertyGrid and friends can find them), all we need is a storage data member, a getter, and a setter. Easy enough! First, let’s talk storage:
// CLRType is a property of our ReflectedProperty class which gives us the property type
// If CLRType is Int32, then CLRType.MakeArrayType() is equivalent to typeof(Int32[])
Type StorageType = (Type != ReflectedType.ARRAY) ? CLRType.MakeArrayType() : CLRType;
// We expose the property as a scalar value if it doesn't actually have more than one array element
Type PropertyType = (ArrayDim > 1) ? CLRType.MakeArrayType() : CLRType;
Because all data members are stored in arrays for ease of getting/setting, we just have to convert our basic type into an array for storage, then expose the property that represents it as its real scalar type.
// Add the private field, named _<Name>
FieldBuilder Field = Builder.DefineField(FieldName, StorageType, FieldAttributes.Private);
// Now add a public property to wrap the field, <Name>
PropertyBuilder Prop = Builder.DefineProperty(Name, System.Reflection.PropertyAttributes.HasDefault, PropertyType, null);
And just like that, we have storage. Now we need to be able to access it. To do this, we need to attach get and set methods to the property.
MethodInfo GetValueMethod = null;
MethodInfo SetValueMethod = null;
Type GetReturnType = CLRType;
Type SetArgumentType = CLRType;
BindingFlags MethodSearchFlags = BindingFlags.Instance | BindingFlags.FlattenHierarchy | BindingFlags.Public;
if (Type == ReflectedType.ARRAY)
{
GetReturnType = SetArgumentType = CLRType.GetGenericArguments()[0];
GetValueMethod = StorageType.GetMethod("get_Item", MethodSearchFlags, null, new Type[] { typeof(Int32) }, null);
SetValueMethod = StorageType.GetMethod("set_Item", MethodSearchFlags, null, new Type[] { typeof(Int32), SetArgumentType }, null);
}
else
{
GetValueMethod = StorageType.GetMethod("Get", MethodSearchFlags, null, new Type[] { typeof(Int32) }, null);
SetValueMethod = StorageType.GetMethod("Set", MethodSearchFlags, null, new Type[] { typeof(Int32), CLRType }, null);
}
Note that for dynamic arrays, we have to use the special get/set_Item functions (which are basically the operator overloading mechanism for the [] operator).
Now for the fun part. As Larry Wall once said, “Real Programmers can write assembly code in any language”. .NET makes it relatively easy, by exposing the CLR intermediate language instruction set to us. We need to write a thunk function to call our get/set methods on our newly created property. First, let’s deal with array access:
// These attributes will be applied to our get/set functions. We need a public, special, hidden function.
MethodAttributes GetSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;
// Build get method
Debug.Assert(GetValueMethod != null);
MethodBuilder GetMethod = Builder.DefineMethod("get_" + Name, GetSetAttr, GetReturnType, new Type[] { typeof(int) });
ILGenerator GetOpCodes = GetMethod.GetILGenerator();
GetOpCodes.Emit(OpCodes.Ldarg_0); // push this
GetOpCodes.Emit(OpCodes.Ldfld, Field); // push field
GetOpCodes.Emit(OpCodes.Ldarg_1); // push index
GetOpCodes.Emit(OpCodes.Call, GetValueMethod); // call this.GetValue(index)
GetOpCodes.Emit(OpCodes.Ret); // return
Prop.SetGetMethod(GetMethod);
// Build set method
Debug.Assert(SetValueMethod != null);
MethodBuilder SetMethod = Builder.DefineMethod("set_" + Name, GetSetAttr, null, new Type[] { typeof(int), SetArgumentType });
ILGenerator SetOpCodes = SetMethod.GetILGenerator();
SetOpCodes.Emit(OpCodes.Ldarg_0); // push this
SetOpCodes.Emit(OpCodes.Ldfld, Field); // push field
SetOpCodes.Emit(OpCodes.Ldarg_1); // push index
SetOpCodes.Emit(OpCodes.Ldarg_2); // push value
SetOpCodes.Emit(OpCodes.Call, SetValueMethod); // call this.SetValue(index, value)
SetOpCodes.Emit(OpCodes.Ret); // return
Prop.SetSetMethod(SetMethod);
Note that we ended up writing the get_MyDataMember and set_MyDataMember functions, which is how C# internally decorates the get/set methods for a property. If you use managed C++, the functions are just named that way in the first place. Also check out how the ILGenerator lets you cheat like crazy with some of the instructions, in this case allowing us to just send in the FieldInfo representing our private data member as an argument (instead of a memory offset).
The scalar value get/set opcodes are identical, except that instead of passing in an index, we use the Ldc_I4 instruction to pass in 0 as the hardcoded index:
// Build get method
Debug.Assert(GetValueMethod != null);
MethodBuilder GetMethod = Builder.DefineMethod("get_" + Name, GetSetAttr, GetReturnType, new Type[] { typeof(int) });
ILGenerator GetOpCodes = GetMethod.GetILGenerator();
GetOpCodes.Emit(OpCodes.Ldarg_0); // push this
GetOpCodes.Emit(OpCodes.Ldfld, Field); // push field
GetOpCodes.Emit(OpCodes.Ldarg_1); // push index
GetOpCodes.Emit(OpCodes.Call, GetValueMethod); // call this.GetValue(index)
GetOpCodes.Emit(OpCodes.Ret); // return
Prop.SetGetMethod(GetMethod);
// Build set method
Debug.Assert(SetValueMethod != null);
MethodBuilder SetMethod = Builder.DefineMethod("set_" + Name, GetSetAttr, null, new Type[] { typeof(int), SetArgumentType });
ILGenerator SetOpCodes = SetMethod.GetILGenerator();
etOpCodes.Emit(OpCodes.Ldarg_0); // push this
SetOpCodes.Emit(OpCodes.Ldfld, Field); // push field
SetOpCodes.Emit(OpCodes.Ldarg_1); // push index
etOpCodes.Emit(OpCodes.Ldarg_2); // push value
SetOpCodes.Emit(OpCodes.Call, SetValueMethod); // call this.SetValue(index, value)
SetOpCodes.Emit(OpCodes.Ret); // return
Prop.SetSetMethod(SetMethod);
At this point we just need some convenience functionality for getting and setting values, and some derived properties to make special-casing the different types and conversions cleaner. I’ve hacked up a sample that provides a more complete implementation here, under the MIT License. Feel free to use this to kickstart your own data marshaling, and make tools development easier!