DynamicMethod explained with System.DateTime binary serialization example

Introduction

DynamicMethod helps us to create a method at runtime, we can associate the method to a new dynamic type or to an existing type. These dynamic methods provide better performance, way to new possibilities, and fun too.

I currently work for a Windows Forms project in which the WCF Services hosted in one part of the world and clients are from all over the world. We use a custom binary serializer, in which our custom serialization and deserialization logic is dynamically added to our custom class (Data Contract) for the first time when it is serialized/deserialized.

The problem comes with DateTime handling. The framework serializes date and time. When it gets de-serialized in the server side, the value is actually same as the client side, but the client is from another time zone. But as per the server’s time zone, the value is wrong.

So I tried to serialize the date and time value to UTC value and then deserialize it back to server’s time zone. For this, I had to work on our custom serialization framework which was written by my ex-colleagues long back. My work on this made me to understand a bit of IL Code and the same is explained in this article.

Before starting….

Instead of just converting the DateTime value to UTC DateTime value using DateTime.ToUniversalTime, I even converted it to a 64-bit integer using DateTime.ToBinary. Because DateTime.ToBinary returns a 64-bit signed integer that encodes the System.DateTime.Kind and System.DateTime.Ticks properties. Ofcourse, the Kind is going to be Universal only, as we already have UTC time.

I also wanted to check whether the value is System.DateTime.MinValue, in that case I just write 0 into the serializer.

C# equivalent Code

public static void SerializeDate(System.Object obj, System.IO.BinaryWriter writer)
{
    System.DateTime actualDate = (System.DateTime)obj;

    if (actualDate == System.DateTime.MinValue)
    {
        writer.WriteInt32(0);
    }
    else
    {
        actualDate = actualDate.ToUniversalTime();
        writer.WriteInt64(actualDate.ToBinary();
    }
}

Step by Step with DynamicMethod

First step is to create a DynamicMethod and get the ILGenerator out of it.

DynamicMethod dynamicMethod = new System.Reflection.Emit.DynamicMethod.DynamicMethod("SerializeDate", null, new Type[] { typeof(Object), typeof(BinaryWriter) }, typeof(DateTime).Module);

var ilGenerator = dynamicMethod.GetILGenerator();

Now we have the instance of ILGenerator to Emit the IL code.

Next we have to unbox the first argument from System.Object to System.DateTime. So for that we need push the first argument to evaluation stack. Currently the stack is empty.

ilGenerator.Emit(OpCodes.Ldarg_0);

Now we have one item in the stack. We have to unbox the object which we have pushed just now to System.DateTime.

ilGenerator.Emit(OpCodes.Unbox_Any, typeof(System.DateTime));

Above line pops the object from stack, unboxes it to the specified type, here we have specified System.DateTime and pushes the unboxed value into stack. And again we have only one value in stack which is a DateTime. Let’s save this value to a local variable for later use, but before that we have to declare the local variable.

LocalBuilder actualDate = ilGenerator.DeclareLocal(typeof(System.DateTime));

 The above line declares a DateTime local variable. Now we can store the value to this variable.

ilGenerator.Emit(OpCodes.Stloc, actualDate);

Above line pops the DateTime value from stack and stores it into the local variable actualDate. Now the stack is empty.

Next step is to check whether the actualDate is equal to System.DateTime.MinValue. For DateTime comparison, as per ILDASM the method name for equals operator is op_Equality. It is a static method, accepts two DateTime type parameters and return boolean. As this is a static method, we don’t need any instance object reference. We need to push actualDate and System.DateTime.MinValue value into static.

ilGenerator.Emit(OpCodes.Ldloc, actualDate);
ilGenerator.Emit(OpCodes.Ldsfld, typeof(System.DateTime).GetField("MinValue"));

First line loads the actualDate value into stack. In the second line, OpCodes.Ldsfld tells the runtime to load static field value of System.DateTime.MinValue into stack. So now, actualDate and MinValue are in stack. Now we have to call the op_Equality method.

ilGenerator.Emit(OpCodes.Call, typeof(System.DateTime).GetMethod("op_Equality", new[] { typeof(System.DateTime), typeof(System.DateTime) }));

In the second parameter for Emit method, I am passing a reference to op_Equality method of System.DateTime type which accepts two System.DateTime parameters. This line pops the last two values from stack, sends them to op_Equality method and calls it. Pushes the returned value into stack which is a Boolean. So now stack is having one value. If the value is true, we have to write a zero to the BinaryWriter, otherwise we have to write the DateTime’s value. In the IL, there is no code block, we have to use Labels. So let’s create a Label and instruct the IL to go to that label if the value in stack is true.

Label labelWriteZero = ilGenerator.DefineLabel();
ilGenerator.Emit(OpCodes.Brtrue_S, labelWriteZero);

In first line, DefineLabel creates a token for a new label. It’s just a token for now, I have not yet added this label to IL, I will add it in some time. Second line, pops one value from stack and checks whether the value is true. If it is true then execution will continue at label position labelWriteZero. Just to remind, I have not yet added the label to IL. Suppose if the value is false, execution will continue from next line. So, I will add IL code for false scenario now. Stack is empty now.

And now, we have to get Universal-Time from our actualDate. We need reference of actualDate to call ToUniversalTime method. So I am pushing it now.

ilGenerator.Emit(OpCodes.Ldloca_S, actualDate);

OpCodes.Ldloca_S, pushes a local variable’s address into stack. Now, I’m going to call ToUniversalTime method.

ilGenerator.Emit(OpCodes.Call, typeof(DateTime).GetMethod("ToUniversalTime", Type.EmptyTypes));

Above line pops the actualDate’s address, then calls ToUniversalTime method on it and pushes the returned value into stack. Let’s stock the Universal Time in actualDate variable.

ilGenerator.Emit(OpCodes.Stloc, actualDate);

Above line pops a value from stack and stores in actualDate variable. Now, stack is empty.

I am going to start writing the value to the BinaryWriter. So I need it in stack.

ilGenerator.Emit(OpCodes.Ldarg_1);

Now, stack is having the BinaryWriter which was passed as the second argument to our method. I need actualDate in stack, so that I can call ToBinary method and write the output to BinaryWriter.

ilGenerator.Emit(OpCodes.Ldloca_S, actualDate);

I have the reference to actualDate in stack. Now I can call ToBinary method.

ilGenerator.Emit(OpCodes.Call, typeof(DateTime).GetMethod("ToBinary", Type.EmptyTypes));

Above line pops the actualDate from stack, then calls ToBinary method and pushes the returned value into stack, ToBinary returns a System.Int64. Now I have BinaryWriter and a System.Int64 values in stack, I am going to write it finally. BinaryWriter has Write method overloaded for all primitive types. As value in stack is System.Int64, I have to get method reference to the Write method which accepts one System.Int64 as parameter and then call it.

ilGenerator.Emit(OpCodes.Call, typeof(BinaryWriter).GetMethod("Write", new[] { typeof(System.Int64) }));

Above line, pops one value for parameter and then pops one value to call the Write method on it. Now our stack is empty and expected value is written in the BinaryWriter.

Still, I have to write IL code for true scenario. So after the current line, I have to return from the method.

ilGenerator.Emit(OpCodes.Ret);

Above line makes execution to stop there.

For true scenario, I have instructed the IL to go to label position labelWriteZero. So, I am adding the label.

ilGenerator.MarkLabel(labelWriteZero);

So, I am supposed to write a 0 in BinaryWriter here. I need BinaryWriter and value ‘0’ in the stack.

ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Ldc_I4_0);

First line, pushes our second argument which is BinaryWriter to stack. Next line pushes the integer value of 0 into stack. As the value is integer, I have to get the BinaryWriter’s Write method which accepts an integer and then call it.

ilGenerator.Emit(OpCodes.Call, typeof(BinaryWriter).GetMethod("Write", new[] { typeof(System.Int32) }));

Above line, pops 0 and BinaryWriter then writes 0 in the BinaryWriter. Here I have completed everything, I have to stop execution of the method by adding a return statement.

ilGenerator.Emit(OpCodes.Ret);

When the return statement executes, the stack must be empty as our method no return type specified. We have popped all the items from stack in our IL execution. For convenience, full code is given below.

Final C# Code using DynamicMethod

DynamicMethod dynamicMethod = new System.Reflection.Emit.DynamicMethod.DynamicMethod("SerializeDate", null, new Type[] { typeof(Object), typeof(BinaryWriter) }, typeof(DateTime).Module);

var ilGenerator = dynamicMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Unbox_Any, typeof(System.DateTime));
LocalBuilder actualDate = ilGenerator.DeclareLocal(typeof(System.DateTime));
ilGenerator.Emit(OpCodes.Stloc, actualDate);
ilGenerator.Emit(OpCodes.Ldloc, actualDate);
ilGenerator.Emit(OpCodes.Ldsfld, typeof(System.DateTime).GetField("MinValue"));
ilGenerator.Emit(OpCodes.Call, typeof(System.DateTime).GetMethod("op_Equality", new[] { typeof(System.DateTime), typeof(System.DateTime) }));
Label labelWriteZero = ilGenerator.DefineLabel();
ilGenerator.Emit(OpCodes.Brtrue_S, labelWriteZero);
ilGenerator.Emit(OpCodes.Ldloca_S, actualDate);
ilGenerator.Emit(OpCodes.Call, typeof(DateTime).GetMethod("ToUniversalTime", Type.EmptyTypes));
ilGenerator.Emit(OpCodes.Stloc, actualDate);
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Ldloca_S, actualDate);
ilGenerator.Emit(OpCodes.Call, typeof(DateTime).GetMethod("ToBinary", Type.EmptyTypes));
ilGenerator.Emit(OpCodes.Call, typeof(BinaryWriter).GetMethod("Write", new[] { typeof(System.Int64) }));
ilGenerator.Emit(OpCodes.Ret);
ilGenerator.MarkLabel(labelWriteZero);
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Ldc_I4_0);
ilGenerator.Emit(OpCodes.Call, typeof(BinaryWriter).GetMethod("Write", new[] { typeof(System.Int32) }));
ilGenerator.Emit(OpCodes.Ret);

Add comment

biuquote
  • Comment
  • Preview
Loading