Run-time injection - как изменить поведение чужого класса?
От: Albeoris  
Дата: 26.01.14 07:47
Оценка:
Доброго времени суток!

Есть программа, которая использует набор библиотек, написанных на C#.
Очень хочется внести изменения в эти библиотеки, так, чтобы некоторые классы и их методы вели себя иначе.
Библиотеки не обфусцированы, однако скомпилированы под .NET2.0 и содержат много созданного компилятором кода. Их пересоздание — весьма нетривиальный процесс из-за ограничений студии, ругающейся на левые имена и недостижимые метки.

Используя .NET Reflector и плагин Reflexil, я воткнул код, динамически загружающий мою библиотеку.
Вопрос — как теперь внести изменения в типы и методы оригинала?

Если бы методы класса были по-умолчанию виртуальными, я бы просто отнаследовался от интересующего меня класса и переопределил его, после чего заменил бы значение в нужных полях с помощью отражения. К сожалению, это не так.
Была идея вынести все публичные элементы за интерфейс, но я не знаю как в run-time изменить уже загруженный тип, навесить на него интерфейс, и изменить типы существующих полей на этот интерфейс.
Опять же, непонятно — как изменить статичные классы и их методы.

Подскажите, как такое можно провернуть.
Если ничего не получится, придётся всё-таки расковыривать библиотеки. На этот слуйчай, может, кто-нибудь подскажет что-нибудь кроме .NET Reflector и dotPeek для декомпиляции? Или надстройки к ним, позволяющие избавиться от $, >, < в именах?..
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re: Run-time injection - как изменить поведение чужого класса?
От: ldarcy  
Дата: 26.01.14 16:01
Оценка:
Здравствуйте, Albeoris, Вы писали:

A>Доброго времени суток!


A> чтобы некоторые классы и их методы вели себя иначе.


http://msdn.microsoft.com/en-us/library/hh549176.aspx

http://msdn.microsoft.com/en-us/library/hh549176.aspx#bkmk_example__the_y2k_bug
Re[2]: Run-time injection - как изменить поведение чужого класса?
От: Albeoris  
Дата: 26.01.14 16:39
Оценка:
Здравствуйте, ldarcy, Вы писали:

L>http://msdn.microsoft.com/en-us/library/hh549176.aspx

Выглядит шикарно. Вот только, данное решение создаёт обёртки над оригинальными классами и...

var shim = new ShimMyClass();
var instance = shim.Instance;


Заменить старую реализацию на новую невозможно. Все вызовы должны направляться к Shim'ам. Увы, над исходным кодом я почти не властен.
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re: Run-time injection - как изменить поведение чужого класса?
От: hardcase Пират http://nemerle.org
Дата: 26.01.14 19:52
Оценка: +1
Здравствуйте, Albeoris, Вы писали:

A>На этот слуйчай, может, кто-нибудь подскажет что-нибудь кроме .NET Reflector и dotPeek для декомпиляции?


Стандартным ildasm декомпилировать в IL, засандалить необходимый код по всем необходимым местам и собрать сборку обратно с помощью ilasm.
/* иЗвиНите зА неРовнЫй поЧерК */
Re[3]: Run-time injection - как изменить поведение чужого класса?
От: ldarcy  
Дата: 27.01.14 03:37
Оценка:
Здравствуйте, Albeoris, Вы писали:

A>Здравствуйте, ldarcy, Вы писали:


L>>http://msdn.microsoft.com/en-us/library/hh549176.aspx

A>Выглядит шикарно. Вот только, данное решение создаёт обёртки над оригинальными классами и...

в примере The Y2K bug нет никаких оберток. Код вызывает Datetime.Now, как и раньше.
Re: Run-time injection - как изменить поведение чужого класса?
От: Sinclair Россия https://github.com/evilguest/
Дата: 27.01.14 03:49
Оценка:
Здравствуйте, Albeoris, Вы писали:

A>Если ничего не получится, придётся всё-таки расковыривать библиотеки. На этот слуйчай, может, кто-нибудь подскажет что-нибудь кроме .NET Reflector и dotPeek для декомпиляции? Или надстройки к ним, позволяющие избавиться от $, >, < в именах?..

Вы, похоже, выбрали не ту версию языка в настройках рефлектора. Он прекрасно восстанавливает "созданный компилятором код". Поставьте наиболее последнюю версию C#.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re: Run-time injection - как изменить поведение чужого класса?
От: Doc Россия http://andrey.moveax.ru
Дата: 27.01.14 04:22
Оценка:
Здравствуйте, Albeoris, Вы писали:

A>Если бы методы класса были по-умолчанию виртуальными, я бы просто отнаследовался от интересующего меня класса и переопределил его, после чего заменил бы значение в нужных полях с помощью отражения. К сожалению, это не так.


Тогда можно использовать шаблон Декоратор + при необходимости создать прокси-класс, который будет исходного типа (чтобы новый класс можно было использовать вместо исходного там где есть контроль типа).
Re[2]: Run-time injection - как изменить поведение чужого класса?
От: Doc Россия http://andrey.moveax.ru
Дата: 27.01.14 04:22
Оценка: +1
Здравствуйте, ldarcy, Вы писали:

L>http://msdn.microsoft.com/en-us/library/hh549176.aspx

L>http://msdn.microsoft.com/en-us/library/hh549176.aspx#bkmk_example__the_y2k_bug

Сама Microsoft подчеркивает что shims это средство для unit-test и не предназначено для production кода.
Re[2]: Run-time injection - как изменить поведение чужого класса?
От: Albeoris  
Дата: 28.01.14 22:01
Оценка:
Здравствуйте, Doc, Вы писали:
Doc>Тогда можно использовать шаблон Декоратор + при необходимости создать прокси-класс, который будет исходного типа (чтобы новый класс можно было использовать вместо исходного там где есть контроль типа).
Этож не плюсы, где можно что угодно приводить к чему угодно. Это безопасный C#, в котором я могу отнаследоваться от какого-нибудь типа и даже подсунуть свой экземпляр вместо оригинального, но все не виртуальные методы будут вызываться у базового класса, а не моя имплементация.
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[2]: Run-time injection - как изменить поведение чужого класса?
От: Albeoris  
Дата: 28.01.14 22:04
Оценка:
Здравствуйте, Sinclair, Вы писали:
S>Вы, похоже, выбрали не ту версию языка в настройках рефлектора. Он прекрасно восстанавливает "созданный компилятором код". Поставьте наиболее последнюю версию C#.

Вот библиотеки:
http://yadi.sk/d/7tBnIwlMGjQdQ

Версия верная — .NET 2.0, в котором, используя MonoDevelop, используется System.Core 3.5. Ошибок при декомпиляции тьма, но их можно исправить.
Этими сборками оперирует Unity3D, и даже после исправлениях всеш ошибок, приложение падает при попытке сериализовать данные редактора и инжектировать их в новую либу. =\
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[2]: Run-time injection - как изменить поведение чужого класса?
От: Albeoris  
Дата: 28.01.14 22:06
Оценка:
Здравствуйте, hardcase, Вы писали:
H>Стандартным ildasm декомпилировать в IL, засандалить необходимый код по всем необходимым местам и собрать сборку обратно с помощью ilasm.

На данный момент, это единственный из гарантированно рабочих вариантов. Но писать код на IL едва ли кто-нибудь будет. Но ты навёл меня на интересную мысль — ведь ничто не мешает мне декомпилировать всю сборку в IL, после чего убрать все аттрибуты sealed у классов и добавить virtual всем методам. Верно? Это можно сделать как-нибудь автоматически?
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[3]: Run-time injection - как изменить поведение чужого класса?
От: Doc Россия http://andrey.moveax.ru
Дата: 29.01.14 03:03
Оценка:
Здравствуйте, Albeoris, Вы писали:

A>Этож не плюсы, где можно что угодно приводить к чему угодно. Это безопасный C#, в котором я могу отнаследоваться от какого-нибудь типа и даже подсунуть свой экземпляр вместо оригинального, но все не виртуальные методы будут вызываться у базового класса, а не моя имплементация.


Что-то вы совсем не туда.

1) Для декоратора наследование не обязательно.
http://andrey.moveax.ru/patterns/oop/structural/decorator/

2) Dynamic proxy поможет создать имитацию требуемого типа
http://andrey.moveax.ru/post/csharp-patterns-dynamic-decorator-part2.aspx
Re[4]: Run-time injection - как изменить поведение чужого класса?
От: Albeoris  
Дата: 29.01.14 05:04
Оценка:
Здравствуйте, Doc, Вы писали:
Doc>Что-то вы совсем не туда.

Doc>1) Для декоратора наследование не обязательно.

Doc>http://andrey.moveax.ru/patterns/oop/structural/decorator/
А полиморфизм обязателен.

Doc>2) Dynamic proxy поможет создать имитацию требуемого типа

Doc>http://andrey.moveax.ru/post/csharp-patterns-dynamic-decorator-part2.aspx
При условии наличия интерфейса.

Окей, чтобы было лучше понятно, объясню на пальцах. Есть некоторые метод, который принимает на вход DateTime. У него он дергет свойство Year. Моя задача — сделать так, чтобы либо метод принимал на вход DateTimeEx, либо я мог передать ему DateTimeEx несмотря на то, что он хочет DateTime. Как я понимаю, ни то ни другое невозможно без изменения IL-кода. Моя задача — изменить его и реализовать задуманное. DateTime не реализует метод IDateTime. Он не позволяет отнаследоваться от него и переопределить свойство Year. И dynamic ему тоже не скормишь.

Поэтому ни фейковые сборки, ни декораторы мне не подходят. Что очень печалит. Я бы очень хотел верить в магию...
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[5]: Run-time injection - как изменить поведение чужого класса?
От: Sinix  
Дата: 29.01.14 05:24
Оценка: +1
Здравствуйте, Albeoris, Вы писали:

A>Окей, чтобы было лучше понятно, объясню на пальцах. Есть некоторые метод, который принимает на вход DateTime. У него он дергет свойство Year. Моя задача — сделать так, чтобы либо метод принимал на вход DateTimeEx, либо я мог передать ему DateTimeEx несмотря на то, что он хочет DateTime.

В общем случае (если требуется решение для продакшна, с лицензионной чистотой и беспроблемным обновлением библиотек) задача не решается.

Частные решения:
1. Покупаем исходники, правим.
2. Пишем патчер, который будет править чужой il-код, см в сторону Mono.Cecil и обёрток вокруг него. Если сборка подписана строгим именем, придётся править все зависящие от неё сборки.
3. Перестаём самому стругать себе грабли, отходим на шаг назад и смотрим, можно ли решить исходную проблему без правки чужих библиотек.
Re[6]: Run-time injection - как изменить поведение чужого класса?
От: Albeoris  
Дата: 29.01.14 07:00
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Частные решения:

S>1. Покупаем исходники, правим.
Денег не хватит. Переговоры ведутся, но вероятность благоприятного исхода крайне мала.

S>2. Пишем патчер, который будет править чужой il-код, см в сторону Mono.Cecil и обёрток вокруг него. Если сборка подписана строгим именем, придётся править все зависящие от неё сборки.

В своё время пытался освоить Mono.Cecil, но всё упрёлось в то, что абсолютно верный (на мой взгляд) припер отказывался работать и не вносил никаких изменений, осталяя библиотеку неизменной. Если не сложно — можно пример по превращению не виртуальных методов в виртуальные (на примере любой либы, любого типа, любого метода — полноценную реализацию я сделаю сам)?

S>3. Перестаём самому стругать себе грабли, отходим на шаг назад и смотрим, можно ли решить исходную проблему без правки чужих библиотек.

Нельзя, так как это попытка прикрутить систему плагинов к приложению на них не расчитанному.
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[7]: Run-time injection - как изменить поведение чужого класса?
От: Sinix  
Дата: 29.01.14 07:16
Оценка:
Здравствуйте, Albeoris, Вы писали:

A>В своё время пытался освоить Mono.Cecil, но всё упрёлось в то, что абсолютно верный (на мой взгляд) припер отказывался работать и не вносил никаких изменений, осталяя библиотеку неизменной. Если не сложно — можно пример по превращению не виртуальных методов в виртуальные (на примере любой либы, любого типа, любого метода — полноценную реализацию я сделаю сам)?

Что-то такое делал, но очень давно. Вечером попробую, по результатам отпишусь.

S>>3. Перестаём самому стругать себе грабли, отходим на шаг назад и смотрим, можно ли решить исходную проблему без правки чужих библиотек.

A>Нельзя, так как это попытка прикрутить систему плагинов к приложению на них не расчитанному.
О как. Удачи, что ещё сказать
Re[8]: Run-time injection - как изменить поведение чужого класса?
От: Albeoris  
Дата: 29.01.14 08:40
Оценка: 24 (1)
Здравствуйте, Sinix, Вы писали:

A>>В своё время пытался освоить Mono.Cecil, но всё упрёлось в то, что абсолютно верный (на мой взгляд) припер отказывался работать и не вносил никаких изменений, осталяя библиотеку неизменной. Если не сложно — можно пример по превращению не виртуальных методов в виртуальные (на примере любой либы, любого типа, любого метода — полноценную реализацию я сделаю сам)?

S>Что-то такое делал, но очень давно. Вечером попробую, по результатам отпишусь.

Хм. Так, с этим отбой. Всё оказалось очень просто. Не знаю — почему раньше не получалось:

private static void Main(string[] args)
{
    if (args.Length == 0)
        return;

    string assemblyPath = args[0];

    AssemblyDefinition assembly = AssemblyDefinition.ReadAssembly(assemblyPath);
    ChangeAssembly(assembly);
    assembly.Write(assemblyPath);
}

private static void ChangeAssembly(AssemblyDefinition assembly)
{
    foreach (TypeDefinition type in assembly.MainModule.Types)
        ChangeType(type);
}

private static void ChangeType(TypeDefinition type)
{
    if (type.IsSealed)
        type.IsSealed = false;
    if (!type.IsPublic)
        type.IsPublic = true;

    foreach (FieldDefinition field in type.Fields)
        ChangeField(field);
    foreach (MethodDefinition method in type.Methods)
        ChangeMethod(method);
    foreach (TypeDefinition child in type.NestedTypes)
        ChangeType(child);
}

private static void ChangeField(FieldDefinition field)
{
    if (field.IsInitOnly)
        field.IsInitOnly = false;
    
    if (!field.IsPublic)
        field.IsPublic = true;
}

private static void ChangeMethod(MethodDefinition method)
{
    if (!method.IsPublic)
        method.IsPublic = true;
    
    if (!method.IsStatic && !method.IsVirtual && !method.IsAbstract)
    {
        method.IsVirtual = true;
        method.IsNewSlot = true;
    }
}


Но тут возникла другая проблема — прога благополучно падает с матами о том, что десериализуемый тип не имеет конструктора по-умолчанию. Смотрю рефлектором — действительно не имеет. Причём и до моих изменений. То ли там местный AOP развлекается, то ли конструкторы по умолчанию погибли смертью храбрых, то ли ошибки где-то ещё и ему не очень нравятся мои структуры с виртуальными методами. :D
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[9]: Run-time injection - как изменить поведение чужого класса?
От: Sinix  
Дата: 29.01.14 08:53
Оценка:
Здравствуйте, Albeoris, Вы писали:

A>Но тут возникла другая проблема — прога благополучно падает с матами о том, что десериализуемый тип не имеет конструктора по-умолчанию.

Куча причин может быть, навскидку не угадаешь.

A>и ему не очень нравятся мои структуры с виртуальными методами. :D

???
Структуры не поддерживают объявление виртуальных методов напрямую, только реализацию методов из объявленных интерфейсов и переопределение методов object.

Да и смысл? Один фиг наследника от структуры не создашь.

В общем, чтобы не было дальнейших сюрпризов, очень советую прогнать PEVerify на полученном бинарнике.
Re[9]: Run-time injection - как изменить поведение чужого класса?
От: Albeoris  
Дата: 29.01.14 08:57
Оценка:
A>Но тут возникла другая проблема — прога благополучно падает с матами о том, что десериализуемый тип не имеет конструктора по-умолчанию. Смотрю рефлектором — действительно не имеет. Причём и до моих изменений. То ли там местный AOP развлекается, то ли конструкторы по умолчанию погибли смертью храбрых, то ли ошибки где-то ещё и ему не очень нравятся мои структуры с виртуальными методами. :D

Забавно. Методы замечательно паблишатся и виртуалятся. Проблемы только с полями. Потому что как только я делаю приватные поля класса публичными, он тут же требует конструктор без параметров. Мистика какая-то... Ну да ладно, сейчас каким-нибудь кастылём подопру...
Видимо, в этом направлении и буду копать. Теперь, имея под рукой уже изрядно исковерканную библиотеку, где всё, что угодно можно переопределять, наследовать и задавать, останутся всякие мелочи.
Наверное, было бы весьма забавно воткнуть туда Dependency Injection или выделить из всех типов интерфейсы и реализовать их динамическую загрузку из библиотек-плагинов. ^-^
"Хаос всегда побеждает порядок, поскольку лучше организован." (с) Терри Пратчетт
Re[9]: Run-time injection - как изменить поведение чужого класса?
От: Sinix  
Дата: 29.01.14 09:05
Оценка:
P.S. По колу:
1. type.IsSealed = false; не надо выполнять для структур, делегатов и static-типов. type.IsPublic = true — пропускать для сгенеренных типов (замыкания, .Module, энумераторы и авайтеры).
2. field.IsPublic = true; — нужно автосгенеренные поля (для свойств/событий/лямбд/dynamic etc)

3. if (!method.IsStatic .. ) — + проверку на структуры и автосгенеренные типы.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.