How are CIL 'fault' clauses different from 'catch' clauses in C#?

拈花ヽ惹草 提交于 2019-12-03 01:50:13

there are four different kinds of exception handler blocks:

  • catch clauses: "Catch all objects of the specified type."
  • filter clauses: "Enter handler only if filter succeeds."
  • finally clauses: "Handle all exceptions and normal exit."
  • fault clauses: "Handle all exceptions but not normal exit."

Given these brief explanations (cited from the CLI Standard, btw.), these should map to C# as follows:

  • catchcatch (FooException) { … }
  • filter — not available in C# (but in VB.NET as Catch FooException When booleanExpression)
  • finallyfinally { … }
  • faultcatch { … }

It's that last line where you went wrong. Read the descriptions again. fault and finally are described practically identically. The difference between them is that finally is always entered, whereas fault is only entered if control leaves the try via an exception. Note that this means that a catch block may have already acted.

If you write this in C#:

try {
    ...
} catch (SpecificException ex) {
    ...
} catch {
    ...
}

Then there is no way that the third block will be entered if control leaves the try via a SpecificException. That's why catch {} isn't a mapping for fault.

.NET exceptions piggy-back onto the operating system's support for exceptions. Called Structured Exception Handling on Windows. Unix operating systems have something similar, signals.

A managed exception is a very specific case of an SEH exception. The exception code is 0xe0434f53. The last three hex pairs spell "COM", tells you something about the way .NET got started.

A program in general might have a stake at knowing when any exception is raised and handled, not just managed exceptions. You can see this back in the MSVC C++ compiler as well. A catch(...) clause only catches C++ exceptions. But if you compile with the /EHa option then it catches any exception. Including the really nasty stuff, processor exceptions like access violations.

The fault clause is the CLR's version of that, its associated block will execute for any operating system exception, not just managed ones. The C# and VB.NET languages do not support this, they only support exception handling for managed exceptions. But other languages may, I only know of the C++/CLI compiler emitting them. Done for example in its version of the using statement, called "stack semantics".

It does make sense that C++/CLI would support that, it is after all a language that strongly supports directly calling native code from managed code. But not for C# and VB.NET, they only ever run unmanaged code through the pinvoke marshaller or the COM interop layer in the CLR. Which already sets up a 'catch-them-all' handler that translates unmanaged exceptions into managed ones. Which is the mechanism by which you get a System.AccessViolationException.

stakx

1. If catch { … } really is a catch clause, then how are fault clauses different from catch clauses?

The C# compiler (at least the one that ships with .NET) actually appears to compile catch { … } as if it were really catch (object) { … }. This can be shown with the code below.

// using System;
// using System.Linq;
// using System.Reflection;

static Type GetCaughtTypeOfCatchClauseWithoutTypeSpecification()
{
    try
    {
        return MethodBase
               .GetCurrentMethod()
               .GetMethodBody()
               .ExceptionHandlingClauses
               .Where(clause => clause.Flags == ExceptionHandlingClauseOptions.Clause)
               .Select(clause => clause.CatchType)
               .Single();
    }
    catch // <-- this is what the above code is inspecting
    {
        throw;
    }
}

That method returns typeof(object).

So conceptually, a fault handler  is  is similar to a catch { … }; however, the C# compiler never generates code for that exact construct but pretends that it is a catch (object) { … }, which is conceptually a catch clause. Thus a catch clause gets emitted.

Side note: Jeffrey Richter's book "CLR via C#" has some related information (on pp. 472–474): Namely that the CLR allows any value to be thrown, not just Exception objects. However, starting with CLR version 2, non-Exception values are automatically wrapped in a RuntimeWrappedException object. So it seems somewhat surprising that C# would transform catch into catch (object) instead of catch (Exception). There is however a reason for this: The CLR can be told not to wrap non-Exception values by applying a [assembly: RuntimeCompatibility(WrapNonExceptionThrows = false)] attribute.

By the way, the VB.NET compiler, unlike the C# compiler, translates Catch to Catch anonymousVariable As Exception.


2. Does the C# compiler ever output fault clauses at all?

It obviously doesn't emit fault clauses for catch { … }. However, Bart de Smet's blog post "Reader challenge – fault handlers in C#" suggests that the C# compiler does produce fault clauses in certain circumstances.

As people have pointed out, generally speaking the C# compiler doesn't generate fault handlers. However, stakx linked to Bart de Smet's blog post on how to get the C# compiler to generate a fault handler.

C# does use fault handlers to implement using statements that are inside iterator blocks. Eg the following C# code will cause the compiler to use a fault clause:

public IEnumerable<string> GetSomeEnumerable()
{
    using (Disposable.Empty)
    {
        yield return DoSomeWork();
    }
}

Decompiling the generated assembly with dotPeek and the "Show compiler-generated code" option on, you can see the fault clause:

bool IEnumerator.MoveNext()
{
    try
    {
        switch (this.<>1__state)
        {
        case 0:
            this.<>1__state = -1;
            this.<>7__wrap1 = Disposable.Empty;
            this.<>1__state = 1;
            this.<>2__current = this.<>4__this.DoSomeWork();
            this.<>1__state = 2;
            return true;
        case 2:
            this.<>1__state = 1;
            this.<>m__Finally2();
            break;
        }
        return false;
    }
    __fault
    {
        this.System.IDisposable.Dispose();
    }
}

Where normally a using statement would map to a try/finally block, this doesn't make sense for iterator blocks - a try/finally would Dispose after the first value is generated.

But if DoSomeWork throws an exception, you do want to Dispose. So a fault handler is useful here. It will call Dispose only in the case where an exception occurs, and allow the exception to bubble up. Conceptually this is similar to a catch block that disposes and then re-throws.

supercat

A fault block would be equivalent to saying:

bool success;
try
{
    success = false;
    ... do stuff
    success = true; // Also include this immediately before any 'return'    
}
finally
{
    if (!success)
    {
        ... do "fault" stuff here
    }
}

Note that this is somewhat different semantically from catch-and-rethrow. Among other things, with the implementation above, if an exception occurs and the stack trace is reporting line numbers, it will include the number of the line in ...do stuff where the exception occurred. By contrast, when using catch-and-rethrow, the stack trace would report the line number of the rethrow. If ...do stuff includes two or more calls to foo, and one of those calls throws an exception, knowing the line number of the call that failed could be helpful, but catch-and-rethrow would lose that information.

The biggest problems with the above implementation are that one must manually add success = true; to every place in the code that could exit the try block, and that there's no way for the finally block to know what exception may be pending. If I had my druthers, there would be a finally (Exception ex) statement which would set Ex to the exception that caused the try block to exit (or null if the block exited normally). Not only would that eliminate the need to manually set the 'success' flag, but it would allow sensible handling of the case where an exception occurs in cleanup code. In such a situation, one should not obscure the cleanup exception (even if the original exception would normally represent a condition the calling code was expecting, the cleanup failure probably represents a condition it wasn't) but one probably doesn't want to lose the original exception either (since it probably contains clues as to why the cleanup failed). Allowing a finally block to know why it was entered, and including an extended version of IDisposable via which a using block could make such information available to cleanup code, would make it possible to resolve such situations cleanly.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!