Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

.NET dynamic, Unity и оплошность в RuntimeBinder

Anna | 17.06.2014 | нет комментариев

Предыстория

Новая версия плана была удачно закончена, протестирована, и теснее устанавливалась заказчикам. Ничего не предвещало беды, все шло по плану и дозволено было немножко расслабиться.
Невзначай, от всех заказчиков стали поступать претензии на ошибки, которые посыпались при попытке применять новую версию программы. Это было дюже необычно, т.к. все было проверено и должно было трудиться как часы, но этого не случилось.

Оплошность, которая возникла на всех рабочих станциях, где была установлена новая версия программы, выглядела так:

System.IndexOutOfRangeException: Index was outside the bounds of the array at Microsoft.CSharp.RuntimeBinder.ExpressionTreeCallRewriter.GetMethodInfoFromExpr(EXPRMETHODINFO methinfo) ...

И, как потом выяснилось, возникала она в безвредном, на 1-й взор, куске кода:

public void FillFrom(dynamic launch)
{
  Log.ShowLog(launch.Id);
}

Из-за того, что у заказчиков на компьютерах стоит Windows XP, мы ограничены в применении .NET Framework’ом 4.0 версией, т.к. версию выше на XP поставить теснее невозможно. Следственно, наш план нацелен на применение именно этой версии фреймворка, невзирая на то, что на наших компьютерах давным-давно стоит VS 2012 и фреймворк 4.5. Это и повлияло на отсутствия ошибки у нас. Следственно, пришлось узнать, что же все таки стало поводом этой ошибки и как нам с ней бороться.

Суть задачи

Из-за применения dynamic объекта при вызове способа компилятор трансформирует эту строчку кода в целый кусок. И в этом сгенерированном коде, взамен прямого вызова способа, происходит определение способа в процессе выполнения. Так называемое «позже связывание». Т.е., у объекта Log в процессе выполнения, определенным образом ищется способ «ShowLog» для последующего вызова. Сгенерированный компилятором код использует RuntimeBinder для поиска надобного способа. Но, что-то вульгарно не так и в процессе выполнения находился неверный способ. Именно при обзоре его параметров и происходила указанная выше оплошность, потому что их число не совпало с ожидаемым.

Повод задачи

Но как же такое могло случиться? Оказывается, RuntimeBinder первоначально находит необходимый способ и потом, в своих недрах берёт все доступные способы у определенного типа и после этого сопоставляет ихmetadata token с токеном обнаруженного способа. При совпадении токенов, биндер берёт совпавший способ и пытается исследовать его параметры, что в нашем случае приводит к ошибке.
Токены могли совпасть только у способов из различных сборок, т.к. в рамках одной сборки номера токенов не могли бы пересечься. И подлинно, такая вероятность была, т.к. класс, в котором появилась задача, был преемником класса из иной сборки.

Сейчас все казалось простым — стремительно написать маленький тестовый пример из 2-х сборок, иерархии 2-х классов и пары способов. Добиться того, Дабы токен способа преемника и способа базового класса совпали, и вызвать способ преемника с применением динамической переменной. Но не здесь-то было. Хоть пример оказался и крайне небольшим и токены совпадали — ничего не происходило и все работало надлежащим образом.

Капнув чуть глубже выяснилось, что RuntimeBinder для поиска подходящего способа берет не все подряд, а только определенные способы, которые попадают под действие определенного фильтра:

BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic

Таким образом, стало ясно, что способы из базового класса никаким образом в поиске надобного не участвуют.

Покопав дальше, стало ясно, что токен способа совпал с токеном не «обыкновенного» способа из базового класса, а с аксессором свойства, которое было определено в базовом классе. Т.е., токен способа «ShowLog» совпал с токеном способа «get_IsNotifying». А вот теснее данный аксессор восхитительно проходил вышеуказанный фильтр.

Казалось, что результат, наконец-то, обнаружен и тестовый пример был немножко откорректирован — в базовом классе возникло качество, токен способа get_… которого совпадал с токеном способом «ShowLog». Но, невзирая на все попытки, тестовый пример работал без ошибок. Способы get_ и set_ из базового класса хоть и проходили фильтр, но находились в конце спискавсех отобранных способов, в начале которого находился верный способ «ShowLog», тот, что удачно определялся RuntimeBinder’ом.

Роль Unity

Но не напрасно в заголовке темы присутствует наименование Unity. Это dependency injection , контейнер от майкрософт, тот, что мы используем в плане.
Как выяснилось, способом тыка, его участие как-то влияло на возникновение этой задачи. Пришлось глядеть, что же такого странного делает контейнер. Позже небольшого постижения, стало ясно, что суть его работы такая: Дабы сделать экземпляр определенного типа, он генерирует особый способ.
Код этого способа заполняется несколькими, предопределенными стратегиями. В нашем случае это были три стратегии:

  • Тактика, которая перебирала все конструкторы у класса для constructor injection
  • Тактика, которая перебирала все свойства класса для для property injection
  • Тактика, которая перебирала все способы класса для method injection

Выполнялись они именно в такой последовательности. Как выяснилось, именно в порядке этого перебора был спрятан конечный ломтик пазла.

Если легко сделать экземпляр какого-то типа, а потом перечислить все способы этого типа, то, в первую очередь, будут идти обыкновенные способа, а лишь после этого теснее аксессоры свойств и событий. Но, если до первого создания объекта этого типа перечислить, скажем, все свойства, то аксессоры, из которых они состоят попадут в предисловие списка.

Получается, Дабы спровоцировать ошибку и принудить RuntimeBinder выбирать неверный способ, необходимо Дабы аксессор с подходящим токеном шел по списку перед верным способом. Дабы аксессор был в начале списка довольно было вызвать, до первого создания объекта данного типа, дальнейший код:

typeof(Log).GetEvents();
typeof(Log).GetProperties();
new Log();

Вследствие такой последовательности действий, список способов данного типа вначале заполняется аксессорами событий, потом свойств. А после этого, позже создания объекта, заполняется каждому остальным. Позже этого, воспроизвести загвоздку на маленьком тестовом примере оказалось проще простого.

Отличия в фреймворках

Отчего же задача проявлялась только на тех машинах, где был 4 фреймворк, а на других нет, не смотря на то, что план нацелен на 4 фреймворк? Как оказалось, в 4.5 фреймворке версия Microsoft.CSharp.dll отличается от версии в 4 фреймворке, хоть и незначительно: В 4 фреймворке это версия за номером 4.0.30319.1, а в 4.5 это версия 4.0.30319.17929, в которой, видимо, поспели исправить некоторые ошибки.

Если взглянуть на код проблемного способа, то он изменился вовсе немножко, было:

MethodInfo[] methods = type.GetMethods(BindingFlags.Instance ...);
for (int i = 0; i < methods.Length; i  )
{
if (methods[i].MetadataToken == methodInfo.MetadataToken)
...

стало:

MethodInfo[] methods = type.GetMethods(BindingFlags.Instance...);
for (int i = 0; i < methods.Length; i  )
{
if (methods[i].MetadataToken == methodInfo.MetadataToken && !(methods[i].Module != methodInfo.Module))
...

Так что, таким вот двойным отрицанием данный баг был поправлен в 4.5 фреймворке.

Итоги ошибки

Как выяснилось опытным путем, итоги такой ошибки могут быть и другими, чай аксессоры есть не только у свойств, но и у событий. И у некоторых акксессоров есть параметры. А у аксессеров индексаторов — параметров может вообще быть разное число.

Получается, что могут появиться ошибки дальнейшего типа, если RuntimeBinder выбирает неверный способ:

  • если выбран способ с меньшим числом параметров — возникает оплошность IndexOutOfRangeException
  • если выбран способ с таким же числом параметров, типы которых всецело совпадают с ожидаемыми, — тогда легко будет вызван неверный способ, а если способ что-то возвращает, то вернется итог работы неправильного способа.
  • если выбран способ с огромным числом параметров, и типы нужного числа параметры всецело совпали с ожидаемыми, то будет вызван неверный способ и произойдет оплошность ArgumentException: Incorrect number of arguments supplied for call to method

Как с этим жить?

На connect.microsoft.com данная задача была зарегистрирована, и, судя по написанному, исправления для 4 фреймворка нет и не будет. Скорее каждого, у большинства данная задача может никогда не появиться т.к. Дабы она случилась необходимо огромное стечение обстоятельств.

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

Для себя мы решили, на нынешний момент, сделать небольшую утилиту в виде MSBuild task, которую мы добавили в процесс билда. Эта утилита анализирует сборки, применяя Mono.Cecil, Дабы иметь вероятность комфортным образом просматривать инструкции способов. В процессе обзора, утилита ищет определенные последовательность инструкций, постигая их операнды, получает тип и наименование способа, тот, что будет искать RuntimeBinder и проверяет не может ли случиться описанная задача. В случае, если такой проблемный вызов будет обнаружен, то при билде плана появится оплошность.

Другими словами, избежать ошибок вообще дозволено только установив пользователям версию фреймворка 4.5. Если же это немыслимо (как в нашем случае), придётся либо не пользоваться dynamic, либо пользоваться с осторожностью.

Тестовый пример

Дабы увидеть ошибку, это код нужно запустить на компьютере с установленным .NET Framework версии 4.0.
Для контраста, на компьютерах с фремворком версией выше все отработает, как нужно.

Assembly A:
A.cs

public class A
{
public void MethodForTokenOffset() {}

public event EventHandler Event
{
add { Console.WriteLine("Event, add");}
remove {}
}

public object this[long id]
{
get
{
Console.WriteLine("Indexator, get {0}", id);
return new { Name = "ThisIsSomeObject" };
}
set { Console.WriteLine("Indexator, set {0}", id); }
}
}

AssemblyB
Program.cs

class Program
{
static void Main()
{
typeof(B).GetEvents();
typeof(B).GetProperties();
new B();
Console.ReadLine();
}
}

B.cs

public class B : A
{
public B()
{
try
{
dynamic obj = new { Handler = new EventHandler((s, e) => Console.WriteLine("EventHandler")), Id = 1L };
MethodForEvent(obj.Handler);
var result = MethodForIndexator(obj.Id);
Console.WriteLine("Method result, {0}", result);
MethodForProperty(obj.Id);
}
catch (Exception e)
{
Console.WriteLine(e);
}
}

public void MethodForEvent(EventHandler handler)
{
Console.WriteLine("MethodForEvent, {0}", handler);
}

public void StubMethodForOffset()
{
}

public long MethodForIndexator(long id)
{
Console.WriteLine("MethodForIndexator, {0}", id);
return 0;
}

public void MethodForProperty(long id)
{
Console.WriteLine("MethodForProperty, {0}", id);
}
}
 Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB