Searching for Exceptions in .NET
Posted in Programming, Software on June 22nd, 2009 by Noldorin – Be the first to commentI recently came across a rather interesting question on StackOverflow that posed the problem:
How can one discover the set of all the exceptions that a given method might throw under arbitrary circumstances?
Of course, there is a simple (and not terribly satisfying) answer; in the great majority of situations, XML documentation for the BCL (and ideally any third-party libraries too) should provide information about any exception that might be thrown and any potential reason for it. Indeed, this is generally all one needs in order to write largely error-safe code. However, not every exception is documented in any case, and for production-quality applications, it is often desirable to insure that there is no realistic chance of an unhandled exception ocurring. For this reason, it is sometimes desirable to do a rigorous check for all exceptions. Clearly, an application-level unhandled (fatal) exception handler would do the job to some extent, and although this is always a good fallback feature to have, it is the least elegant solution to coping with exceptions.
After some consideration, it became quite apparent that the task reduces to the halting problem. However, with a few simplifications, the problem does become relatively solvable. Most importantly, complex logic that determines whether an exception will be thrown must be ignored, and one must simply assume that any throw statement within a given method could possibly cause an exception under certain conditions.
Here is the complete code for the algorithm I wrote. The GetAllExceptions method is an extension method that returns a read-only collection of exceptions, which makes it very straightforward and efficient to use.
Notably, the code detects all of
- instantiated exceptions,
- exceptions return from method/property calls,
- exceptions stored in fields (though if the method return type or field type is non-specific, i.e. a parent class of the actual exception type thrown), this is used instead.
Exceptions are only counted when the appropiate throw instruction is encountered at some level. Also, the stack and local variables are handled correctly, as far as I can tell, so this method should work soundly in pretty much all cases. (It has been tested with some a few quite complex methods within the BCL, as well as simpler user-defined ones.)
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using ClrTest.Reflection;
public static class ExceptionAnalyser
{
public static ReadOnlyCollection GetAllExceptions(this MethodBase method)
{
var exceptionTypes = new HashSet();
var visitedMethods = new HashSet();
var localVars = new Type[ushort.MaxValue];
var stack = new Stack();
GetAllExceptions(method, exceptionTypes, visitedMethods, localVars, stack, 0);
return exceptionTypes.ToList().AsReadOnly();
}
public static void GetAllExceptions(MethodBase method, HashSet exceptionTypes,
HashSet visitedMethods, Type[] localVars, Stack stack, int depth)
{
var ilReader = new ILReader(method);
var allInstructions = ilReader.ToArray();
ILInstruction instruction;
for (int i = 0; i < allInstructions.Length; i++)
{
instruction = allInstructions[i];
if (instruction is InlineMethodInstruction)
{
var methodInstruction = (InlineMethodInstruction)instruction;
if (!visitedMethods.Contains(methodInstruction.Method))
{
visitedMethods.Add(methodInstruction.Method);
GetAllExceptions(methodInstruction.Method, exceptionTypes, visitedMethods,
localVars, stack, depth + 1);
}
var curMethod = methodInstruction.Method;
if (curMethod is ConstructorInfo)
stack.Push(((ConstructorInfo)curMethod).DeclaringType);
else if (method is MethodInfo)
stack.Push(((MethodInfo)curMethod).ReturnParameter.ParameterType);
}
else if (instruction is InlineFieldInstruction)
{
var fieldInstruction = (InlineFieldInstruction)instruction;
stack.Push(fieldInstruction.Field.FieldType);
}
else if (instruction is ShortInlineBrTargetInstruction)
{
}
else if (instruction is InlineBrTargetInstruction)
{
}
else
{
switch (instruction.OpCode.Value)
{
// ld*
case 0x06:
stack.Push(localVars[0]);
break;
case 0x07:
stack.Push(localVars[1]);
break;
case 0x08:
stack.Push(localVars[2]);
break;
case 0x09:
stack.Push(localVars[3]);
break;
case 0x11:
{
var index = (ushort)allInstructions[i + 1].OpCode.Value;
stack.Push(localVars[index]);
break;
}
// st*
case 0x0A:
localVars[0] = stack.Pop();
break;
case 0x0B:
localVars[1] = stack.Pop();
break;
case 0x0C:
localVars[2] = stack.Pop();
break;
case 0x0D:
localVars[3] = stack.Pop();
break;
case 0x13:
{
var index = (ushort)allInstructions[i + 1].OpCode.Value;
localVars[index] = stack.Pop();
break;
}
// throw
case 0x7A:
if (stack.Peek() == null)
break;
exceptionTypes.Add(stack.Pop());
break;
default:
switch (instruction.OpCode.StackBehaviourPop)
{
case StackBehaviour.Pop0:
break;
case StackBehaviour.Pop1:
case StackBehaviour.Popi:
case StackBehaviour.Popref:
case StackBehaviour.Varpop:
stack.Pop();
break;
case StackBehaviour.Pop1_pop1:
case StackBehaviour.Popi_pop1:
case StackBehaviour.Popi_popi:
case StackBehaviour.Popi_popi8:
case StackBehaviour.Popi_popr4:
case StackBehaviour.Popi_popr8:
case StackBehaviour.Popref_pop1:
case StackBehaviour.Popref_popi:
stack.Pop();
stack.Pop();
break;
case StackBehaviour.Popref_popi_pop1:
case StackBehaviour.Popref_popi_popi:
case StackBehaviour.Popref_popi_popi8:
case StackBehaviour.Popref_popi_popr4:
case StackBehaviour.Popref_popi_popr8:
case StackBehaviour.Popref_popi_popref:
stack.Pop();
stack.Pop();
stack.Pop();
break;
}
switch (instruction.OpCode.StackBehaviourPush)
{
case StackBehaviour.Push0:
break;
case StackBehaviour.Push1:
case StackBehaviour.Pushi:
case StackBehaviour.Pushi8:
case StackBehaviour.Pushr4:
case StackBehaviour.Pushr8:
case StackBehaviour.Pushref:
case StackBehaviour.Varpush:
stack.Push(null);
break;
case StackBehaviour.Push1_push1:
stack.Push(null);
stack.Push(null);
break;
}
break;
}
}
}
}
}
To be quite honest, I’m not sure whether I’ll need to use this code myself at any point, but I’ve posted it regardless for the benefit of anyone who might require such rigorous exception checking. It was definitely an interesting challenge, at the least.
Any further comments or suggestions would be welcome, as always.