Prehistory
The new version of the project was successfully completed, tested, and has already been installed to clients. Nothing foreshadowed trouble, everything went according to plan and you could relax a little.
Suddenly, all clients began to receive complaints about errors that fell when trying to use the new version of the program. It was very strange, because everything was checked and had to work like a clock, but this did not happen.
The error that appeared on all workstations where the new version of the program was installed looked like this:
System.IndexOutOfRangeException: Index was outside the bounds of the array
at Microsoft.CSharp.RuntimeBinder.ExpressionTreeCallRewriter.GetMethodInfoFromExpr(EXPRMETHODINFO methinfo)
...
And, as it turned out, she appeared in a seemingly innocuous piece of code:
public void FillFrom(dynamic launch) { Log.ShowLog(launch.Id); }
Due to the fact that clients have computers running Windows XP, we are limited to using the .NET Framework 4.0 version, since The version above on XP can not be put. Therefore, our project aims to use exactly this version of the framework, despite the fact that VS 2012 and 4.5 framework have been on our computers for a long time. This affected the lack of error with us. Therefore, it was necessary to find out what all the same was the cause of this error and how we fight it.
')
The essence of the problem
Due to the use of the dynamic object when calling a method, the compiler turns this line of code into a whole piece. And in this generated code, instead of a direct method call, the method is defined during execution. The so-called
"late binding .
" That is, the Log object is looking for the ShowLog method in a certain way for further call. The code generated by the compiler uses RuntimeBinder to find the desired method. But, something went wrong and the wrong method was in the process of execution. It was during the analysis of its parameters that the above error occurred, because their number did not coincide with the expected one.
Cause of the problem
But how could this happen? It turns out that RuntimeBinder initially finds the desired method and then, in its depths, takes all the available methods from a particular type and then compares their
metadata token with the token of the found method. If the tokens match, the Binder takes the matched method and tries to analyze its parameters, which in our case leads to an error.
Tokens could match only from methods from different assemblies, since within one assembly, the token numbers could not intersect. And indeed, this opportunity was because the class in which the problem occurred was a successor to a class from another assembly.
Now everything seemed simple - quickly write a small test example of two assemblies, a hierarchy of two classes and a couple of methods. To ensure that the token of the method of the heir and the method of the base class coincide, and call the method of the heir using the dynamic variable. But it was not there. Although the example turned out to be very small and the tokens coincided - nothing happened and everything worked properly.
Dropping a little deeper, it turned out that RuntimeBinder to search for a suitable method takes not everything, but only certain methods that fall under the action of a particular filter:
BindingFlags .Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
Thus, it became clear that the methods from the base class are not involved in the search for the right.
Having dug further, it became clear that the token of the method coincided with the token not of the “usual” method from the base class, but with the
accessor of the property that was defined in the base class. That is, the token of the ShowLog method coincided with the token of the get_IsNotifying method. And now this accessor perfectly passed the above filter.
It seemed that the answer was finally found and the test case was slightly corrected — a property appeared in the base class, the get_ method token ... of which coincided with the showLog method. But, despite all the attempts, the test case worked without errors. The get_ and set_ methods from the base class, though they passed the filter, were at the end of the list of all selected methods, at the beginning of which was the correct ShowLog method, which was successfully determined by RuntimeBinder.
Unity role
But not in vain in the title of the theme there is the name
Unity . This is
dependency injection , a container from Microsoft, which we use in the project.
As it turned out, at random, his participation somehow influenced the appearance of this problem. I had to look at what makes such a container unusual. After a little study, it became clear that the essence of his work is this: to create an instance of a particular type, it generates a special method.
The code for this method is populated with several predefined strategies. In our case, these were three strategies:
- A strategy that went through all the constructors in the class for constructor injection
- A strategy that went through all the class properties for property injection
- A strategy that went through all the class methods for method injection
They were executed exactly in this sequence. As it turned out, it was in the order of this search that the last piece of the puzzle was hidden.
If you simply create an instance of some type, and then list all the methods of this type, then, first of all, the usual method will go, and only then the accessors of properties and events. But, if, prior to the first creation of an object of this type, to list, for example, all the properties, then the accessors of which they consist will fall to the top of the list.
It turns out that in order to provoke an error and force RuntimeBinder to choose the wrong method, it is necessary that the accessor with the appropriate token go through the list before the correct method. For the accessor to be at the top of the list, it was enough to call the following code before creating the first object of this type:
typeof(Log).GetEvents(); typeof(Log).GetProperties(); new Log();
Thanks to this sequence of actions, the list of methods of this type is first filled with accessors of events, then properties. And then, after creating the object, it is filled with everything else. After that, it was easy to reproduce the problem on a small test sample.
Framework Differences
Why is the problem manifested only on those machines where there was 4 framework, and not on others, despite the fact that the project aims at 4 framework? As it turned out, in 4.5 framework, the version of Microsoft.CSharp.dll differs from the version in 4 framework, albeit slightly: In 4 framework it is version number
4.0.30319.1 , and in 4.5 it is version
4.0.30319.17929 , in which, apparently, they managed to fix it some mistakes.
If you look at the code of the problem method, it changed quite a bit, it was:
MethodInfo[] methods = type.GetMethods(BindingFlags.Instance ...); for (int i = 0; i < methods.Length; i++) { if (methods[i].MetadataToken == methodInfo.MetadataToken) ...
has become:
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)) ...
So, with this double denial, this bug was fixed in the 4.5 framework.
Consequences of a mistake
As it turned out empirically, the consequences of such an error can be different, because the accessors have not only properties, but also events. And some accessories have options. And the accessors of the indexers - the parameters can generally have a different number.
It turns out that errors of the following type may occur if the RuntimeBinder chooses the wrong method:
- if a method with a smaller number of parameters is selected, an error IndexOutOfRangeException appears
- if a method with the same number of parameters is chosen, the types of which completely coincide with the expected ones, then the wrong method will simply be called, and if the method returns something, then the result of the wrong method will be returned.
- if a method with a large number of parameters is selected, and the types of the required number of parameters are fully consistent with those expected, the wrong method will be called and an error ArgumentException will occur:
How to live with it?
At connect.microsoft.com, this
problem was registered, and, judging from what was written, there are no corrections for 4 frameworks and there will not be. Most likely, the majority of this problem may never arise because to make it happen you need a big set of circumstances.
But, if the problem still occurred, the answer to this question is ambiguous and there may be several approaches. For example, initially go over all methods of all types of all assemblies, etc., so that they are listed in the correct order, although this is rather strange.
For ourselves, we have decided, for the time being, to make a small utility in the form of an
MSBuild task , which we added to the build process. This utility analyzes assemblies using
Mono.Cecil to be able to conveniently view method instructions. In the course of the analysis, the utility searches for certain sequence of instructions, examining their operands, gets the type and name of the method that the RuntimeBinder will look for and checks whether the described problem can occur. If such a problem call is found, then an error will appear during the project build.
In other words, to avoid mistakes in general, you can only install the version of the framework 4.5 to users. If this is not possible (as in our case), you will either have to not use dynamic, or use it with caution.
Test caseTo see the error, this code must be run on a computer with the .NET Framework version 4.0 installed.
For contrast, on computers with the framework above, everything will work as it should.
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); } }