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

Is it possible in Mono.Cecil to determine the actual type of an object on which a method is called?

例如,考虑以下 C# 代码:

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

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

我知道如何获取对应于 SomeFunctionMethodDefinition 对象。 然后我可以遍历 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 没有帮助,因为它 returns 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")) 中一样。看起来像上面的情况,我检查以前的指令必须在虚拟机实际执行代码时重复虚拟机的操作。当然,我不执行它,但我需要识别函数调用并将它们及其参数替换为各自的 returns(即使是占位符)。看起来很痛苦。

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

我不知道实现此目的的简单方法。

我能想到的“最简单”的方法是遍历堆栈并找到用作调用目标的引用被推送到哪里。

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

您还需要考虑通过 method/property 生成推送引用的场景;例如,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>() 方法,并会发现它 returns new T() 并且知道 T 在这种情况下是 SomeType,这就是你的类型正在寻找。

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

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

包括许多单元测试,但我确信我遗漏了一些案例。