неділя, 28 серпня 2011 р.

Mono.Cecil vs SystemReflection

Бесспорно, Mono.Cecil круче навороченее и всякое такое.
Но если небоходимо сравнить 2 метода Cecil не представляет быстрого способа это сделать.
Сесил не сохраняет информации о том, что реализация метода это просто набор байт. Есть возможность посмотреть на высокоровневые штуки под названием Opcode. Можно посмотреть на параметры опкода. Но получить набор байт представляющих метод нереально.
Приходится  делать так

var bytes = string.Join(",", method.Body.Instructions);

С другой стороны, System.Reflection.MethodInfo позволяет сделать GetILAsByteArray.
Но с ним не будет работать Cecil.Decompiler.

З.Ы. Понятное дело что исходники Mono.Cecil открыты и можно отправить туда патч. Но примут его или нет - вопрос, а плодить сущности без необходимости не хочется.

четвер, 25 серпня 2011 р.

AsmDiff unit testing

Пытаюсь добавить юнит тесты к проекту.   И обнаруживаю что не все так просто.

Тот же самый ParameterDefinition из Mono.Cecil объявлен как sealed. Это значит, что например Moq не сможет с ним работать. Придется смотреть на RhinoMock  и товарищей. Хотя что-то мне подсказывает что они тоже обломаются.
Upd. RhinoMock тоже не сумел.

Сюрпризы на этом не заканчиваются.

Например, Mono.Cecil теряет информацию о generic параметрах метода в FullString. В качестве примера


// three genrics, parameter uses generic type, return type int
        int ThreeGenericsInt<T, S, Q>(Dictionary<S, Q> dic)
превращается в 

System.Int32 CoolLibrary.Test::ThreeGenericsInt(System.Collections.Generic.Dictionary`2<S,Q>)


Если убрать generic параметр T, 

// two genrics, parameter uses generic type, return type int
        int ThreeGenericsInt<S, Q>(Dictionary<S, Q> dic)
на выходе будет то же самое

System.Int32 CoolLibrary.Test::ThreeGenericsInt(System.Collections.Generic.Dictionary`2<S,Q>)
И удобный способ заполнения Dictionary на основе method.FullName идет лесом. Конечно вопрос решаемый, но подобных мелочей выскакивает слишком уж много.

Например, считать 2 следующих определения изменением одного метода, или удалением первого, и добавлением нового?

        int ThreeGenericsInt<T, S, Q>(Dictionary<T, Dictionary<S, Q>> dic)
        int ThreeGenericsInt<T, S, Q>(Dictionary<T, Dictionary<S, Q>> dic) where T: IEnumerable
А ведь еще есть атрибуты, изменение котрых тоже стоит отслеживать, поля и свойства и всякое такое прочее.

неділя, 21 серпня 2011 р.

AsmDiff ver0.2

Немного пооптимизировал https://github.com/mungobungo/AsmDiff

diff для .net 2.0 -> .net 4.0 считается около минуты.

Можно было бы еще больше ускорить.  Но одна из тормозных вещей - попытка загрузить как сборку .dll файл не являющийся сборкой. Как подсказал мне добрый профайлер от Eqatec, на эту фигню тратится 20% времени.

Размер файла диффа - 70М, поэтому в релизе сейчас выводятся только названия измененных методов.

Как оказалось в Cecil.Decompiler  чуть более чем дофига NotImplementedException. Поэтому пока  сравниваются и отображаются только байткоды.

Как еще оказалось, в .Net нет метода, который по .dll файлу скажет, сборка ли это или просто бинарник. Только исключениями BadImageException.

Lessons re-learned

  1. Ленивость хорошо, круто, красиво, но абсолютно недебагабельно, и непрофилируемо.Поэтому скорее всего вместо yield return будет стоять жесткий return,  а для асинхронности будут генирироваться события. Пичалька на самом деле. Хотелось к IEnumerable результатам сравнения прикрутить Rx Framework в гуй слое.
  2. Dictionary<T1,T2> имеет очень тормозной метод ContainsKey(). Замена его на try catch{} ускоряет ощутимо. Хотя казалось бы...

Дальнейшие теловдижения/апдейты



  • Улучшить сравнение методов. Сейчас сравнение идет по Method.FullName. Но в Mono.Cecil метод, имеющий сигнатуру int dummy(int x) и метод int dummy<T>(int x) имеют одинаковое имя. Вопрос зачем объявлять Generic тип, который не используется в сигнатуре вопрос открытый, но в .Net framework таковые имеются.
  • Добавить ключей к командной строке. Выводить/не выводить метод, делать ли попытки декомпиляции метода, рассматривать ли вероятность переименованных сборок и т.п.
  • Добавить вывод в .html файло с простейшими jquery bells & whistels.
  • Добавить рекурсивный разбор папок.
[bredogenerator = on]
А вот интересно, если взять да и написать такую же штуку да на чистом Хаскеле. Разобрать PE файлик труда особого не составит. Из метаданных вытянуть названия методов скорее всего тоже.
[bredegenerator = off]

Немного результатов. Сколько методов изменилось при переходе от .net 2.0 к .net 4.0. Не учитываются добавленные/удаленные методы, только те, у которых поменялась реализация.


System.Windows.Forms.dll
System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561
934e089
Classes: 2257, Methods : 27965
System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561
934e089
Classes: 2268, Methods : 28151
Different methods : 700



System.Web.dll
System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
Classes: 1954, Methods : 23661
System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
Classes: 2119, Methods : 24681
Different methods : 1376




System.dll
System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Classes: 1988, Methods : 15498
System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Classes: 2115, Methods : 16480
Different methods : 1584



CommonLanguageRuntimeLibrary
mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Classes: 2344, Methods : 21474
mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Classes: 2781, Methods : 25089
Different methods : 3410


пʼятниця, 19 серпня 2011 р.

AsmDiff вер0.1

Известный в узких кругах Eugene Kirpichov написал в гугллоплюсе


Project to do when at the airport: .NET assembly diff tool. Read two folders of assemblies, report what's different. Should take a few hours and be very useful from the start - surprisingly there's no free tool to do that. I need to know what's really different (e.g. which methods have different code) and I'm not interested in a differing build timestamp or GUID.


У меня выдались свободных полчаса и тулзу я сделал.


На c# c Linq оно решается в один метод.

Code Snippet
  1. public static IEnumerable<DiffRecord> CecilAssebmlyDiff(AssemblyDefinition firstAssembly, AssemblyDefinition secondAssembly)
  2. {
  3.     var firstData = from module in firstAssembly.Modules
  4.                     from type in module.GetTypes()
  5.                     from method in type.Methods
  6.                     let parameters = method.Parameters
  7.                     let returnType = method.ReturnType
  8.  
  9.                     // for some system methods body can be null, so we should check it
  10.                     where method.HasBody
  11.                     let instructions = method.Body.Instructions
  12.  
  13.                     let code = method.SourceCode()
  14.                     let bytes = code
  15.                     select new { type, method, parameters, returnType, bytes };
  16.  
  17.     var secondData = from module in secondAssembly.Modules
  18.                      from type in module.GetTypes()
  19.                      from method in type.Methods
  20.                      let parameters = method.Parameters
  21.                      let returnType = method.ReturnType
  22.  
  23.                      // for some system methods body can be null, so we should check it
  24.                      where method.HasBody
  25.                      let instructions = method.Body.Instructions
  26.  
  27.                      let code = method.SourceCode()
  28.                      let bytes = code
  29.                      select new { type, method, parameters, returnType, bytes };
  30.  
  31.     var changedMethods = from firstMethod in firstData
  32.                          from secondMethod in secondData
  33.                          where
  34.                              // type names are the same
  35.                          firstMethod.type.FullName.Equals(secondMethod.type.FullName)
  36.                              // method names are the same
  37.                           && firstMethod.method.Name == secondMethod.method.Name
  38.                              // return typse are the same
  39.                           && firstMethod.returnType.FullName == secondMethod.returnType.FullName
  40.                              // parameters are the same
  41.                           && firstMethod.parameters.Except(secondMethod.parameters, x => x.ParameterType.FullName).Count() == 0
  42.                              //!!! dirty hack, byte arrays compared by their string representations
  43.                              // bodies ARE NOT the same
  44.                          && !firstMethod.bytes.Equals(secondMethod.bytes)
  45.  
  46.                          select new { firstMethod = firstMethod, secondMethod = secondMethod };


Собственно, ничего военного. Можно было бы вынести загрузку данных для различных сборок в отдельный метод, но лениво.

Результат работы выглядит следующим образом:
$ asmdiff first second
CoolLibrary.dllCoolLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=nullInt32 Test.Sample(x:String)
private int Sample(string x){return 66;}

CoolLibrary.dllCoolLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=nullInt32 Test.Sample(x:String)
private int Sample(string x){return 33;}

Архив с кодом на файлопомойке
В ближайшее время залью на какой-нибудь github.

Скачать сорцы и примеры можно здесь : проект на гитхабе

использовать просто "asmdiff folder1 folder2"

Зы. Рекурсивного обхода директорий нет.
ЗЫЫ. Обработки исключений тоже пока никакой.