В некоторых проектах так случается, что предыдущий разработчик не оставил исходников и на руках только откомпилированная .NET сборка. Или софт писала некоторая компания, которая прекратила существование, доступ к исходникам отсутствует, а необходимо сделать интеграцию с разрабатываемым приложением. В общем, подобные ситуации в работе случаются нередко.
Естественно, можно воспользоваться классной утилитой dnSpy. На мой взгляд — это лучшее приложение, что есть для декомпиляции .NET бинарного кода. Но это хороший вариант, когда сборку в которой нужно разобраться, уже совсем забросили разработчики и патчи ожидать не приходится.
Что делать если библиотека с которой нужна интеграция развивается, нужно периодически обновлять сборки, при этом не делая каждый раз декомпиляцию и правку кода в сборке?
На помощь приходит reflection. Это очень мощный способ интегрироваться с чужой сборкой, но так, чтобы не править её каждый раз. Пример исходников, которые будут рассматриваться далее, на Github.
Тестовая задача
Для примера возьмем два класса. Second — наследник First.
public delegate void TestDelegate(string a, int b); public class First { private int first_a = 16101975; protected int first_b = 1975; private int First_A { get { return first_a; } set { first_a = value; } } public event TestDelegate OnTestDelegate; private string Method1(string a, ref int b) { this.first_b = b; Console.WriteLine("Method1(a = " + a + ", b = " + b.ToString() + ") is executed with the result: " + a); if (OnTestDelegate != null) { OnTestDelegate(a, b); } b += 10; return a; } protected string Method2(string a, int b) { this.first_b = b; Console.WriteLine("Method1(a = " + a + ", b = " + b.ToString() + ") is executed with the result: " + a); if (OnTestDelegate != null) { OnTestDelegate(a, b); } return a; } } public class Second : First { private int second_c = 16101975; protected int second_d = 1975; private int Second_C { get { return second_c; } set { second_c = value; } } }
Получение списка private методов
Я создал консольное приложение. В слуаче с консольным приложением некоорые вещи не столь однозначны. Для Windows Form все гораздо проще. Для начала получим список всех методов для объектов классов: First и Second. Метод для получения GetListOfMethods очень простой, принимает Type класса методы которого нужно получить, задает binding flag для ограничения поиска и boolean поле BaseType, чтобы указать, что нужно получить типа класса от которого был наследован текущий. Напримерах будет понятно как меняется результат вывода если передавать BaseType.
public static BindingFlags Flags = BindingFlags.Instance | BindingFlags.GetProperty | BindingFlags.SetProperty | BindingFlags.GetField | BindingFlags.SetField | BindingFlags.NonPublic; public static string GetMethodScope(MethodInfo mi) { if (mi.IsPrivate) return "private"; if (mi.IsPublic) return "public"; if (mi.Attributes == MethodAttributes.Family) return "protected"; return mi.Attributes.ToString(); } public static MethodInfo[] GetListOfMethods(object obj, BindingFlags flags = BindingFlags.Default, bool BaseType = false) { Type type = obj.GetType(); if (BaseType) { type = type.BaseType; // BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);// BindingFlags.GetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); } return GetListOfMethods(type, flags); } public static MethodInfo[] GetListOfMethods(Type type, BindingFlags flags = BindingFlags.Default) { flags = Flags | flags; // get all public static methods of MyClass type MethodInfo[] methodInfos = type.GetMethods(flags); sb.AppendLine("List of private Methods of the class [" + type.Name + "]:"); foreach (MethodInfo mi in methodInfos) { string args = "("; foreach (ParameterInfo arg in mi.GetParameters()) { args += arg.ParameterType.Name + " " + arg.Name + ", "; } args = args.Trim().Trim(',') + ")"; sb.AppendLine("\t" + GetMethodScope(mi) + "\t" + mi.ReturnType.Name + " " + mi.Name + args); } return methodInfos; }
Если в качестве аргумента передать объект созданный от класса First, то результаты выполнения метода будут следующие:
List of private Methods of the class [First]: private Int32 get_First_A() private Void set_First_A(Int32 value) private String Method1(String a, Int32 b) PrivateScope, Family, HideBySig String Method2(String a, Int32 b) PrivateScope, Family, Virtual, HideBySig, VtableLayoutMask Void Finalize() PrivateScope, Family, HideBySig Object MemberwiseClone()
Если отобразить список приватных методов класса Second, то список будет такой:
List of private Methods of the class [Second]: private Int32 get_Second_C() private Void set_Second_C(Int32 value) PrivateScope, Family, HideBySig String Method2(String a, Int32 b) PrivateScope, Family, Virtual, HideBySig, VtableLayoutMask Void Finalize() PrivateScope, Family, HideBySig Object MemberwiseClone()
Из списка пропали private методы родительского класса. Однако остался protected метод Method2.
Если при получении списка методов Second использовать BaseType, то список методов будет как и впервом случае:
List of private Methods of the class [First]: private Int32 get_First_A() private Void set_First_A(Int32 value) private String Method1(String a, Int32 b) PrivateScope, Family, HideBySig String Method2(String a, Int32 b) PrivateScope, Family, Virtual, HideBySig, VtableLayoutMask Void Finalize() PrivateScope, Family, HideBySig Object MemberwiseClone()
Возможность использования BaseType очень важна, поскольку в преокте мы получаем объект созданный от многократно наследованного класса. Чтобы добраться до private методов базовых классов нужно использовать BaseType.
Такие сложности возникли из-за того, что private методы в базовом классе и наследниках могут совпадать по имени. Для internal или protected методов/свойств/полей компилятор проверяет отсуствие коллизий.
Если в BindingFlags задать флаг DeclaredOnly (Specifies that only members declared at the level of the supplied type’s hierarchy should be considered. Inherited members are not considered), то получим следующее:
List of private Methods of the class [First]: private Int32 get_First_A() private Void set_First_A(Int32 value) private String Method1(String a, Int32 b) PrivateScope, Family, HideBySig String Method2(String a, Int32 b)
Т.е. это только методы созданные в классе First без тех, что были отнаследованы от некоторого базового для всех классов. Видно, что для свойств (properties) автоматически было создано два метода get_First_A() возвращающий Int32 и set_First_A(Int32 value), которому в качестве аргумента передается значение Int32.
Список private fields
Для получения списка приватных полей используется метод:
public static FieldInfo[] GetListOfFields(object obj, BindingFlags flags = BindingFlags.Default, bool BaseType = false) { FieldInfo[] fields = null; Type type = obj.GetType(); flags = Flags | flags; if (BaseType) { type = type.BaseType; // BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);// BindingFlags.GetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); } fields = type.GetFields(flags); sb.AppendLine("List of private fields of the class [" + type.Name + "]:"); foreach (FieldInfo field in fields) { sb.Append("\t" + GetFieldScope(field) + " " + field.FieldType.Name + "\t" + field.Name); object value = GetPrivateFieldValue(type, field.Name, obj, flags); if (value != null) { sb.Append(" = " + value.ToString()); } sb.AppendLine(); } sb.AppendLine(); sb.AppendLine("Change value of the private fields of the class " + type.Name + ":"); foreach (FieldInfo field in fields) { if (field.FieldType.Name.ToLower() == "int32") { bool state = SetPrivateFieldValue(obj, field.Name, -16101975, flags, BaseType); if (state) { object value = GetPrivateFieldValue(type, field.Name, obj, flags); if (value != null) { sb.AppendLine("\t" + GetFieldScope(field) + " " + field.FieldType.Name + "\t" + field.Name + " = " + value.ToString()); } } } } return fields; }
При выполении метода на объекте класса First получим такой результат:
List of private fields of the class [First]: private Int32 first_a = 16101975 protected Int32 first_b = 1975 private TestDelegate OnTestDelegate = ReflectionTester.TestDelegate
При запуске на объекте класса Second:
List of private fields of the class [Second]: private Int32 second_c = 16101975 protected Int32 second_d = 1975 protected Int32 first_b = 1975
Видно, что приватные методы базового класса не отображаются, но protected поле first_b отобразилось.
При включении флага DeclaredOnly для класса First получим такой список:
List of private fields of the class [First]: private Int32 first_a = -16101975 protected Int32 first_b = 1675 private TestDelegate OnTestDelegate = ReflectionTester.TestDelegate
Собственно, разницы никакой, поскольку полей отнаследованных нет.
Изменение значений private полей класса в .NET
Итак, у нас есть класс-наследник от некоторого базового класса в котором разработчик поместил в private нужные нам поля. Как их изменить? Код для изменения значения приватных (private) полей:
/// <summary> /// A static method to get the FieldInfo of a private field of any object. /// </summary> /// <param name="type">The Type that has the private field</param> /// <param name="fieldName">The name of the private field</param> /// <returns>FieldInfo object. It has the field name and a useful GetValue() method.</returns> public static FieldInfo GetPrivateFieldInfo(Type type, string fieldName, BindingFlags flags = BindingFlags.Default) { if (flags == BindingFlags.Default) flags = Flags; FieldInfo[] fields = type.GetFields(flags); if (fields != null) { foreach (FieldInfo fi in fields) //fields.FirstOrDefault(feildInfo => feildInfo.Name == fieldName); { if (fi.Name.ToLower() == fieldName.ToLower()) return fi; } } else { sb.AppendLine("There is no any field found."); } return null; } /// <summary> /// A static method to get the FieldInfo of a private field of any object. /// </summary> /// <param name="type">The Type that has the private field</param> /// <param name="fieldName">The name of the private field</param> /// <param name="o">The instance from which to read the private value.</param> /// <returns>The value of the property boxed as an object.</returns> public static object GetPrivateFieldValue(Type type, string fieldName, object o, BindingFlags flags = BindingFlags.Default) { FieldInfo fi = GetPrivateFieldInfo(type, fieldName, flags); if (fi != null) { return fi.GetValue(o); } else { sb.AppendLine("Field with the name [" + fieldName + "] is not found."); } return null; } public static bool SetPrivateFieldValue(object obj, string fieldName, object value, BindingFlags flags = BindingFlags.Default, bool BaseType = false) { Type type = obj.GetType(); if (BaseType) { type = type.BaseType; } FieldInfo info = GetPrivateFieldInfo(type, fieldName, flags); if (info != null) { info.SetValue(obj, value); return true; } else { sb.AppendLine("Field [" + fieldName + "] is not found to SetPrivateFieldValue."); return false; } }
Ничего сложного. Для получения FieldInfo нужного приватного поля достаточно:
- Передать класс в котором оно создано (аргумент type).
- Передать имя поля fieldName.
- Задать флаги BindingFlags для поиска.
Затем вызываем SetValue для изменения значения.
Вызов private методов класса в .NET
Для вызова private методов код чуть посложнее.
public static void ExecMethod(object obj, string MethodName, Object[] arguments, BindingFlags flags = BindingFlags.Default, bool BaseType = false) { //Type t = typeof(HotExchager_Solver.Form1); Type type = obj.GetType(); flags = Flags | flags; if (BaseType) { type = type.BaseType; // BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);// BindingFlags.GetField | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static); } string args = string.Empty; foreach (object arg in arguments) { args += arg.ToString() + ", "; } args = args.Trim().TrimEnd(','); sb.AppendLine("Execute method: " + MethodName + "(" + args +")"); MethodInfo m = type.GetMethod(MethodName, flags); if (m != null) { object result = m.Invoke(obj, arguments); args = string.Empty; foreach (object arg in arguments) { args += arg.ToString() + ", "; } args = args.Trim().TrimEnd(','); sb.AppendLine("The method has returned: " + result.ToString() + " Arguments: " + args); } else { sb.AppendLine("Couldn't found the method [" + MethodName + "]"); } }
В общем-то особо пояснять нечего. По имени метода получили MethodInfo и дальше вызываем Invoke, передав в аргументах параметры. Если аргумент ссылочный вида ref или out, то после вызова invoke массив object будет изменен.
Пример вызова метода:
ExecMethod(obj, "Method1", new object[] { "1016", 1675 }, flags, true);
Подписка на события через reflection
Подписка на события немного отличается в случае консольного приложения. Код для подписки на события такой:
public static void SubscribeEvent(object obj, Type control, Type typeHandler, string EventName, MethodInfo method, bool IsConsole = false) { sb.AppendLine("Subscribe event [" + EventName + "] to the method [" + method.Name + "] using the delegate [" + typeHandler.Name + "]."); EventInfo eventInfo = control.GetEvent(EventName); //"Load" // Create the delegate on the test class because that's where the // method is. This corresponds with `new EventHandler(test.WriteTrace)`. //Type type = typeof(EventHandler); Delegate handler; if (IsConsole) { handler = Delegate.CreateDelegate(typeHandler, null, method); eventInfo.AddEventHandler(obj, handler); } else { handler = Delegate.CreateDelegate(typeHandler, obj, method); eventInfo.AddEventHandler(control, handler); } } public static void SubscribeEvent(object obj, Control control, Type typeHandler, string EventName, MethodInfo method) { if (typeof(Control).IsAssignableFrom(control.GetType())) { SubscribeEvent(obj, control.GetType(), typeHandler, EventName, method); } } public static void UnSubscribeEvent(object obj, Control control, Type typeHandler, string EventName, MethodInfo method) { if (typeof(Control).IsAssignableFrom(control.GetType())) { UnSubscribeEvent(obj, control.GetType(), typeHandler, EventName, method); } } public static void UnSubscribeEvent(object obj, Type control, Type typeHandler, string EventName, MethodInfo method, bool IsConsole = false) { if (obj != null) { EventInfo eventInfo = control.GetEvent(EventName); //Type type = typeof(EventHandler); if (IsConsole) { Delegate handler = Delegate.CreateDelegate(typeHandler, null, method); // detach the event handler if (handler != null) eventInfo.RemoveEventHandler(obj, handler); } else { Delegate handler = Delegate.CreateDelegate(typeHandler, obj, method); // detach the event handler if (handler != null) eventInfo.RemoveEventHandler(control, handler); } } }
Для вызова метода в качестве аргумента нужено передать метод, который по аргументам соответствует делегату. Пример получения MethodInfo в случае если этот метод создан в консольном приложении и статический:
Type type = typeof(Program); MethodInfo method = type.GetMethod("First_OnTestDelegateReflection", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
В качестве obj в параметрах метода передается объект класса в котором создано событие. Например, в классе First определено событие TestDelegate:
public event TestDelegate OnTestDelegate;
- Соотвественно после создания объекта класса First этот объект передается первым аргументом.
- Второй аргумент в случае консольного приложения — тип объекта obj.GetType(). Если бы метод вызывался для Window Form приложения, то в качестве типа передавался бы , например, Control в котром формируется событие. Скажем, кнопка.
- Третий аргумент — делегат: typeof(TestDelegate)
- Четвертый аргумент — имя события. Например, «OnTestDelegate».
- Пятый аргумент — метод, который будет вызываться при возникновении события.
Как использовать возможности reflection
Например, в откомпилированной библиотеке есть некоторая форма в коорой по кнопке вызывается обработчик. Необходимо подправить алгоритм работы метода, который вызывается по кнопке. Как изменить обработчик:
- Отнаследуем класс формы.
- Создадим новый метод — обработчик события срабатывания по кнопке.
- Отпишем от события «Click» метод работающий в оригинальной библиотеке.
- Подпишем на событие «Click» созданный в новом классе метод.
При срабатывании события вызовется вновь созданный метод из корого уже можно вызвать private метод, который был реализован в базовом классе.
private void SubscribeButtonCloseClickEvent(Control btnSearch) { MethodInfo method = typeof(Form_Ext).GetMethod("btnClose_OnClick", BindingFlags.NonPublic | BindingFlags.Instance); if (method != null) { PrivateValueAccessor.SubscribeEvent(this, btnClose, "Click", method); } else { MessageBox.Show("Couldn't find method \"btnClose_OnClick\"."); } }
Например, нажатие на кнопку «Закрыть» вызывает некоторый обработчик, который реализован некорректно и приводит в редких случаях к падению приложения.
В наследнике формы, классе Form_Ext реализован новый метод «btnClose_OnClick», в котором работа обработчика поправлена, добавлены необходимые дополнительные шаги. Этот метод «вешается» в качестве основного обработчика события «Click».
Обработчик «button1_Click» реализованный в базовом классе отписывается от события Click кнопки btnClose:
private void UnsubscribeButtonCloseClickEvent(Control btnSearch) { MethodInfo method = typeof(Form_Ext).BaseType.GetMethod("button1_Click", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); if (method != null) { PrivateValueAccessor.UnSubscribeEvent(this, btnSearch, "Click", method); } else { MessageBox.Show("Couldn't find method \"button1_Click\"."); } }
При вызове метода btnClose_OnClick в нем после выполнения необходимых шагов делается Invoke метода «button1_Click».