Guidelines for Writing Highly Available Managed Code


In many cases, the capability to specify an escalation policy is required primarily to protect against add-ins that weren't written explicitly with high availability in mind. For example, I've discussed how a host can cause a resource failure to be escalated to a request to unload an entire application domain if the code in question happens to be updating shared state when the resource failure occurs. Ideally, these situations would never occur in the first place, and hence, the escalation policy wouldn't be needed. After all, terminating an application domain likely results in some operations failing and having to be retried from the user's perspective. Although you generally can't guarantee that all the add-ins you load into your host will be written with high availability in mind, you can follow some guidelines when writing your own managed code to ensure that the situations causing an application domain to be unloaded are kept to a minimum and that no resources you allocate are leaked if an application domain unload does occur.

In particular, the following guidelines can help you write code to function best in environments requiring long process lifetimes:

  • Use SafeHandles to encapsulate handles to native resources.

  • Use the synchronization primitives provided by the .NET Framework.

  • Ensure that all calls you make to unmanaged code return to the CLR.

  • Annotate your libraries with the HostProtectionAttribute.

Use SafeHandles to Encapsulate All Native Handles

As described earlier in the chapter, SafeHandles leverage the concepts of critical finalization and constrained execution regions to ensure that native handles are properly released when an application domain is unloaded. In general, you probably won't have to make explicit use of SafeHandles because the classes provided by the .NET Framework that wrap native resources all use SafeHandles on your behalf. For example, the classes in Microsoft.Win32 that provide registry access wrap registry handles in a SafeHandle, and the file-related classes in System.IO use SafeHandles to encapsulate native file handles. However, if you are accessing a native handle in your managed code without using an existing .NET Framework class, you have two options for wrapping your native handle in a SafeHandle. First, the .NET Framework provides a few classes derived from System.Runtime.InteropServices for wrapping specific types of handles. For example, the SafeFileHandle class in Microsoft.Win32.SafeHandles encapsulates a native file handle. There are also classes that wrap Open Database Connectivity (ODBC) connection handles, COM interface pointers, and so on. However, if one of the existing SafeHandle-derived classes doesn't meet your needs, writing your own is relatively straightforward.

Writing a class that leverages SafeHandle to encapsulate a new native handle type requires the following four steps:

1.

Create a class derived from System.Runtime.InteropServices.SafeHandle.

2.

Provide a constructor that enables callers to associate a native handle (typically represented by an IntPtr) with your SafeHandle.

3.

Provide an implementation of the ReleaseHandle method.

4.

Provide an implementation of the IsInvalid property.

IsInvalid and ReleaseHandle are abstract members that all classes derived from SafeHandle must implement. IsInvalid is a boolean property the CLR accesses to determine whether the underlying native handle is valid and therefore needs to be freed. The ReleaseHandle method is called by the CLR during critical finalization to free the native handle. Your implementation of ReleaseHandle will vary depending on which Win32 API is required to free the underlying handle. For example, if your SafeHandle-derived class encapsulates registry handles, your implementation of ReleaseHandle will likely call RegCloseKey. If your class wraps handles to device contexts used for printing, your implementation of ReleaseHandle would call Win32's DeleteDC method, and so on.

Both IsInvalid and ReleaseHandle are executed within a constrained execution region, so make sure that your implementations do not allocate memory. In most cases, IsInvalid should require just a simple check of the value of the handle, and ReleaseHandle should require only a PInvoke call to the Win32 API required to free the handle you've wrapped.

The following class is an example of a SafeHandle that can be used to encapsulate many types of Win32 handles. In particular, this class works with any handle that is freed using Win32's CloseHandle API. Examples of handles that can be used with this class include events, processes, files, and mutexes. My SafeHandle-derived class, along with a portion of the definition of SafeHandle itself, is shown here:

[SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode=true)] [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode=true)] public abstract class SafeHandle : CriticalFinalizerObject, IDisposable {     public abstract bool IsInvalid { get; }     protected abstract bool ReleaseHandle();     // Other methods on SafeHandle omitted... } public class SafeOSHandle : SafeHandle {         public SafeOSHandle(IntPtr existingHandle, bool ownsHandle)                    : base(IntPtr.Zero, ownsHandle)         {             SetHandle(existingHandle);         }     // Handle values of 0 and -1 are invalid.     public override bool IsInvalid     {         get { return handle == IntPtr.Zero || handle == new IntPtr(-1); }     }     [DllImport("kernel32.dll"), SuppressUnmanagedCodeSecurity]     private static extern bool CloseHandle(IntPtr handle);     // The implementation of ReleaseHandle simply calls Win32's     // CloseHandle.     override protected bool ReleaseHandle()     {         return CloseHandle(handle);     } }

Several aspects of SafeOSHandle are worth noting:

  • SafeHandle is derived from CriticalFinalizerObject. By deriving from CriticalFinalizerObject, the finalizers for all SafeHandle classes are guaranteed to be run, even when a thread or application domain is rudely aborted.

  • All classes derived from SafeHandle require the permission to call unmanaged code. SafeHandle is annotated with both an InheritanceDemand and a LinkDemand that require the ability to call unmanaged code. In practice, partially trusted callers likely wouldn't be able to derive from SafeHandle anyway because the implementation of ReleaseHandle often involves calling a Win32 API through PInvoke, which requires the ability to access unmanaged code.

  • The constructor has an ownsHandle parameter. The ownsHandle parameter is set to false in scenarios where you are using an instance of SafeHandle to wrap a handle you didn't explicitly create yourself. When ownsHandle is false, the CLR will not free the handle during critical finalization.

  • The call to CloseHandle is annotated with SuppressUnmanagedCodeSecurityAttribute. Generally, calls through PInvoke to unmanaged APIs cause the CLR to perform a full stack walk to determine whether all callers on the stack have permission to call unmanaged code. However, the dynamic nature of the stack walk can cause additional resources to be required, which should be avoided while running in a CER. The SuppressUnmanagedCodeSecurityAttribute has the effect of moving the security check from the time at which the method is called to the time at which it is jit-compiled. This happens because SuppressUnmanagedCodeSecurityAttribute causes a link demand to occur instead of a full stack walk. So the security check happens when the method is prepared for execution in a CER instead of while running in the CER, thereby avoiding potential failures when the method is executed.

Use Only the Synchronization Primitives Provided by the .NET Framework

I've shown how hosts can use the escalation policy interfaces to treat resource failures differently if they occur in a critical region of code. Recall that a critical region of code is defined as any code that the CLR determines to be manipulating state that is shared across multiple threads. The heuristic the CLR uses to determine whether code is editing shared state is based on synchronization primitives. Specifically, if a resource failure occurs on a thread in which code is waiting on a synchronization primitive, the CLR assumes the code is using the primitive to synchronize access to shared state. However, this heuristic is useful only if the CLR can always detect when code is waiting on a synchronization primitive. So it's important always to use the synchronization primitives provided by the .NET Framework instead of inventing your own. In particular, the System.Threading namespace provides the following set of primitives you can use for synchronization:

  • Monitor

  • Mutex

  • ReaderWriterLock

If you synchronize access to shared state using a mechanism of your own, the CLR won't be able to detect that you are editing shared state should a resource failure occur. So the escalation policy defined by the host for resource failures in critical regions of code will not be used, thereby potentially leaving an application domain in an inconsistent state.

Ensure That Calls to Unmanaged Code Return to the CLR

Throughout this chapter, I've discussed how the CLR relies on the ability to abort threads and unload application domains to guarantee process integrity. However, in some cases a thread can enter a state that prevents the CLR from being able to abort it. In particular, if you use PInvoke to call an unmanaged API that waits infinitely on a synchronization primitive or performs any other type of blocking operation, the CLR won't be able to abort the thread. Once a thread leaves the CLR through a PInvoke call, the CLR is no longer able to control all aspects of that thread's execution. In particular, all synchronization primitives created out in unmanaged code will be unknown to the CLR. So the CLR cannot unblock the thread to abort it. You can avoid this situation primarily by specifying reasonable timeout values whenever you wait on a blocking operation in unmanaged code. Most of the Win32 APIs that allow you to wait for a particular resource enable you to specify timeout values. For example, consider the case in which you use PInvoke to call an unmanaged function that creates a mutex and uses the Win32 API WaitForSingleObject to wait until the mutex is signaled. To ensure that your call won't wait indefinitely, your call to WaitForSingleObject should specify a finite timeout value. In this way, you'll be able to regain control after a specified interval and avoid all situations that would prevent the CLR from being able to abort your thread.

Annotate Your Libraries with the HostProtectionAttribute

In Chapter 12, I discuss how hosts can use a feature called host protection to prevent APIs that violate various aspects of their programming model. For example, hosts can use the host protection feature to prevent an add-in from using any API that allows it to share state across threads. If an add-in is not allowed to share state in the first place, the host portion of escalation policy dealing with critical regions of code will never be required, thus resulting in fewer application domain unloads (or whatever other action the host specified).

For host protection to be effective, all APIs that provide capabilities identified by a set of host protection categories must be annotated with a custom attribute called the HostProtectionAttribute. Refer to Chapter 12 for more a complete description of the host protection categories and how to use HostProtectionAttribute to annotate your managed code libraries properly.



    Customizing the Microsoft  .NET Framework Common Language Runtime
    Customizing the Microsoft .NET Framework Common Language Runtime
    ISBN: 735619883
    EAN: N/A
    Year: 2005
    Pages: 119

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net