views:

667

answers:

5

In .NET, using reflection how can I get class variables that are used in a method?

Ex:

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

Note: GetClassVariablesInMethod(M1 MethodInfo) returns B and C variables. By variables I mean Value and/or Type and Constructor Parameters of that specific variable.

A: 

Reflection is primarily an API for inspecting metadata. What you're trying to do is inspect raw IL which is not a supported function of reflection. Reflection just returns IL as a raw byte[] which must be manually inspected.

JaredPar
not very detailed...
romkyns
@romkyns, neither is your comment.
JaredPar
Irrespective of romkyns's comment, your answer is indeed not very detailed. Two other answers here (mine and Jb Evain's) have a complete solution.
Timwi
A: 

You need to get the MethodInfo. Call GetMethodBody() to get the method body structure and then call GetILAsByteArray on that. The convert that byte array into a stream of comprehensible IL.

Roughly speaking

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

where OpCodeList is constructed via

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

You can then work out which instructions are IL property calls or member variable look ups or whatever you require and resolve then via GetType().Module.ResolveField.

(Caveat code above more or less work but was ripped from a bigger project I did so maybe missing minor details).

Edit: Argument size is an extension method on OpCode that just uses a look up table to do find the appropriate value

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

You'll find sizes in ECMA 335 which you'll also need to look at for the OpCodes to find which OpCodes you to search for to find the calls you are looking for.

Ian G
Thank you very much. Code does not work as is it only needs the OpCode.ArgumentSize() function to work properly. That is an extension you wrote I think.
kerem
Thanks a lot for posting this code; it's very useful. However, there's a bug in it. The argument size of the switch instruction (OperandType.InlineSwitch) is not constant, so your ArgumentSize() function cannot return the correct value. The correct value is 4*(x+1) where x is the 32-bit integer that follows the opcode.
Timwi
Alternatively, you can use a method that is known to work :http://evain.net/blog/articles/2009/04/30/reflection-based-cil-reader
Jb Evain
Looks like you're all trying to reinvent the wheel here. Writing a bugfree CIL Reader is far from being an easy task. Hopefully, as Jb Evain stated, what you're trying to achieve is possible leveraging existing libraries : ILReader, Mono.Cecil, etc.
Romain Verdier
A: 

@Ian G: I have compiled the list from ECMA 335 and found out that I can use

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

And the opcode length list is here, if anyone needs it:

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}
};
kerem
There is a significant bug in this; the operand size for InlineSwitch is wrong. See my comment on the accepted answer for details.
Timwi
+1  A: 

Here's a complete version of the correct answer. This uses material from other answers, but incorporates an important bugfix which no-one else spotted.

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();
        }
    }
}
Timwi
The long? for the argument is not really elegant :)
Jb Evain
I think it is perfectly elegant. It is an optional value. The only thing that isn't elegant is how it tries (and fails) to use that Argument field for the argument of the 'switch' instruction, which doesn't fit in a long.
Timwi
+5  A: 

There's a lot of different answers, but as not a single one appeals to me, here's mine. It's using my Reflection based IL reader.

Here's a method retrieving all the fields used by a method:

static IEnumerable<FieldInfo> GetUsedFields (MethodInfo method)
{
    return (from instruction in method.GetInstructions ()
           where instruction.OpCode.OperandType == OperandType.InlineField
           select (FieldInfo) instruction.Operand).Distinct ();
}
Jb Evain