Come posso ottenere i campi utilizzati in un metodo (.NET)?
-
07-07-2019 - |
Domanda
In .NET, usando la riflessione come posso ottenere variabili di classe utilizzate in un metodo?
Esempio:
class A
{
UltraClass B = new(..);
SupaClass C = new(..);
void M1()
{
B.xyz(); // it can be a method call
int a = C.a; // a variable access
}
}
Nota: GetClassVariablesInMethod (M1 MethodInfo) restituisce le variabili B e C. Per variabili intendo valore e / o tipo e parametri del costruttore di quella specifica variabile.
Soluzione
Ci sono molte risposte diverse, ma poiché non una sola mi piace, ecco la mia. Sta usando il mio Lettore IL basato su Reflection .
Ecco un metodo per recuperare tutti i campi utilizzati da un metodo:
static IEnumerable<FieldInfo> GetUsedFields (MethodInfo method)
{
return (from instruction in method.GetInstructions ()
where instruction.OpCode.OperandType == OperandType.InlineField
select (FieldInfo) instruction.Operand).Distinct ();
}
Altri suggerimenti
Ecco una versione completa della risposta corretta. Questo utilizza materiale proveniente da altre risposte, ma incorpora un importante bugfix che nessun altro ha notato.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace Timwi.ILReaderExample
{
public class ILReader
{
public class Instruction
{
public int StartOffset { get; private set; }
public OpCode OpCode { get; private set; }
public long? Argument { get; private set; }
public Instruction(int startOffset, OpCode opCode, long? argument)
{
StartOffset = startOffset;
OpCode = opCode;
Argument = argument;
}
public override string ToString()
{
return OpCode.ToString() + (Argument == null ? string.Empty : " " + Argument.Value);
}
}
private Dictionary<short, OpCode> _opCodeList;
public ILReader()
{
_opCodeList = typeof(OpCodes).GetFields().Where(f => f.FieldType == typeof(OpCode)).Select(f => (OpCode) f.GetValue(null)).ToDictionary(o => o.Value);
}
public IEnumerable<Instruction> ReadIL(MethodBase method)
{
MethodBody body = method.GetMethodBody();
if (body == null)
yield break;
int offset = 0;
byte[] il = body.GetILAsByteArray();
while (offset < il.Length)
{
int startOffset = offset;
byte opCodeByte = il[offset];
short opCodeValue = opCodeByte;
offset++;
// If it's an extended opcode then grab the second byte. The 0xFE prefix codes aren't marked as prefix operators though.
if (opCodeValue == 0xFE || _opCodeList[opCodeValue].OpCodeType == OpCodeType.Prefix)
{
opCodeValue = (short) ((opCodeValue << 8) + il[offset]);
offset++;
}
OpCode code = _opCodeList[opCodeValue];
Int64? argument = null;
int argumentSize = 4;
if (code.OperandType == OperandType.InlineNone)
argumentSize = 0;
else if (code.OperandType == OperandType.ShortInlineBrTarget || code.OperandType == OperandType.ShortInlineI || code.OperandType == OperandType.ShortInlineVar)
argumentSize = 1;
else if (code.OperandType == OperandType.InlineVar)
argumentSize = 2;
else if (code.OperandType == OperandType.InlineI8 || code.OperandType == OperandType.InlineR)
argumentSize = 8;
else if (code.OperandType == OperandType.InlineSwitch)
{
long num = il[offset] + (il[offset + 1] << 8) + (il[offset + 2] << 16) + (il[offset + 3] << 24);
argumentSize = (int) (4 * num + 4);
}
// This does not currently handle the 'switch' instruction meaningfully.
if (argumentSize > 0)
{
Int64 arg = 0;
for (int i = 0; i < argumentSize; ++i)
{
Int64 v = il[offset + i];
arg += v << (i * 8);
}
argument = arg;
offset += argumentSize;
}
yield return new Instruction(startOffset, code, argument);
}
}
}
public static partial class Program
{
public static void Main(string[] args)
{
var reader = new ILReader();
var module = typeof(Program).Module;
foreach (var instruction in reader.ReadIL(typeof(Program).GetMethod("Main")))
{
string arg = instruction.Argument.ToString();
if (instruction.OpCode == OpCodes.Ldfld || instruction.OpCode == OpCodes.Ldflda || instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldsflda || instruction.OpCode == OpCodes.Stfld)
arg = module.ResolveField((int) instruction.Argument).Name;
else if (instruction.OpCode == OpCodes.Call || instruction.OpCode == OpCodes.Calli || instruction.OpCode == OpCodes.Callvirt)
arg = module.ResolveMethod((int) instruction.Argument).Name;
else if (instruction.OpCode == OpCodes.Newobj)
// This displays the type whose constructor is being called, but you can also determine the specific constructor and find out about its parameter types
arg = module.ResolveMethod((int) instruction.Argument).DeclaringType.FullName;
else if (instruction.OpCode == OpCodes.Ldtoken)
arg = module.ResolveMember((int) instruction.Argument).Name;
else if (instruction.OpCode == OpCodes.Ldstr)
arg = module.ResolveString((int) instruction.Argument);
else if (instruction.OpCode == OpCodes.Constrained || instruction.OpCode == OpCodes.Box)
arg = module.ResolveType((int) instruction.Argument).FullName;
else if (instruction.OpCode == OpCodes.Switch)
// For the 'switch' instruction, the "instruction.Argument" is meaningless. You'll need extra code to handle this.
arg = "?";
Console.WriteLine(instruction.OpCode + " " + arg);
}
Console.ReadLine();
}
}
}
Devi ottenere MethodInfo. Chiamare GetMethodBody () per ottenere la struttura del corpo del metodo e quindi chiamare GetILAsByteArray su quello. Converte quell'array di byte in un flusso di IL comprensibile.
In parole povere
public static List<Instruction> ReadIL(MethodInfo method)
{
MethodBody body = method.GetMethodBody();
if (body == null)
return null;
var instructions = new List<Instruction>();
int offset = 0;
byte[] il = body.GetILAsByteArray();
while (offset < il.Length)
{
int startOffset = offset;
byte opCodeByte = il[offset];
short opCodeValue = opCodeByte;
// If it's an extended opcode then grab the second byte. The 0xFE
// prefix codes aren't marked as prefix operators though.
if (OpCodeList[opCodeValue].OpCodeType == OpCodeType.Prefix
|| opCodeValue == 0xFE)
{
opCodeValue = (short) ((opCodeValue << 8) + il[offset + 1]);
offset += 1;
}
// Move to the first byte of the argument.
offset += 1;
OpCode code = OpCodeList[opCodeValue];
Int64? argument = null;
if (code.ArgumentSize() > 0)
{
Int64 arg = 0;
Debug.Assert(code.ArgumentSize() <= 8);
for (int i = 0; i < code.ArgumentSize(); ++i)
{
Int64 v = il[offset + i];
arg += v << (i*8);
}
argument = arg;
offset += code.ArgumentSize();
}
var instruction = new Instruction(startOffset, code, argument);
instructions.Add(instruction);
}
return instructions;
}
dove OpCodeList è costruito tramite
OpCodeList = new Dictionary<short, OpCode>();
foreach (var opCode in typeof (OpCodes).GetFields()
.Where(f => f.FieldType == typeof (OpCode))
.Select(f => (OpCode) f.GetValue(null)))
{
OpCodeList.Add(opCode.Value, opCode);
}
È quindi possibile determinare quali istruzioni sono chiamate di proprietà IL o ricerche di variabili membro o qualsiasi altra cosa richiesta e risolta quindi tramite GetType (). Module.ResolveField.
(Il codice Caveat sopra più o meno funziona ma è stato strappato da un progetto più grande che ho fatto, quindi forse mi mancano dettagli minori).
Modifica: La dimensione dell'argomento è un metodo di estensione su OpCode che utilizza solo una tabella di ricerca per trovare il valore appropriato
public static int ArgumentSize(this OpCode opCode)
{
Dictionary<OperandType, int> operandSizes
= new Dictionary<OperandType, int>()
{
{OperandType.InlineBrTarget, 4},
{OperandType.InlineField, 4},
{OperandType.InlineI, 4},
// etc., etc.
};
return operandSizes[opCode.OperandType];
}
Troverai taglie in ECMA 335 che dovrai anche cercare gli OpCode per trovare gli OpCodes da cercare per trovare le chiamate che stai cercando.
Reflection è principalmente un'API per l'ispezione dei metadati. Quello che stai cercando di fare è ispezionare IL crudo che non è una funzione di riflessione supportata. Reflection restituisce semplicemente IL come byte non elaborato [] che deve essere ispezionato manualmente.
@Ian G: ho compilato l'elenco da ECMA 335 e ho scoperto che posso usare
List<MethodInfo> mis =
myObject.GetType().GetMethods().Where((MethodInfo mi) =>
{
mi.GetCustomAttributes(typeof(MyAttribute), true).Length > 0;
}
).ToList();
foreach(MethodInfo mi in mis)
{
List<Instruction> lst = ReflectionHelper.ReadIL(mi);
... find useful opcode
FieldInfo fi = mi.Module.ResolveField((int)usefulOpcode.Argument);
object o = fi.GetValue(myObject);
...
}
E l'elenco della lunghezza del codice operativo è qui, se qualcuno ne ha bisogno:
Dictionary<OperandType, int> operandSizes
= new Dictionary<OperandType, int>()
{
{OperandType.InlineBrTarget, 4},
{OperandType.InlineField, 4},
{OperandType.InlineI, 4},
{OperandType.InlineI8,8},
{OperandType.InlineMethod,4},
{OperandType.InlineNone,0},
{OperandType.InlineR,8},
{OperandType.InlineSig,4},
{OperandType.InlineString,4},
{OperandType.InlineSwitch,4},
{OperandType.InlineTok,4},
{OperandType.InlineType,4},
{OperandType.InlineVar,2},
{OperandType.ShortInlineBrTarget,1},
{OperandType.ShortInlineI,1},
{OperandType.ShortInlineR,4},
{OperandType.ShortInlineVar,1}
};