Jeffrey Richter: Excerpt #5 from CLR via C#, Third Edition

9780735627048fJeffrey Richter here again. Today I’d like to share one more section from my latest book. It’s from Chapter 20, “Exceptions and State Management.” You can search this blog for more excerpts from the book. Thanks for your interest!

Constrained Execution Regions (CERs)

Many applications don’t need to be robust and recover from any and all kinds of failures.
This is true of many client applications like Notepad.exe and Calc.exe. And, of course, many
of us have seen Microsoft Office applications like WinWord.exe, Excel.exe, and Outlook.exe
terminate due to unhandled exceptions. Also, many server-side applications, like Web servers,
are stateless and are automatically restarted if they fail due to an unhandled exception. Of
course some servers, like SQL Server, are all about state management and having data lost
due to an unhandled exception is potentially much more disastrous.

In the CLR, we have AppDomains (discussed in Chapter 22), which contain state. When an
AppDomain is unloaded, all its state is unloaded. And so, if a thread in an AppDomain 
experiences an unhandled exception, it is OK to unload the AppDomain (which destroys all
its state) without terminating the whole process.8

8 This is definitely true if the thread lives its whole life inside a single AppDomain (like in the ASP.NET and managed
SQL Server stored procedure scenarios). But you might have to terminate the whole process if a thread crosses
AppDomain boundaries during its lifetime.

By definition, a CER is a block of code that must be resilient to failure. Since AppDomains can
be unloaded, destroying their state, CERs are typically used to manipulate any state that is
shared by multiple AppDomains or processes. CERs are useful when trying to maintain state
in the face of exceptions that get thrown unexpectedly. Sometimes we refer to these kinds of
exceptions as asynchronous exceptions. For example, when calling a method, the CLR has to
load an assembly, create a type object in the AppDomain’s loader heap, call the type’s static
constructor, JIT IL into native code, and so on. Any of these operations could fail, and the CLR  
reports the failure by throwing an exception.

If any of these operations fail within a catch or finally block, then your error recovery or
cleanup code won’t execute in its entirety. Here is an example of code that exhibits the
potential problem:

private static void Demo1() {
try {
Console.WriteLine("In try");
}
finally {
// Type1’s static constructor is implicitly called in here
Type1.M();
}
}

private sealed class Type1 {
static Type1() {
// if this throws an exception, M won’t get called
Console.WriteLine("Type1's static ctor called");
}

public static void M() { }
}

When I run the code above, I get the following output:

In try
Type1's static ctor called

What we want is to not even start executing the code in the try block above unless we know
that the code in the associated catch and finally blocks is guaranteed (or as close as we
can get to guaranteed) to execute. We can accomplish this by modifying the code as follows:

private static void Demo2() {
// Force the code in the finally to be eagerly prepared
RuntimeHelpers.PrepareConstrainedRegions(); // System.Runtime.CompilerServices namespace
try {
Console.WriteLine("In try");
}
finally {
// Type2’s static constructor is implicitly called in here
Type2.M();
}
}

public class Type2 {
static Type2() {
Console.WriteLine("Type2's static ctor called");
}

// Use this attribute defined in the System.Runtime.ConstrainedExecution namespace
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public static void M() { }
}

Now, when I run this version of the code, I get the following output:

Type2's static ctor called
In try

The PrepareConstrainedRegions method is a very special method. When the JIT compiler
sees this method being called immediately before a try block, it will eagerly compile the
code in the try’s catch and finally blocks. The JIT compiler will load any assemblies, create
any type objects, invoke any static constructors, and JIT any methods. If any of these opera-
tions result in an exception, then the exception occurs before the thread enters the try block.

When the JIT compiler eagerly prepares methods, it also walks the entire call graph eagerly 
preparing called methods. However, the JIT compiler only prepares methods that have
the ReliabilityContractAttribute applied to them with either Consistency.
WillNotCorruptState or Consistency.MayCorruptInstance because the CLR can’t 
make any guarantees about methods that might corrupt AppDomain or process 
state. Inside a catch or finally block that you are protecting with a call to
PrepareConstrainedRegions, you want to make sure that you only call methods with 
the ReliabillityContractAttribute set as I’ve just described.

The ReliabilityContractAttribute looks like this:

public sealed class ReliabilityContractAttribute : Attribute {
public ReliabilityContractAttribute(Consistency consistencyGuarantee, Cer cer);
public Cer Cer { get; }
public Consistency ConsistencyGuarantee { get; }
}

This attribute lets a developer document the reliability contract of a particular method9 to
the method’s potential callers. Both the Cer and Consistency types are enumerated types
defined as follows:

enum Consistency {
MayCorruptProcess, MayCorruptAppDomain, MayCorruptInstance, WillNotCorruptState
}

enum Cer { None, MayFail, Success }

9 You can also apply this attribute to an interface, a constructor, a structure, a class, or an assembly to affect the
members inside it.

If the method you are writing promises not to corrupt any state, use Consistency.
WillNotCorruptState
. Otherwise, document what your method does by using one of the
other three possible values that match whatever state your method might corrupt. If the
method that you are writing promises not to fail, use Cer.Success. Otherwise, use Cer.
MayFail
. Any method that does not have the ReliabilityContractAttribute applied to it
is equivalent to being marked like this:

[ReliabilityContract(Consistency.MayCorruptProcess, Cer.None)]

The Cer.None value indicates that the method makes no CER guarantees. In other words, it
wasn’t written with CERs in mind; therefore, it may fail and it may or may not report that it
failed. Remember that most of these settings are giving a method a way to document what it
offers to potential callers so that they know what to expect. The CLR and JIT compiler do not
use this information.

When you want to write a reliable method, make it small and constrain what it does. Make
sure that it doesn’t allocate any objects (no boxing, for example), don’t call any virtual meth-
ods or interface methods, use any delegates, or use reflection because the JIT compiler can’t
tell what method will actually be called. However, you can manually prepare these methods
by calling one of these methods defined by the RuntimeHelpers’s class:

public static void PrepareMethod(RuntimeMethodHandle method)
public static void PrepareMethod(RuntimeMethodHandle method,
RuntimeTypeHandle[] instantiation)
public static void PrepareDelegate(Delegate d);
public static void PrepareContractedDelegate(Delegate d);

Note that the compiler and the CLR do nothing to verify that you’ve written your method to
actually live up to the guarantees you document via the ReliabiltyContractAttribute. If
you do something wrong, then state corruption is possible.

Note  Even if all the methods are eagerly prepared, a method call could still result in a
StackOverflowException. When the CLR is not being hosted, a StackOverflowException
causes the process to terminate immediately by the CLR internally calling Environment.
FailFast
. When hosted, the PreparedConstrainedRegions method checks the stack to
see if there is approximately 48KB of stack space remaining. If there is limited stack space, the
StackOverflowException occurs before entering the try block.

You should also look at RuntimeHelper’s ExecuteCodeWithGuaranteedCleanup method
which is another way to execute code with guaranteed cleanup:

public static void ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode,
Object userData);

When calling this method, you pass the body of the try and finally block as callback
methods whose prototypes match these two delegates respectively:

public delegate void TryCode(Object userData);
public delegate void CleanupCode(Object userData, Boolean exceptionThrown);

And finally, another way to get guaranteed code execution is to use the
CriticalFinalizerObject class which is explained in great detail in Chapter 21.