Написание фильтра исключений
Исключение можно обработать посредством перехода на уровень обработчика исключений или путем продолжения выполнения. Вместо использования кода обработчика исключений для обработки исключения и падения можно использовать выражение фильтра для очистки проблемы. Затем, возвращая EXCEPTION_CONTINUE_EXECUTION (-1), вы можете возобновить обычный поток без очистки стека.
После некоторых исключений возобновление невозможно. Если фильтр оценивается как -1 для такого исключения, система вызывает новое исключение. При вызове RaiseException определите, будет ли продолжаться исключение.
Например, следующий код использует вызов функции в выражении фильтра : эта функция обрабатывает проблему, а затем возвращает -1 для возобновления нормального потока управления:
// exceptions_Writing_an_Exception_Filter.cpp #include int main() < int Eval_Exception( int ); __try <>__except ( Eval_Exception( GetExceptionCode( ))) < ; >> void ResetVars( int ) <> int Eval_Exception ( int n_except ) < if ( n_except != STATUS_INTEGER_OVERFLOW && n_except != STATUS_FLOAT_OVERFLOW ) // Pass on most exceptions return EXCEPTION_CONTINUE_SEARCH; // Execute some code to clean up problem ResetVars( 0 ); // initializes data to 0 return EXCEPTION_CONTINUE_EXECUTION; >
Рекомендуется использовать вызов функции в выражении фильтра всякий раз, когда фильтру нужно сделать что-либо сложное. Вычисление выражения приводит к выполнению функции, в данном случае — Eval_Exception .
Обратите внимание на использование GetExceptionCode для определения исключения. Эта функция должна вызываться внутри выражения фильтра инструкции __except . Eval_Exception не может вызываться GetExceptionCode , но он должен иметь код исключения, переданный в него.
Если исключение не вызвано переполнением при операции с целыми числами или числами с плавающей запятой, этот обработчик передает управление другому обработчику. В этом случае обработчик вызывает функцию ( ResetVars — это только пример, а не функция API), чтобы сбросить некоторые глобальные переменные. Блок инструкций __except , который в этом примере пуст, никогда не может выполняться, так как Eval_Exception никогда не возвращает EXCEPTION_EXECUTE_HANDLER (1).
Вызов функции — хороший способ работы со сложными выражениями фильтров. Удобны также две другие возможности языка C:
- условный оператор;
- оператор «запятая».
Условный оператор часто используется здесь. Его можно использовать для проверка для определенного кода возврата, а затем возвращать одно из двух разных значений. Например, фильтр в следующем коде распознает исключение только в том случае, если исключение: STATUS_INTEGER_OVERFLOW
__except( GetExceptionCode() == STATUS_INTEGER_OVERFLOW ? 1 : 0 )
Цель условного оператора в этом случае — в основном, обеспечить ясность, так как следующий код дает такие же результаты.
__except( GetExceptionCode() == STATUS_INTEGER_OVERFLOW )
Условный оператор более полезен в ситуациях, когда фильтр может иметь значение -1. EXCEPTION_CONTINUE_EXECUTION
Оператор запятых позволяет выполнять несколько выражений в последовательности. Затем он возвращает значение последнего выражения. Например, в следующем коде код исключения сохраняется в переменной и затем проверяется.
__except( nCode = GetExceptionCode(), nCode == STATUS_INTEGER_OVERFLOW )
С чего начинается фильтр исключений в конструкции
Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core
Последнее обновление: 31.10.2015
Фильтры исключений срабатывают, если при выполнении метода действия будет выброшено необработанное исключение.
С одной стороны, мы могли поместить всю логику выполнения метода в блок try. catch и отследить исключение. Однако область работы фильтров исключения несколько шире. Они позволяют отследить не только исключения, возникающие в самом методе, но исключения, генерируемые результатами действий, а также другими применяемыми к данному действию фильтрами. В этом и состоит мощь данного типа фильтров.
Все фильтры исключений должны применять интерфейс IExceptionFilter :
public interface IExceptionFilter
И если вдруг приложение выбрасывает необрабатываемое исключение, то фильтр вызывает метод OnException .
Передаваемый в этот метод параметр - ExceptionContext является объектом, производным от ControllerContext . Поэтому из него можно извлечь как специфичную для фильтра информацию, так и общую информацию о запросе.
В частности класс ExceptionContext обладает следующими свойствами, которые позволяют получить информацию об исключении:
Содержит информацию о методе действия, на котором было выброшено исключение
Представляет само необработанное исключение
Значение, показывающее, считается ли исключение обработанным. Если мы на фильтре помечаем его значение в true, то исключение считается обработанным
Результат метода действия, к которому применяется фильтр исключения
С помощью свойства Exception мы можем получить доступ к выбрасываемому исключению.
Установив свойство ExceptionHandled в true, фильтр тем самым помечает исключение как обработанное.
С помощью свойства Result фильтр управляет результатом действий. Общераспространенной практикой в данном случае является перенаправление пользователя на страницу ошибки или отображение ошибки на экране.
Теперь создадим простенький фильтр, который будет обрабатывать исключение IndexOutOfRangeException :
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace Filters.Filters < public class IndexException : FilterAttribute, IExceptionFilter < public void OnException(ExceptionContext exceptionContext) < if (!exceptionContext.ExceptionHandled && exceptionContext.Exception is IndexOutOfRangeException) < exceptionContext.Result = new RedirectResult("/Content/ExceptionFound.html"); exceptionContext.ExceptionHandled = true; >> > >
Здесь в методе OnException первым делом мы проверяем, не установлено ли значение свойства ExceptionHandled . Если оно установлено в true, следовательно, какой-то другой фильтр исключений уже обработал данное исключение. Также проверяется тип исключения. Поскольку мы ловим в данном случае только исключения типа IndexOutOfRangeException , следовательно, нас только они интересуют.
Далее мы устанавливаем результат метода, к которому применен фильтр. Предполагается, что в проекте в каталоге Content у нас находится некоторая страница ExceptionFound.html, которая отображает пользователю сообщение об ошибке.
В данном случае важно пометить исключение как обработанное: exceptionContext.ExceptionHandled = true . Иначе, если мы этого не сделаем, то мы можем увидеть в браузере все диагностическое сообщение об ошибке, которое обычно посылает фреймворк в ответ клиенту.
[IndexException] public ActionResult Index()
В данном случае метод Index выбросит необработанное исключение, и оно будет объектом типа IndexOutOfRangeException , а пользователь будет перенаправлен на страницу ExceptionFound.htm .
Подобным образом мы можем обработать и другие типы исключений.
HandleErrorAttribute. Встроенная обработка исключений.
Создавать свои фильтры исключений необязательно, так как во фреймворке имеется встроенная реализация интерфейса IExceptionFilter - атрибут HandleErrorAttribute. Он имеет ряд свойств, с помощью которых мы можем произвести гибкую настройку фильтра:
Представляет тип обрабатываемого исключения. По умолчанию используется System.Exception
Имя представления, которое рендерится данным фильтром. Если значение не задано, то по умолчанию используются следующие пути: /Views/Имя_контроллера/Error.cshtml или /Views/Shared/Error.cshtml
Имя используемой мастер-страницы
При обработке исключения фильтр исключений посылает статусный код HTTP 500 и генерирует указанное в свойстве View представление. Например, используем предыдущий пример с фильтром исключений, применив встроенную реализацию:
[HandleError(ExceptionType = typeof(System.IndexOutOfRangeException), View = "ExceptionFound")] public ActionResult About()
В данном случае очевидно, что на строке mas[6] = 4; будет выброшено исключение. В режиме отладки у вас приостановится выполнение программы, тогда вы можете нажать на кнопку Continue на панели инструментов. Здесь опять мы обрабатываем исключение типа IndexOutOfRangeException, и при возникновении такового генерируем в ответ представление ExceptionFound.cshtml . Данное представление должно находиться в проекте в каталоге Views/Имя_контроллера/ .
Сразу надо сказать, что если вы хотите при разработке видеть обрабатываемые фильтром HandleErrorAttribute, то надо включить данную функциональность в файле конфигурации web.config с помощью тега :
С чего начинается фильтр исключений в конструкции
За обработку исключения отвечает блок catch , который может иметь следующие формы:
catch < // выполняемые инструкции >
catch (тип_исключения) < // выполняемые инструкции >
Обрабатывает только те исключения, которые соответствуют типу, указаному в скобках после оператора catch. Например, обработаем только исключения типа DivideByZeroException:
try < int x = 5; int y = x / 0; Console.WriteLine($"Результат: "); > catch(DivideByZeroException)
catch (тип_исключения имя_переменной) < // выполняемые инструкции >
Обрабатывает только те исключения, которые соответствуют типу, указаному в скобках после оператора catch. А вся информация об исключении помещается в переменную данного типа. Например:
try < int x = 5; int y = x / 0; Console.WriteLine($"Результат: "); > catch(DivideByZeroException ex) < Console.WriteLine($"Возникло исключение "); >
Фильтры исключений
Фильтры исключений позволяют обрабатывать исключения в зависимости от определенных условий. Для их применения после выражения catch идет выражение when , после которого в скобках указывается условие:
catch when(условие)
В этом случае обработка исключения в блоке catch производится только в том случае, если условие в выражении when истинно. Например:
int x = 1; int y = 0; try < int result1 = x / y; int result2 = y / x; >catch (DivideByZeroException) when (y == 0) < Console.WriteLine("y не должен быть равен 0"); >catch(DivideByZeroException ex)
В данном случае будет выброшено исключение, так как y=0. Здесь два блока catch, и оба они обрабатывают исключения типа DivideByZeroException, то есть по сути все исключения, генерируемые при делении на ноль. Но поскольку для первого блока указано условие y == 0 , то именно этот блок будет обрабатывать данное исключение - условие, указанное после оператора when возвращает true.
int x = 0; int y = 1; try < int result1 = x / y; int result2 = y / x; >catch (DivideByZeroException) when (y == 0) < Console.WriteLine("y не должен быть равен 0"); >catch(DivideByZeroException ex)
В данном случае будет выброшено исключение, так как x=0. Условие первого блока catch - y == 0 теперь возвращает false. Поэтому CLR будет дальше искать соответствующие блоки catch далее и для обработки исключения выберет второй блок catch. В итоге если мы уберем второй блок catch, то исключение вообще не будет обрабатываться.
Фильтры исключений в CLR
Привет, хабралюди. Сегодня мы рассмотрим один из механизмов CLR, который напрямую недоступен для разработчиков на языке C# — фильтры исключений.
Опрос среди моих знакомых программистов на C# показал, что они (само собой) никогда этим механизмом не пользовались и даже не знают о его существовании. Поэтому предлагаю всем любознательным ознакомиться с текстом статьи.
Итак, фильтры исключений — это механизм, который позволяет блоку catch декларировать предусловия, которым должно удовлетворять исключение, дабы быть пойманным данным блоком. Этот механизм работает не совсем так же, как выполнение проверок внутри блока catch .
Под катом — код на VB.NET, F#, CIL и C#, а также проверка различных декомпиляторов на обработку механизма фильтров.
Откуда есть пошли фильтры исключений
Фильтры исключений встроены в среду CLR и являются одним из механизмов, который среда использует при обработке исключения. Последовательность выглядит следующим образом:
На этапе поиска подходящего блока catch CLR выполняет обход своего внутреннего стека обработчиков исключений, а также выполняет фильтры исключений. Обратите внимание — это происходит до выполнения кода в блоке finally . Мы обсудим этот момент позже.
Как это выглядит на VB.NET
Фильтры исключений нативно поддерживаются языком VB.NET. Вот пример того, как выглядит код, использующий фильтры:
Sub FilterException() Try Dim exception As New Exception exception.Data.Add("foo", "bar1") Console.WriteLine("Throwing") Throw exception Catch ex As Exception When Filter(ex) ' здесь фильтр Console.WriteLine("Caught") Finally Console.WriteLine("Finally") End Try End Sub Function Filter(exception As Exception) As Boolean Console.WriteLine("Filtering") Return exception.Data.Item("foo").Equals("bar") End Function
При выполнении данного кода будет выдана следующая цепочка сообщений:
Throwing Filtering Caught Finally
Как это выглядит в F#
При подготовке статьи я нашёл в интернете информацию о том, что F# поддерживает фильтры исключений. Что ж, проверим это. Вот пример кода:
Код на F#
open System let filter (ex : Exception) = printfn "Filtering" ex.Data.["foo"] :?> string = "bar" let filterException() = try let ex = Exception() ex.Data.["foo"] printfn "Caught" [] let main argv = filterException() 0
Этот код компилируется без фильтров, с обычным catch [mscorlib]System.Object . Мне так и не удалось заставить компилятор F# сделать фильтр исключений. Если вам известны альтернативные способы это сделать — добро пожаловать в комментарии.
Как это выглядит в CIL
CIL (Common Intermediate Language) — это аналог низкоуровневого языка ассемблера для .NET-машины. Скомпилированные сборки можно дизассемблировать в этот язык с помощью инструмента ildasm , и собирать обратно с помощью ilasm , которые поставляются вместе с .NET.
Приведу фрагмент кода на VB.NET, каким я его увидел в ildasm :
Много кода на CIL
.method public static void FilterException() cil managed < // Code size 110 (0x6e) .maxstack 3 .locals init ([0] class [mscorlib]System.Exception exception, [1] class [mscorlib]System.Exception ex) IL_0000: nop IL_0001: nop .try < .try < IL_0002: newobj instance void [mscorlib]System.Exception. ctor() IL_0007: stloc.0 IL_0008: ldloc.0 IL_0009: callvirt instance class [mscorlib]System.Collections.IDictionary [mscorlib]System.Exception::get_Data() IL_000e: ldstr "foo" IL_0013: ldstr "bar" IL_0018: callvirt instance void [mscorlib]System.Collections.IDictionary::Add(object, object) IL_001d: nop IL_001e: ldstr "Throwing" IL_0023: call void [mscorlib]System.Console::WriteLine(string) IL_0028: nop IL_0029: ldloc.0 IL_002a: throw IL_002b: leave.s IL_006b >// end .try filter < IL_002d: isinst [mscorlib]System.Exception IL_0032: dup IL_0033: brtrue.s IL_0039 IL_0035: pop IL_0036: ldc.i4.0 IL_0037: br.s IL_0049 IL_0039: dup IL_003a: stloc.1 IL_003b: call void [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.ProjectData::SetProjectError(class [mscorlib]System.Exception) IL_0040: ldloc.1 IL_0041: call bool FilterSamples.VbNetFilter::Filter(class [mscorlib]System.Exception) IL_0046: ldc.i4.0 IL_0047: cgt.un IL_0049: endfilter >// end filter < // handler IL_004b: pop IL_004c: ldstr "Caught" IL_0051: call void [mscorlib]System.Console::WriteLine(string) IL_0056: nop IL_0057: call void [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.ProjectData::ClearProjectError() IL_005c: leave.s IL_006b >// end handler > // end .try finally < IL_005e: nop IL_005f: ldstr "Finally" IL_0064: call void [mscorlib]System.Console::WriteLine(string) IL_0069: nop IL_006a: endfinally >// end handler IL_006b: nop IL_006c: nop IL_006d: ret > // end of method VbNetFilter::FilterException
Как видно, компилятор VB.NET, конечно, сильно расписал наш код в виде CIL. Больше всего нас интересует блок filter :
filter < // Проверяем, что брошенный объект является экземпляром System.Exception: IL_002d: isinst [mscorlib]System.Exception IL_0032: dup IL_0033: brtrue.s IL_0039 IL_0035: pop IL_0036: ldc.i4.0 // Если нет - то выходим: IL_0037: br.s IL_0049 IL_0039: dup // Тут какой-то служебный вызов: IL_003a: stloc.1 IL_003b: call void [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.ProjectData::SetProjectError(class [mscorlib]System.Exception) // Вызываем функцию, которую мы определили как фильтр: IL_0040: ldloc.1 IL_0041: call bool FilterSamples.VbNetFilter::Filter(class [mscorlib]System.Exception) IL_0046: ldc.i4.0 IL_0047: cgt.un IL_0049: endfilter >// end filter
Итак, компилятор вынес в блок фильтра проверку типа исключения, а также вызов нашей функции. Если в конце выполнения блока фильтра на стеке лежит значение 1 , то соответствующий этому фильтру блок catch будет выполнен; иначе — нет.
Стоит отметить, что компилятор C# проверки типов не выносит в блок filter , а использует специальную CIL-конструкцию catch с указанием типа. То есть, компилятор C# не использует механизм filter вообще.
Кстати говоря, для генерации этого блока можно использовать метод ILGenerator.BeginExceptFilterBlock (если вы пишете свой компилятор).
Как это выглядит в декомпиляторе
В этом разделе я попробую декомпилировать полученный код с помощью нескольких известных инструментов и посмотреть, что из этого получится.
Последний JetBrains dotPeek 1.1 при попытке декомпиляции сборки с фильтром радостно сообщил следующее:
public static void FilterException() < // ISSUE: unable to decompile the method. >
.NET Reflector 8.2 поступил более адекватно и что-то смог декомпилировать в C#:
public static void FilterException() < try < Exception exception = new Exception(); exception.Data.Add("foo", "bar"); Console.WriteLine("Throwing"); throw exception; >catch when (?) < Console.WriteLine("Caught"); ProjectData.ClearProjectError(); >finally < Console.WriteLine("Finally"); >>
Что ж, неплохо — хотя код и некомпилируемый, но по нему хотя бы видно наличие фильтра. То, что фильтр не был расшифрован, можно списать на недостатки C#-транслятора. Попробуем то же самое с транслятором в VB.NET:
Public Shared Sub FilterException() Try Dim exception As New Exception exception.Data.Add("foo", "bar") Console.WriteLine("Throwing") Throw exception Catch obj1 As Object When (?) Console.WriteLine("Caught") ProjectData.ClearProjectError Finally Console.WriteLine("Finally") End Try End Sub
Увы, попытка точно так же провалилась — декомпилятор почему-то не смог определить имя фильтрующей функции (хотя, как мы видели выше, ildasm с этим прекрасно справился).
Могу только предположить, что рассмотренные инструменты пока плохо работают с кодом фильтров .NET4.5.
Чем это отличается от проверок в теле блока catch
Рассмотрим фрагмент кода, почти аналогичный коду на VB.NET:
Код на C#
static void FilterException() < try < var exception = new Exception(); exception.Data["foo"] = "bar"; Console.WriteLine("Throwing"); throw exception; >catch (Exception exception) < if (!Filter(exception)) < throw; >Console.WriteLine("Caught"); > > static bool Filter(Exception exception)
А теперь попробуем найти разницу в поведении между примерами на C# и VB.NET. Всё достаточно просто: выражение throw; в C# теряет номер строки в стеке. Если изменить фильтр так, чтобы он возвращал false , то приложение упадёт с сообщением
Unhandled Exception: System.Exception: Exception of type 'System.Exception' was thrown. at CSharpFilter.Program.FilterException() in CSharpFilter\Program.cs:line 25 at CSharpFilter.Program.Main(String[] args) in CSharpFilter\Program.cs:line 9
Судя по стеку, исключение было сгенерировано на 25 строке (строка throw; ), а не на строке 19 ( throw exception; ). Код на VB.NET в таких же условиях показывает изначальное место выпадения исключения.
UPD. Изначально я ошибочно написал, что throw; теряет весь стек, но в комментариях подсказали, что это действительно совсем не так. Происходит лишь незначительная модификация номера строки в стеке. Причём на mono это не воспроизводится — стек исключения там не меняется после throw; (спасибо kekekeks за эти подробности).
О безопасности
Эрик Липперт в своём блоге рассматривает ситуацию, когда фильтры исключений позволяют вредоносной стороне выполнить свой код с повышенными привилегиями в некоторых случаях.
Коротко: если вы выполняете временное повышение привилегий для какого-то внешнего и потенциально разрушительного кода, то нельзя полагаться на finally , т.к. перед выполненнием блока finally могут быть вызваны фильтры исключений, расположенные выше по стеку вызовов (а злоумышленник может вытворять в коде этих фильтров всё, что ему вздумается). Помните — поиск подходящего блока catch всегда выполняется до выполнения блока finally .
Заключение
Сегодня мы рассмотрели один из редко встречаемых программистами на C# механизмов среды CLR. Сам я не пишу на VB.NET, но считаю, что эта информация может быть полезна всем разработчикам .NET-платформы. Ну а если вы занимаетесь разработкой языков, компиляторов или декомпиляторов для этой платформы, то вам тем более эта информация пригодится.
PS. Код, картинки и текст статьи выложены на github: github.com/ForNeVeR/ClrExceptionFilters.
- обработка исключений
- clr internals
