是否可以在 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);
}
我知道如何获取对应于 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
没有帮助,因为它 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;
}
}
}
其中 locals
是 methodDef.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
包括许多单元测试,但我确信我遗漏了一些案例。
例如,考虑以下 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
没有帮助,因为它 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;
}
}
}
其中 locals
是 methodDef.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
包括许多单元测试,但我确信我遗漏了一些案例。