.NET IL Generation - Writing DeepCopy

Implementing a powerful object cloning library using IL generation.

Published on Saturday, November 4, 2017

This is the second part in a series of short posts covering code generation on the .NET platform.

IL Generation

Last time, we skimmed over some methods to generate code on .NET and one of them was emitting IL. IL generation lets us circumvent the rules C# and other languages put in place to protect us from our own stupidity. Without those rules, we can implement all kinds of fancy foot guns. Rules like “don't access private members of foreign types” and “don't modify readonly fields”. That last one is interesting: C#'s readonly translates into initonly on the IL/metadata level so theoretically we shouldn't be able to modify those fields even using IL. As a matter of fact we can, but it comes at a cost: our IL will no longer be verifiable. That means that certain tools will bark at you if you try to write IL code which commits this sin, tools such as PEVerify and ILVerify. Verifiable code also has ramifications for Security-Transparent Code. Thankfully for us, Code Access Security and Security Transparent Code don't exist in .NET Core and they usually don't cause issue for .NET Framework.

Enough stalling, onto our mission briefing.

DeepCopy

Today we're going to implement the guts of a library for creating deep copies of objects. Essentially it provides one method:

public static T Copy<T>(T original);

Our library will be called DeepCopy and the source is up on GitHub at ReubenBond/DeepCopy feel free to mess about with it. The majority of the code was adapted from the Orleans codebase.

Deep copying is important for frameworks such as Orleans, since it allows us to safely send mutable objects between grains on the same node without having to first serialize & then deserialze them, among other things. Of course, immutable objects (such as strings) are shared without copying. Oddly enough, serializing then deserializing an object is the accepted Stack Overflow answer to the question of “how can I deep copy an object?”.

Let's see if we can fix that.

Battle Plan

The Copy method will recursively copy every field in the input object into a new instance of the same type. It must be able to deal with multiple references to the same object, so that if the user provides an object which contains a reference to itself then the result will also contain a reference to itself. That means we'll need to perform reference tracking. That's easy to do: we maintain a Dictionary<object, object> which maps from original object to copy object. Our main Copy<T>(T orig) method will call into a helper method with that dictionary as a parameter:

public static T Copy<T>(T original, CopyContext context)
{
  /* TODO: implementation */
}

The copy routine is roughly as follows:

  • If the input is null, return null.
  • If the input has already been copied (or is currently being copied), return its copy.
  • If the input is 'immutable', return the input.
  • If the input is an array, copy each element into a new array and return it.
  • Create a new instance of the input type and recursively copy each field from the input to the output and return it.

Our definition of immutable is simple: the type is either a primitive or it's marked using a special [Immutable] attribute. More elaborate immutability could be probably be soundly implemented, so submit a PR if you've improved upon it.

Everything but the last step in our routine is simple enough to do without generating code. The last step, recursively copying each field, can be performed using reflection to get and set field values. Reflection is a real performance killer on the hot path, though, and so we're going to go our own route using IL.

Diving Into The Code

The main IL generation in DeepCopy occurs inside CopierGenerator.cs in the CreateCopier<T>(Type type) method. Let's walk through it:

First we create a new DynamicMethod which will hold the IL code we emit. We have to tell DynamicMethod what the signature of the type we're creating is. In our case, it's a generic delegate type, delegate T DeepCopyDelegate<T>(T original, CopyContext context). Then we get the ILGenerator for the method so that we can begin emitting IL code to it.

var dynamicMethod = new DynamicMethod(
    type.Name + "DeepCopier",
    typeof(T), // The return type of the delegate
    new[] {typeof(T), typeof(CopyContext)}, // The parameter types of the delegate.
    typeof(CopierGenerator).Module,
    true);

var il = dynamicMethod.GetILGenerator(); 

The IL is going to be rather complicated because it needs to deal with immutable types and value types, but let's walk through it bit-by-bit.

// Declare a variable to store the result.
il.DeclareLocal(type);

Next we need to initialize our new local variable to a new instance of the input type. There are 3 cases to consider, each corresponding to a block in the following code:

  • The type is a value type (struct). Initialize it by essentially using a default(T) expression.
  • The type has a parameterless constructor. Initialize it by calling new T().
  • The type does not have a parameterless constructor. In this case we ask the framework for help and we call FormatterServices.GetUninitializedObject(type).
// Construct the result.
var constructorInfo = type.GetConstructor(Type.EmptyTypes);
if (type.IsValueType)
{
    // Value types can be initialized directly.
    // C#: result = default(T);
    il.Emit(OpCodes.Ldloca_S, (byte)0);
    il.Emit(OpCodes.Initobj, type);
}
else if (constructorInfo != null)
{
    // If a default constructor exists, use that.
    // C#: result = new T();
    il.Emit(OpCodes.Newobj, constructorInfo);
    il.Emit(OpCodes.Stloc_0);
}
else
{
    // If no default constructor exists, create an instance using GetUninitializedObject
    // C#: result = (T)FormatterServices.GetUninitializedObject(type);
    var field = this.fieldBuilder.GetOrCreateStaticField(type);
    il.Emit(OpCodes.Ldsfld, field);
    il.Emit(OpCodes.Call, this.methodInfos.GetUninitializedObject);
    il.Emit(OpCodes.Castclass, type);
    il.Emit(OpCodes.Stloc_0);
}

Interlude - What IL Should We Emit?

Even if you're not a first-timer with IL, it's not always easy to work out what IL you need to emit to achieve the desired result. This is where tools come in to help you. Personally I typically write my code in C# first, slap it into LINQPad, hit run and open the IL tab in the output. It's great for experimenting.

LINQPad is seriously handy!

Another option is to use a decompiler/disassembler like JetBrains' dotPeek. You would compile your assembly and open it in dotPeek to reveal the IL.

Finally, if you're like me, then ReSharper is indispensible. It's like coding on rails (train tracks, not Ruby). ReSharper comes with a convenient IL Viewer.

ReSharper IL Viewer

Alright, so that's how you work out what IL to generate. You'll occasionally want to visit the docs, too.

Back To Emit

Now we have a new instance of the input type stored in our local result variable. Before we do anything else, we must record the newly created reference. We push each argument onto the stack in order and use the non-virtual Call op-code to invoke context.RecordObject(original, result). We can use the non-virtual Call op-code to call CopyContext.RecordObject because CopyContext is a sealed class. If it wasn't, we would use Callvirt instead.

// An instance of a value types can never appear multiple times in an object graph,
// so only record reference types in the context.
if (!type.IsValueType)
{
    // Record the object.
    // C#: context.RecordObject(original, result);
    il.Emit(OpCodes.Ldarg_1); // context
    il.Emit(OpCodes.Ldarg_0); // original
    il.Emit(OpCodes.Ldloc_0); // result, i.e, the copy of original
    il.Emit(OpCodes.Call, this.methodInfos.RecordObject);
}

On to the meat of our generator! With the accounting out of the way, we can enumerate over each field and generate code to copy each one into our result variable. The comments narrate the process:

// Copy each field.
foreach (var field in this.copyPolicy.GetCopyableFields(type))
{
    // Load a reference to the result.
    if (type.IsValueType)
    {
        // Value types need to be loaded by address rather than copied onto the stack.
        il.Emit(OpCodes.Ldloca_S, (byte)0);
    }
    else
    {
        il.Emit(OpCodes.Ldloc_0);
    }

    // Load the field from the result.
    il.Emit(OpCodes.Ldarg_0);
    il.Emit(OpCodes.Ldfld, field);

    // Deep-copy the field if needed, otherwise just leave it as-is.
    if (!this.copyPolicy.IsShallowCopyable(field.FieldType))
    {
        // Copy the field using the generic Copy<T> method.
        // C#: Copy<T>(field)
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Call, this.methodInfos.CopyInner.MakeGenericMethod(field.FieldType));
    }

    // Store the copy of the field on the result.
    il.Emit(OpCodes.Stfld, field);
}

Return the result and build our delegate using CreateDelegate so that we can start using it immediately.

// C#: return result;
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ret);
return dynamicMethod.CreateDelegate(typeof(DeepCopyDelegate<T>)) as DeepCopyDelegate<T>;

That's the guts of the library. Of course many details were left out, such as: