One of the biggest benefits to programming in .NET is that you don't have to know the Win32 API set and MFC like in the "old days". Writing managed code is considerably simpler than writing C or C++ code using the Win32 APIs.
While those statements are true, they are only true to a certain point. There are still things in the managed world that you can't do. It may not happen often, but at some point you will need to write a managed type that needs to use unmanaged resources in order tow work. Fortunately for us, .NET provides the P/Invoke layer. P/Invoke is a shorthand notation for "Platform Invoke", where the "platform" is the unmanaged Win32 API set.
In reality, you are using the Win32 APIs a lot more than you may realize. A large number of the Framework BCL (Base Class Library) classes actually do this on your behalf, without exposing the unmanaged resource to you. This approach hides the complexity of using the unmanaged resource from you and, more importantly, it makes sure that the resource is freed properly when it is no longer needed.
The majority of these unmanaged types are handles. A handle is essentially a pointer to a portion of memory.
In the earlier versions of the .NET Framework (versions 1.0 and 1.1), all operating system handles could be encapsulated only by an
IntPtr object. This provided a very convenient, managed way to interoperate with native (unmanaged) code; however, it didn't provide any safety or reliability measures. Due to the lack of these features,
IntPtr allowed handles to be leaked, particularly by asynchronous exceptions. These exceptions present a huge hurdle for the garbage collector (GC) being able to clean up those resources. It was possible, at times, that an object would be reclaimed by garbage collection while it was still executing a method inside a platform invoke call. Releasing the handle passed to the platform invoke call would lead to handle corruption.
More importantly, because handles are central to the way the operating system works, Windows is very aggressive about reclaiming and recycling handles. As a result, a handle could be recycled to point to another resource that could contain sensitive data. This is known as a recycle attack and can potentially corrupt data and be a security threat.
As part of Microsoft's continuing effort to improve the .NET Framework, they recognized these problems and set out to correct them. The end result of this effort is
ContrainedExecutionRegion (CER), both introduced in version 2.0 of the Framework.
In managed code today,
SafeHandle is the best way to represent handles. The
SafeHandle class represents a managed wrapper for operating system handles and was designed to address the reliability and security issues of using handles. The goal of
SafeHandle is to guarantee that managed code will not leak a handle.
In order to do that,
SafeHandle has a finalizer on it to ensure that the handle is closed. Since
SafeHandle inherits from
CriticalFinalizerObject, its finalizer is guaranteed to run without being aborted and that it will run after the finalizers of other objects.
There is an added benefit of implementing a finalizer for
SafeHandle as well. Most classes that previously used
IntPtr to wrap handles no longer need to provide finalizers themselves. This helps reduce the number of errors and leaks due to incorrectly written Dispose patterns and helps reduce the number of collectable objects alive while waiting for your finalizer to run, which can help the GC reclaim memory much faster.
SafeHandle is also integrated with platform invoke, which automatically increments an internal reference counter every time the underlying handle is passed to a platform invoke call and decrements it when the call completes. This prevents the handle from being released while there are still pending calls.
Deriving from SafeHandle
SafeHandle is an abstract class, which means that you can't use it directly. This forces you to create a derived class that specifically represents the handle you want to encapsulate. By using these derived classes in the platform invoke definitions (prototypes), you get type safety for your handles.
To create a
SafeHandle derived class, you must know how to create and free the handle you want to encapsulate. This is different for different handles because some use
CloseHandle while others use more specific methods, such as
UnmapViewOfFile. You must also override the
ReleaseHandle methods. To be safe, the default constructor should call the base constructor and provide a value that represents an invalid handle and a Boolean value that indicates if the native handle will be owned by the
As you can see, creating a
SafeHandle derived class is not for the faint-of-heart. The Framework provides a set of pre-written abstract classes that derive from
SafeHandle to provide some common functionality for file and operating system handles that can be used to help create your own
SafeHandle derived classes. They are contained in the
Microsoft.Win32.SafeHandles namespace exposes the following classes:
- Win32 critical handle, where -1 indicates an invalid handle.
- Win32 critical handle, where either 0 or -1 indicates an invalid handle.
- A file handle wrapper. This class cannot be inherited.
- Win32 safe handle, where -1 indicates an invalid handle.
- Win32 safe handle, where either 0 or -1 indicates an invalid handle.
- A wait handle wrapper. This class cannot be inherited
The MSDN documentation[^] on the
SafeHandle class provides a very complete example for creating a custom safe handle for an operating system file handle that derives from
Constrained Execution Region
A Constrained Execution Region (CER) provides guarantees that the region of code will execute without interruption even when exceptions, such as
StackOverflowException, occur. CERs exist to help write code to maintain consistency, but they make no guarantees that the code is correct. The only thing that a CER does guarantee is that the code inside the region will run without interruption from an exception.
In order to make this guarantee, the CLR will JIT compile any code inside the CER ahead of time, prior to the first instruction being executed. This allows any exceptions that would be thrown during execution are either encountered before the code runs or after it has completed. In addition, there are restrictions placed on what code can execute in a CER. These two aspects of a CER provide a way of making strong guarantees about whether the code will execute.
Restrictions Inside a CER
When implementing a CER, the developer must follow certain strict rules:
- Arbitrary virtual methods (unless they have been eagerly prepared) cannot be called
- Memory cannot be allocated
- Only methods with suitably strong reliability contracts should be called
The restrictions on not allocating memory are, perhaps, the most restrictive. The CLR allocates memory whenever it access a multidimensional array (but not a jagged array), when it locks a section of code, or anytime the compiler has added a boxing instruction. In addition, certain platform invoke definitions require allocations.
CER's are exposed in three ways by the runtime:
- A stack-overflow safe from of a
try/finally block, called
- A call to
RuntimeHelpers.PrepareConstrainedRegions that is immediately followed by a
try/finally. In this case, the
try block is not actually constrained, but all
finally blocks are.
- Any subclass of
CriticalFinalizerObject, which have a finalizer that is eagerly prepared.
Reliability contracts describe if the method is expected to succeed and the level of consistency guaranteed when that method is called from within a CER. Reliability contracts are important when designing methods that may be called from within a CER. If your method validates input and throws an exception, you need to inform the caller that this method may fail when called from within a CER.
Brian Grunkemeyer[^] provides an example taken from a ReliableArrayList prototype he worked on showing an example of editing multiple fields of a data structure in an atomic fashion.
While constrained execution regions and
SafeHandle provide a great framework for cleaning up unmanaged resources, there is no "silver bullet" for reliability problems. You need a CER any place you edit process-wide state or you allocate unmanaged handles without using
SafeHandle to guarantee the cleanup.
The restrictions enforced by the runtime on what can be called from within a CER and the potential performance impact they can cause definitely make using a CER an advanced programming feature, and, hopefully, you won't ever need this level of reliability in your own code and can simply rely on the runtime providing it for you.
References and Further Reading
For further information, you can check out the following references:
- Revised the section on Constrained Execution Regions.
- Replaced the example code with references to the MSDN article.