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

Фильтры исключений в CLR

Anna | 17.06.2014 | нет комментариев
Привет, програлюди. Сегодня мы разглядим один из механизмов 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"] <- "bar"
        printfn "Throwing"
        raise ex
    with // дальше фильтр
    | :?  Exception as ex when filter(ex) -> printfn "Caught"

[<EntryPoint>]
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)
{
	return exception.Data["foo"].Equals("bar");
}

А сейчас испробуем обнаружить разницу в поведении между примерами на C# и VB.NET. Всё довольно легко: выражение throw; в C# теряет номер строки в стеке. Если изменить фильтр так, Дабы он возвращал false, то приложение упадёт с сообщением

Unhandled Exception: System.Exception: Exception of type 'System.Exception' was thrown.
   at CSharpFilter.Program.FilterException() in CSharpFilterProgram.cs:line 25
   at CSharpFilter.Program.Main(String[] args) in CSharpFilterProgram.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.

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB