Bobson
Bobson

Reputation: 13716

Determining which base constructor a constructor calls

I have a whole series of classes which look like

public class ReadVersionCommand : Command
{
    public ReadVersionCommand() : base(0x00, 0x01, 0x00, null) { }
}
public class DisplayTextCommand : Command
{
    public DisplayTextCommand(byte[] data) : base(0x05, 0x00, 0x00, data) { }
}
public class ReadKeyCommand : Command
{
    public ReadKeyCommand(byte p3, byte[] data) : base(0x09, 0x00, p3, data) { }
}

I want to iterate over all these classes, and generate information based on the four parameters to the base Command class (which I don't have control over). Ideally, I'd do this at runtime, so that we can add more subclasses to Command and have them automatically show up the next time we run the code.

I know how to use reflection iterate all the classes in question.

I know how to take each Type object and get the constructor.

I know how to take each ConstructorInfo object and get the parameters passed to the constructor, both the types and the names. I need to differentiate between a constructor which has one byte p2 parameter and one which has one byte p3, and I can do that.

I know how to get the base Command class's constructor, and list the types and names (byte p1, byte p2, byte p3, byte[] data).

If there were any code in the body of each constructor, I know how to get it with GetMethodBody().

However, I can't find any way to tell that each constructor is actually calling the base(byte, byte, byte, byte[]) constructor, and I can't find any way to see what the static values which are being passed to are. The values themselves are "magic" values which mean things to the underlying class, but only in combination. (i.e. 0x00, 0x01, 0x00 means one thing, and 0x01, 0x00, 0x00 means something very different.)

How can I get the values passed to the base constructor using reflection?

Upvotes: 1

Views: 432

Answers (3)

Bobson
Bobson

Reputation: 13716

As was suggested in the comments, I ended up just creating an instance of the object then querying the set parameters.

Effectively:

ctor = type.GetConstructor();
parameters = ctor.GetParameters();
foreach (p in parameters)
{
   // Mark that we have this parameter
}
// Construct array of parameters, using garbage values.
ctor.Invoke(callingParameters);
// For each parameter we didn't have, read the value.

It's ugly, and I'm not proud of it, but it works for my purposes.

Note: Even though this is the answer I went with this time, I'm going to accept M.Stramm's answer instead. It's definitely a better answer than this, and if I ever need to do this again, I will be using that solution instead.

Upvotes: 1

M.Stramm
M.Stramm

Reputation: 1309

First off, the obvious answer is that you are asking for the wrong thing. You should use attributes on your derived classes and query for their contents. Something like

[Magic(0xDE, 0xAD, 0xBE, 0xEF)]
public class ReadVersionCommand {}

Now that that's out of the way, to answer your stated problem 100%** you can use the Nuget Package ICSharpCode.Decompiler which powers ILSpy to do some runtime decompilation and give you exactly what you asked for. Because it's a fiddly bit of work to make it run, I did that for you.

Output:

public ReadVersionCommand ();
If you use this ReadVersionCommand constructor, then the first parameter to Command's constructor will be 19

public ReadVersionCommand (byte b2);
If you use this ReadVersionCommand constructor, then the first parameter to Command's constructor will be 5

public ReadVersionCommand (byte b1, byte b2);
If you use this ReadVersionCommand constructor, then the first parameter to Command's constructor will be b1

Code:

namespace ConsoleApp1 {
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using ICSharpCode.Decompiler;
    using ICSharpCode.Decompiler.Ast;
    using ICSharpCode.NRefactory.CSharp;
    using Mono.Cecil;

    class Program {
        static void Main(string[] args) {
            var path = Assembly.GetExecutingAssembly().Location;
            var assembly = AssemblyDefinition.ReadAssembly(path);

            var types = assembly.Modules.SelectMany(x => x.Types).ToList();

            var baseType = types.FirstOrDefault(x => x.FullName == typeof(Command).FullName);
            var derivedTypes = types.Where(x => x.BaseType == baseType);

            var ctx = new DecompilerContext(assembly.MainModule);
            foreach (var type in derivedTypes) {
                var astBuilder = new AstBuilder(ctx);
                astBuilder.AddType(type);

                var ast = astBuilder.SyntaxTree;
                var ctorDecls = ast.Descendants.OfType<ConstructorDeclaration>();

                var descriptors = ctorDecls.Select(ctor => Describe(type, ctor));
                foreach (var desc in descriptors) {
                    var firstParameter = desc.BaseCallParameters.FirstOrDefault();

                    Console.WriteLine(desc.Signature);
                    Console.WriteLine($"If you use this {desc.Type.Name} constructor, then the first parameter to {baseType.Name}'s constructor will be {firstParameter}");
                    Console.WriteLine();
                }

                Console.ReadLine();
            }
        }

        private static string GetPrettyCtorName(ConstructorDeclaration ctor) {
            var copy = ctor.Clone();
            var blocks = copy.Children.OfType<BlockStatement>().ToList();
            foreach (var block in blocks) {
                block.Remove();
            }

            return copy.ToString().Replace(Environment.NewLine, "");
        }

        private static ConstructorDescriptor Describe(TypeDefinition type, ConstructorDeclaration ctor) {
            return new ConstructorDescriptor {
                Type = type,
                Signature = GetPrettyCtorName(ctor),
                BaseCallParameters =
                            ctor
                            .Descendants
                            .OfType<MemberReferenceExpression>()
                            .Where(y => y.ToString() == "base..ctor")
                            .Select(y => y.Parent)
                            .FirstOrDefault()
                            ?.Children
                            .Skip(1)

            };
        }
    }

    public class ConstructorDescriptor {
        public TypeDefinition Type { get; set; }
        public string Signature { get; set; }
        public IEnumerable<AstNode> BaseCallParameters { get; set; }
    }

    public class Command {
        public Command(byte b1, byte b2, byte b3, byte[] data) { }
    }

    public class ReadVersionCommand : Command {
        public ReadVersionCommand() : base(0x13, 0x37, 0x48, null) { }

        public ReadVersionCommand(byte b2) : base(0x05, b2, 0x00, null) { }

        public ReadVersionCommand(byte b1, byte b2) : base(b1, b2, 0x00, null) { }
    }
}

** Well, more like 90% since the code does not use Reflection. You could however achieve the same by parsing the IL in the MethodBody of the ctor to get the parameters.

Upvotes: 2

ben
ben

Reputation: 1501

Reflection doesn't let you inspect the actual code inside a method in a helpful way - see this answer regarding reflection: Can I use reflection to inspect the code in a method?.

But here's an approach that should work. It uses reflection to find all the subclasses, and then retrieves the data you want from a "Prototype" instance of each class.

public abstract class Command
{
    // define a public property for each element you want to query
    public byte Data { get; }

    public Command(byte data)
    {
        Data = data;
    }
}

public class Command1 : Command
{
    // Require each subclass to define a static "prototype" instance,
    // calling the constructor with default values for any args
    public static Command Prototype = new Command1();

    public Command1() : base(0x12)
    {
    }
}

[TestFixture]
public class ReflectionTest
{
    [Test]
    public static void ListPrototypes()
    {
        // find all loaded subclasses of Command
        var subclasses =
            from assembly in AppDomain.CurrentDomain.GetAssemblies()
            from type in assembly.GetTypes()
            where type.IsSubclassOf(typeof(Command))
            select type;
        foreach (var subclass in subclasses)
        {
            // get the prototype instance of each class
            var prototype =
                subclass.GetField("Prototype", BindingFlags.Public | BindingFlags.Static)?.GetValue(null) as Command;
            if (prototype != null)
            {
                // emit the data from the prototype
                Console.WriteLine($"{subclass.Name}, Data={prototype.Data}");
            }
        }
    }
}

Upvotes: 1

Related Questions