Question

Using Mono.Cecil I want to rewrite the following property:

public string FirstName
{
    get { return _FirstName; }
    set
    {
        _FirstName = value;
    }
}

to this:

public string FirstName
{
    get { return _FirstName; }
    set
    {
        if (System.Object.Equals(_FirstName, value))
        {
            return;
        }
        _FirstName = value;
    }
}

This is just a snippet of what the rewrite will be but it is where I'm having a problem.

Using Reflector I can see the following code rewrites the property as required except the call to System.Object.Equals(). If expect the IL code to be:

call bool [mscorlib]System.Object::Equals(object, object)

but it is being written as:

call instance void RewriteSharp.Person::.ctor()

The code to write the call to System.Object.Equals is:

setMethodWriter.InsertBefore(
    firstExistingInstruction, 
    setMethodWriter.Create(OpCodes.Call, objectEqualsMethodReference));

The method used to init objectEqualsMethodReference is:

private static MethodReference GetSystemObjectEqualsMethodReference(
    AssemblyDefinition assembly
)
{

    var typeReference = assembly.MainModule.GetTypeReferences()
        .Single(t => t.FullName == "System.Object");

    var typeDefinition = typeReference.Resolve();

    var methodDefinition = typeDefinition.Methods.Single(
                            m => m.Name == "Equals"
                                && m.Parameters.Count == 2
                                && m.Parameters[0].ParameterType.Name == "Object"
                                && m.Parameters[1].ParameterType.Name == "Object"
    );

    return methodDefinition;
}

It seems to me setMethodWriter.Create() or GetSystemObjectEqualsMethodReference() is incorrect and no amount of debugging has solved the problem.

The property being written and code to rewrite the property have the same framework target. 3.5 and 4.0 both fail.

I'm using the master branch https://github.com/jbevain/cecil to build Mono.Cecil.

Complete Code Listing

using Mono.Cecil;
using Mono.Cecil.Cil;
using System;
using System.Linq;

namespace RewriteNotifyPropertyChanged
{
class Program
{
static void Main(string[] args)
{
    var rewrite = "..\\RewriteSharp.dll";
    var rewritten  = "..\\RewritenSharp.dll";

    var typeName = "Person";
    var propertyName = "FirstName";

    var assembly = AssemblyDefinition.ReadAssembly(rewrite);
    var typeDefinition = assembly.MainModule.Types.Single(t => t.Name == typeName);
    var propertyDefintion = typeDefinition.Properties
        .Single(p => p.Name == propertyName);

    var setMethodWriter = propertyDefintion.SetMethod.Body.GetILProcessor();
    var backingFieldReference = GetBackingFieldReference(typeDefinition, propertyName);
    var objectEqualsMethodReference = GetSystemObjectEqualsMethodReference(assembly);
    var firstExistingInstruction = setMethodWriter.Body.Instructions[0];

    setMethodWriter.InsertBefore(
        firstExistingInstruction, 
        setMethodWriter.Create(OpCodes.Ldarg_0));

    setMethodWriter.InsertBefore(
        firstExistingInstruction, 
        setMethodWriter.Create(OpCodes.Ldfld, backingFieldReference));

    setMethodWriter.InsertBefore(
        firstExistingInstruction, 
        setMethodWriter.Create(OpCodes.Ldarg_1));

    setMethodWriter.InsertBefore(
        firstExistingInstruction, 
        setMethodWriter.Create(OpCodes.Call, objectEqualsMethodReference));

    setMethodWriter.InsertBefore(
        firstExistingInstruction, 
        setMethodWriter.Create(OpCodes.Brfalse_S, firstExistingInstruction));

    setMethodWriter.InsertBefore(
        firstExistingInstruction, 
        setMethodWriter.Create(OpCodes.Ret));

    assembly.Write(rewritten, new WriterParameters { WriteSymbols = true });

    Console.WriteLine("Done.");
    Console.ReadKey();
}

private static MethodReference GetSystemObjectEqualsMethodReference(
    AssemblyDefinition assembly
)
{

    var typeReference = assembly.MainModule.GetTypeReferences()
        .Single(t => t.FullName == "System.Object");

    var typeDefinition = typeReference.Resolve();

    var methodDefinition = typeDefinition.Methods.Single(
                            m => m.Name == "Equals"
                                && m.Parameters.Count == 2
                                && m.Parameters[0].ParameterType.Name == "Object"
                                && m.Parameters[1].ParameterType.Name == "Object"
    );

    return methodDefinition;
}

private static FieldReference GetBackingFieldReference(
    TypeDefinition typeDefinition, 
    string propertyName
)
{
    var fieldName = "_" + propertyName;
    var fieldReference = typeDefinition.Fields.Single(f => f.Name == fieldName);

    return fieldReference;
}
}
}
Was it helpful?

Solution

Cecil, unlike System.Reflection, makes the distinction between a reference and a definition, and those are scoped per module. It means that you can't simply use a MethodDefinition from another module inside your own. You have to create a proper reference to it. This is a process called importing in the Cecil terminology.

Concretely, GetSystemObjectEqualsMethodReference returns a method defined in the corlib, you need to create a reference to it in your module :

Replacing:

var objectEqualsMethodReference = GetSystemObjectEqualsMethodReference(assembly);

by:

var objectEqualsMethodReference = assembly.MainModule.Import (GetSystemObjectEqualsMethodReference(assembly));

And fixing the IL should make it work.

Also, while I'm at it, the method:

private static MethodReference GetSystemObjectEqualsMethodReference(AssemblyDefinition assembly)
{
    var typeReference = assembly.MainModule.GetTypeReferences()
        .Single(t => t.FullName == "System.Object");

    var typeDefinition = typeReference.Resolve();

    var methodDefinition = typeDefinition.Methods.Single(
                            m => m.Name == "Equals"
                                && m.Parameters.Count == 2
                                && m.Parameters[0].ParameterType.Name == "Object"
                                && m.Parameters[1].ParameterType.Name == "Object"
    );

    return methodDefinition;
}

Would be better written as:

private static MethodReference GetSystemObjectEqualsMethodReference(AssemblyDefinition assembly)
{
    var @object = assembly.MainModule.TypeSystem.Object.Resolve ();

    return @object.Methods.Single(
        m => m.Name == "Equals"
            && m.Parameters.Count == 2
            && m.Parameters[0].ParameterType.MetadataType == MetadataType.Object
            && m.Parameters[1].ParameterType.MetadataType == MetadataType.Object);
}

And

assembly.Write(rewritten, new WriterParameters { WriteSymbols = true });

Doesn't make much sense if you don't pass new ReaderParameters { ReadSymbols = true } when reading the assembly.

OTHER TIPS

You could take a look at KindOfMagic codeplex project.

It does almost the same, but little bit better - it does not call Object.Equals(), but equality operator defined on the target type.

http://kindofmagic.codeplex.com

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top