How to inject call to System.Object.Equals with Mono.Cecil?

这一生的挚爱 提交于 2020-01-01 15:52:05

问题


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;
}
}
}

回答1:


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.




回答2:


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



来源:https://stackoverflow.com/questions/4372205/how-to-inject-call-to-system-object-equals-with-mono-cecil

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!