在 Mono.Cecil 中是否可以确定调用方法的对象的实际类型?

问题描述

例如,考虑以下 C# 代码

interface IBase { void f(int); }
interface IDerived : IBase { /* inherits f from IBase */ }

...
void SomeFunction()
{
    IDerived o = ...;
    o.f(5);
}

我知道如何获取SomeFunction 对应的 MethodDeFinition 对象。 然后我可以循环遍历 MethodDeFinition.Instructions:

var methodDef = getmethodDeFinitionOfSomeFunction();
foreach (var instruction in methodDef.Body.Instructions)
{
    switch (instruction.Operand)
    {
        case MethodReference mr:
            ...
            break;
    }
    yield return memberRef;
}

这样我就可以发现方法SomeFunction调用函数IBase.f

现在我想知道调用函数f的对象的声明类型,即o的声明类型。

检查 mr.DeclaringType 没有帮助,因为它返回 IBase

这是我目前所拥有的:

TypeReference typeRef = null;
if (instruction.OpCode == OpCodes.Callvirt)
{
    // Identify the type of the object on which the call is being made.
    var objInstruction = instruction;
    if (instruction.PrevIoUs.OpCode == OpCodes.Tail)
    {
        objInstruction = instruction.PrevIoUs;
    }
    for (int i = mr.Parameters.Count; i >= 0; --i)
    {
        objInstruction = objInstruction.PrevIoUs;
    }
    if (objInstruction.OpCode == OpCodes.Ldloc_0 ||
        objInstruction.OpCode == OpCodes.Ldloc_1 ||
        objInstruction.OpCode == OpCodes.Ldloc_2 ||
        objInstruction.OpCode == OpCodes.Ldloc_3)
    {
        var localIndex = objInstruction.OpCode.Op2 - OpCodes.Ldloc_0.Op2;
        typeRef = locals[localIndex].VariableType;
    }
    else
    {
        switch (objInstruction.Operand)
        {
            case FieldDeFinition fd:
                typeRef = fd.DeclaringType;
                break;
            case VariableDeFinition vd:
                typeRef = vd.VariableType;
                break;
        }
    }
}

其中 localsmethodDef.Body.Variables

但这当然还不够,因为函数的参数可以是对其他函数调用,例如 f(g("hello"))。看起来像上面的情况,当它实际执行代码时,我检查以前的指令必须重复虚拟机的操作。当然,我不执行它,但我需要识别函数调用并将它们及其参数替换为各自的返回值(即使是占位符)。看起来很痛苦。

有没有更简单的方法?也许已经内置了一些东西?

解决方法

我不知道实现这一目标的简单方法。

我能想到的“最简单”的方法是遍历堆栈并找到用作调用目标的引用被压入的位置。

基本上,考虑到每条指令如何影响堆栈,从调用指令开始,一次返回一条指令;通过这种方式,您可以找到推送用作调用目标的引用的确切指令(很久以前我写过类似的内容;您可以使用 https://github.com/lytico/db4o/blob/master/db4o.net/Db4oTool/Db4oTool/Core/StackAnalyzer.cs 处的代码作为灵感)。

您还需要考虑通过方法/属性生成推送引用的场景;例如,SomeFunction().f(5)。在这种情况下,您可能需要评估该方法以找出返回的实际类型。

请记住,您需要处理许多不同的情况;例如,想象一下下面的代码:

class Utils
{
   public static T Instantiate<T>() where T : new() => new T();
}

class SomeType
{
    public void F(int i) {}
}

class Usage
{
    static void Main()
    {
       var o = Utils.Instantiate<SomeType>();
       o.F(1);
    }
}

在遍历堆栈时,您会发现 o 是方法调用的目标;然后您将评估 Instantiate<T>() 方法并发现它返回 new T() 并且知道在这种情况下 TSomeType,这就是您要查找的类型。

,

所以 Vagaus 的回答帮助我想出了一个可行的实现。

我在 github 上发布了它 - https://github.com/MarkKharitonov/MonoCecilExtensions

包括许多单元测试,但我确定我错过了一些案例。