Click here to Skip to main content
15,885,782 members
Articles / Programming Languages / C#
Article

IDisposable: For the CF Guys, Too

Rate me:
Please Sign up or sign in to vote.
4.82/5 (5 votes)
24 Sep 2008BSD13 min read 21.7K   17   1
Implementing the Disposable Design Principle on the .NET Compact Framework.

Introduction

In my first article, I introduced the Disposable Design Principle. This is the model that Microsoft has moved to for .NET 2.0, in preference over the old IDisposable code pattern (actually, Microsoft did this in a backwards-compatible way, whereas I recommend a complete break). However, some changes are necessary to support the .NET Compact Framework (which Microsoft did not move over to the Disposable Design Principle).

A Brief Look at Shutdowns

There are three types of shutdowns possible when running framework code:

  • Orderly shutdown - when a shutdown request is issued to code running normally, e.g., Application.Exit.
  • Graceful abortive shutdown - when an AppDomain is unloaded: all threads are aborted and then all finalizers are run.
  • Rude abortive shutdown - when an AppDomain is rudely unloaded: all threads are aborted (with different semantics than the graceful abortive shutdown) and then finalizers are run.

My next article will go into more detail regarding shutdown situations and when to expect them. For the purposes of this article, only the following facts are necessary:

  • When threads are aborted during a graceful abortive shutdown, the runtime will wait for threads to exit a Constrained Execution Region, unmanaged code, or a finally block.
  • When threads are aborted during a rude abortive shutdown, the runtime will wait for threads to exit a Constrained Execution Region or unmanaged code, but not an ordinary finally block.

Review of the Disposable Design Principle

This is the same description of the Disposable Design Principle as in my first article, except I've changed some of the wording to apply it to the Compact Framework (and added a note that Level 0 type constructors must be called from within an atomic execution region).

The Disposable Design Principle splits up resource management responsibilities into Level 0 types (which handle unmanaged resources), and Level 1 types (which are still small wrapper classes that closely resemble the native API, but only handle managed resources):

  1. An "atomic execution region" is defined as one of (in order from strongest guarantee to weakest):
    1. Unmanaged code - in this case, the Disposable Design Principle provides a no-leak guarantee for any type of shutdown.
    2. A Constrained Execution Region - in this case, the Disposable Design Principle provides a no-leak guarantee for any type of shutdown.
    3. A finally block - in this case, the Disposable Design Principle provides a no-leak guarantee for orderly and graceful abortive shutdowns, but may leak on a rude abortive shutdown.
    4. Ordinary managed code - in this case, the Disposable Design Principle provides a no-leak guarantee for an orderly shutdown, but may leak on graceful abortive or rude abortive shutdowns.
  2. Level 0 types directly wrap unmanaged resources, and are only concerned with deallocation of their resource.
    1. Level 0 types are either abstract or sealed.
    2. Level 0 types must be designed to execute completely within an atomic execution region.
      • For Constrained Execution Regions, this means that Level 0 types must be a descendant of CriticalFinalizerObject.
      • For finally blocks, this means that Level 0 types must derive from a separately-defined base type which implements IDisposable to deallocate the unmanaged resource explicitly (possibly called in the context of a finally block) or from a finalizer.
    3. Constructors for Level 0 types must be called from within an atomic execution region.
      • The special full framework interop handling of SafeHandle return values is considered unmanaged code (and therefore an atomic execution region of the strongest guarantee).
  3. Level 1 types only deal with managed resources.
    1. Level 1 types are generally sealed unless they are defining a base Level 1 type for a Level 1 hierarchy.
    2. Level 1 types derive from Level 1 types or from IDisposable directly; they do not derive from CriticalFinalizerObject or Level 0 types.
    3. Level 1 types may have fields that are Level 0 or Level 1 types.
    4. Level 1 types implement IDisposable.Dispose by calling Dispose on each of its Level 0 and Level 1 fields, and then calling base.Dispose, if applicable.
    5. Level 1 types do not have finalizers.
    6. When defining a Level 1 type hierarchy, the abstract root base type should define a protected property with the name and type of the associated Level 0 type.

In a future article, I will introduce Level 2 types, which allow running (almost) arbitrary shutdown code, but place additional requirements on end-users.

.NET Compact Framework Restrictions

The main difficulty in developing Level 0 types for the .NET Compact Framework is the lack of SafeHandle. This article will present a Compact Framework version of SafeHandle, but it will require special additional code when used in interop situations. Due to the differences between the compact and full framework support for interop (e.g., DllImportAttribute.BestFitMapping, SuppressUnmanagedCodeSecurityAttribute, etc.), it is generally best if no interop code is shared between the two platforms anyway.

Another difficulty is the lack of Constrained Execution Regions, although it turns out, this difficulty is really a blessing in disguise.

The Simplifying Assumption

Because there are no Constrained Execution Regions on the Compact Framework, it is not possible to write managed Level 0 types that can handle rude abortive shutdowns.

The lack of Constrained Execution Regions on the .NET Compact Framework leaves coders with one of two possibilities:

  1. Write an unmanaged wrapper function for every unmanaged resource allocation function, which can directly manipulate the SafeHandle (or derived type) implementation. Alternatively, a single unmanaged wrapper function may be written, taking a delegate (as a function pointer), but this introduces additional overhead and does not handle non-IntPtr unmanaged resource types.
  2. Assume that on compact platforms, if a rude abortive shutdown happens, then the whole process is going to exit.

This article makes the simplifying assumption. On the compact platform, most .NET processes have exactly one AppDomain, so this assumption holds true. The Level 0 types developed in this article will support graceful abortive shutdowns, so they also support multiple AppDomains per process if their shutdowns are always graceful.

The Level 0 types in my first article supported rude abortive shutdowns through the use of Constrained Execution Regions. The Level 0 types in this article will support graceful abortive shutdowns through the use of finally blocks.

Minimal SafeHandle for the .NET Compact Framework

The following code is a simplified SafeHandle:

  • There is no reference counting, so Compact Framework interop code must use GC.KeepAlive correctly to compensate.
  • SafeHandle instances always own their handles.
  • The handle does not have a separate "closed" state; the handle is closed when the SafeHandle is destroyed.
C#
// .NET 2.0 and up already have System.Runtime.InteropServices.SafeHandle
#if DOTNET_ISCF
namespace System.Runtime.InteropServices
{
    /// <summary>
    /// Base type for all Level 0 types.
    /// </summary>
    /// <remarks>
    /// <para>This is part of the TBA compatibility layer, and is only
    ///       included in TBA libraries if necessary for their
    ///       supported platform.</para>
    /// <para>This type maintains an <see cref="IntPtr"/> handle.</para>
    /// <para>Note that this type does not support interop directly;
    ///       any P/Invoke code must use the Dangerous functions
    ///       on the .NET CF platform.</para>
    /// </remarks>
    public abstract class SafeHandle : IDisposable
    {
        /// <summary>
        /// The handle to be wrapped.
        /// </summary>
        protected IntPtr handle;

        /// <summary>
        /// Whether this object has been disposed.
        /// </summary>
        private bool disposed;

        /// <summary>
        /// Initializes a new instance with the specified invalid handle value.
        /// </summary>
        /// <param name="invalidValue">The invalid handle
        ///          value, usually 0 or -1.</param>
        /// <param name="owned">Must be true.</param>
        public SafeHandle(IntPtr invalidValue, bool owned)
        {
            handle = invalidValue;
        }

        /// <summary>
        /// Disposes the handle if necessary.
        /// </summary>
        private void DoDispose()
        {
            if (!IsInvalid)
                ReleaseHandle();
        }

        /// <summary>
        /// Returns the value of the <see cref="handle"/> field.
        /// </summary>
        /// <remarks>
        /// <para>This function is normally used to support passing
        ///    <c>IntPtr</c> arguments to P/Invoke functions
        ///    on the .NET Compact Framework.
        /// Note that a call to <see cref="GC.KeepAlive"/> is normally required.</para>
        /// </remarks>
        /// <returns>The value of the <see cref="handle"/> field.</returns>
        public IntPtr DangerousGetHandle()
        {
            return handle;
        }

        /// <summary>
        /// Sets the handle to the specified handle value. A call to this function
        /// should be within a <c>finally</c> block.
        /// </summary>
        /// <remarks>
        /// <para>This function is normally used to support returning
        /// <c>IntPtr</c> values from P/Invoke functions
        /// on the .NET Compact Framework.</para>
        /// <para>This is functionally equivalent to calling
        /// <c>SafeHandle.SetHandle</c> on a non-Compact Framework platform.</para>
        /// </remarks>
        /// <param name="Handle">The handle value to set.</param>
        public void DangerousSetHandle(IntPtr Handle)
        {
            handle = Handle;
        }

        /// <summary>
        /// Whether the handle value is invalid.
        /// </summary>
        public abstract bool IsInvalid { get; }

        /// <summary>
        /// Releases the handle. May not throw exceptions.
        /// </summary>
        /// <returns>Whether releasing the handle was successful.</returns>
        protected abstract bool ReleaseHandle();

        /// <summary>
        /// Frees the handle.
        /// </summary>
        /// <remarks>
        /// <para>It is safe to call this function multiple times.</para>
        /// </remarks>
        public void Dispose()
        {
            // Note: once this function has started, the finalizer
            // cannot run (because of the GC.KeepAlive at the end).
            if (!disposed)
                DoDispose();
            disposed = true;

            // Since the dispose method just completed, don't call the finalizer
            GC.SuppressFinalize(this);

            // Ensure the object is not garbage collected
            // until the dispose method completes
            GC.KeepAlive(this);
        }

        /// <summary>
        /// The finalizer will catch those situations
        /// where the object was not disposed.
        /// </summary>
        ~SafeHandle()
        {
            // Note: this function can run in one of five
            // situations (see CLR via C#, Jeffrey Richter, pg 478):
            //  1) Generation 0 is full
            //  2) Code explicitly calls System.GC's static Collect method
            //  3) Windows is reporting low memory conditions
            //  4) The CLR is unloading an AppDomain - in this case,
            //     AppDomain.CurrentDomain.IsFinalizingForUnload() is true.
            //  5) The CLR is shutting down (e.g., a normal process termination)
            //     - in this case, Environment.HasShutdownStarted is true.
            // However, if we're here, then at least
            // we know that Dispose() was never called.
            DoDispose();
        }
    }
}
#endif

Notes:

  • The DOTNET_ISCF is a personal convention, indicating that compiling is being done for the Compact Framework.
  • The type is placed in the System.Runtime.InteropServices namespace. Others may disagree with this decision; I did it simply so I could share as much code as possible between the compact and regular framework platforms.
  • This type does have to keep track of whether or not it has been disposed, to allow Dispose to be called multiple times without side effects.
  • GC.SuppressFinalize and GC.KeepAlive prevent the finalizer from running if Dispose is called (or will be called). Therefore, when the finalizer runs, it knows that Dispose was never called and will never be called.
  • IsInvalid and ReleaseHandle do not run inside Constrained Execution Regions. However, since they may be run in a finalizer context, many of the same restrictions apply. E.g., they should not raise exceptions or access any managed objects.

This simplified SafeHandle may now be used to define Level 1 types. Before starting, there are a handful of helper functions that help out with P/Invoke on the Compact Framework:

C#
/// <summary>
/// General interop utility functions.
/// </summary>
public static class Interop
{
    /// <summary>
    /// Translates a Win32 error code into a HRESULT error code.
    /// </summary>
    /// <param name="win32Error">The Win32 error code to translate.</param>
    /// <returns>The equivalent HRESULT error code.</returns>
    public static int HRFromWin32Error(int win32Error)
    {
        // This function follows the same logic
        // as the HRESULT_FROM_WIN32 macro in unmanaged code.
        if (win32Error <= 0)
            return win32Error;
        uint ret = unchecked((uint)win32Error);
        // Strip off the code portion (lower 16 bits)
        ret &= 0xFFFF;
        // Set facility to FACILITY_WIN32 (7)
        ret |= (7 << 16);
        // Set error bit
        ret |= 0x80000000;
        return unchecked((int)ret);
    }

#if DOTNET_ISCF
    /// <summary>
    /// Throws an exception for the last P/Invoke function
    /// that had <see cref="DllImportAttribute.SetLastError"/>
    /// set to <c>true</c>.
    /// </summary>
    public static void ThrowExceptionForLastWin32Error()
    {
        Marshal.ThrowExceptionForHR(HRFromWin32Error(Marshal.GetLastWin32Error()));
    }
#else
    /// <summary>
    /// Throws an exception for the last P/Invoke function that had
    /// <see cref="DllImportAttribute.SetLastError"/>
    /// set to <c>true</c>.
    /// </summary>
    public static void ThrowExceptionForLastWin32Error()
    {
        Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
    }
#endif
}

Wrapping Unmanaged Resources - Using Existing "Level 0" Types (The Easy Case)

Unfortunately, on the Compact Framework, WaitHandle is not a true Level 1 type. By deriving from it, however, we can still create a ManualResetTimer that acts like a Level 1 type.

C#
#if DOTNET_ISCF
internal static partial class NativeMethods
{
    [DllImport("kernel32.dll", EntryPoint = 
      "CreateWaitableTimer", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr DoCreateWaitableTimer(IntPtr lpTimerAttributes,
        [MarshalAs(UnmanagedType.Bool)] bool bManualReset, string lpTimerName);
    internal static void CreateWaitableTimer(IntPtr lpTimerAttributes, 
             bool bManualReset, string lpTimerName, WaitHandle ret)
    {
        try { }
        finally
        {
            ret.Handle = DoCreateWaitableTimer(lpTimerAttributes, 
                         bManualReset, lpTimerName);
        }
        if (ret.Handle == IntPtr.Zero)
            Interop.ThrowExceptionForLastWin32Error();
    }

    [DllImport("kernel32.dll", EntryPoint = 
               "CancelWaitableTimer", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool DoCancelWaitableTimer(IntPtr hTimer);
    internal static void CancelWaitableTimer(WaitHandle hTimer)
    {
        bool ret = DoCancelWaitableTimer(hTimer.Handle);
        GC.KeepAlive(hTimer);
        if (!ret)
            Interop.ThrowExceptionForLastWin32Error();
    }

    [DllImport("kernel32.dll", EntryPoint = 
               "SetWaitableTimer", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool DoSetWaitableTimer(IntPtr hTimer, 
        [In] ref long pDueTime, int lPeriod,
        IntPtr pfnCompletionRoutine, IntPtr lpArgToCompletionRoutine, 
        [MarshalAs(UnmanagedType.Bool)] bool fResume);
    internal static void SetWaitableTimer(WaitHandle hTimer, 
        long pDueTime, int lPeriod, IntPtr pfnCompletionRoutine,
        IntPtr lpArgToCompletionRoutine, bool fResume)
    {
        bool ret = DoSetWaitableTimer(hTimer.Handle, ref pDueTime, 
                   lPeriod, pfnCompletionRoutine, lpArgToCompletionRoutine, fResume);
        GC.KeepAlive(hTimer);
        if (!ret)
            Interop.ThrowExceptionForLastWin32Error();
    }
}
#else
[SecurityPermission(SecurityAction.LinkDemand, Flags = 
                    SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
    [DllImport("kernel32.dll", EntryPoint = 
        "CreateWaitableTimer", CharSet = CharSet.Auto, BestFitMapping = false,
        ThrowOnUnmappableChar = true, SetLastError = true), 
        SuppressUnmanagedCodeSecurity]
    private static extern SafeWaitHandle DoCreateWaitableTimer(IntPtr lpTimerAttributes,
        [MarshalAs(UnmanagedType.Bool)] bool bManualReset, string lpTimerName);
    internal static void CreateWaitableTimer(IntPtr lpTimerAttributes, 
             bool bManualReset, string lpTimerName, WaitHandle ret)
    {
        ret.SafeWaitHandle = DoCreateWaitableTimer(lpTimerAttributes, 
                             bManualReset, lpTimerName);
        if (ret.SafeWaitHandle.IsInvalid)
            Interop.ThrowExceptionForLastWin32Error();
    }

    [DllImport("kernel32.dll", EntryPoint = "CancelWaitableTimer", 
               SetLastError = true), SuppressUnmanagedCodeSecurity]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool DoCancelWaitableTimer(SafeWaitHandle hTimer);
    internal static void CancelWaitableTimer(WaitHandle hTimer)
    {
        if (!DoCancelWaitableTimer(hTimer.SafeWaitHandle))
            Interop.ThrowExceptionForLastWin32Error();
    }

    [DllImport("kernel32.dll", EntryPoint = "SetWaitableTimer", 
               SetLastError = true), SuppressUnmanagedCodeSecurity]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool DoSetWaitableTimer(SafeWaitHandle hTimer, 
        [In] ref long pDueTime, int lPeriod,
        IntPtr pfnCompletionRoutine, IntPtr lpArgToCompletionRoutine, 
        [MarshalAs(UnmanagedType.Bool)] bool fResume);
    internal static void SetWaitableTimer(WaitHandle hTimer, 
        long pDueTime, int lPeriod, IntPtr pfnCompletionRoutine,
        IntPtr lpArgToCompletionRoutine, bool fResume)
    {
        if (!DoSetWaitableTimer(hTimer.SafeWaitHandle, ref pDueTime, 
            lPeriod, pfnCompletionRoutine, lpArgToCompletionRoutine, fResume))
            Interop.ThrowExceptionForLastWin32Error();
    }
}
#endif

/// <summary>
/// A manual-reset, non-periodic, waitable timer.
/// </summary>
public sealed class ManualResetTimer : WaitHandle
{
    /// <summary>
    /// Creates a new <see cref="ManualResetTimer"/>.
    /// </summary>
    public ManualResetTimer()
    {
        NativeMethods.CreateWaitableTimer(IntPtr.Zero, true, null, this);
    }

    /// <summary>
    /// Cancels the timer. This does not change the signaled state.
    /// </summary>
    public void Cancel()
    {
        NativeMethods.CancelWaitableTimer(this);
    }

    /// <summary>
    /// Sets the timer to signal at the specified time,
    /// which may be an absolute time or a relative (negative) time.
    /// </summary>
    /// <param name="dueTime">The time,
    /// interpreted as a <c>FILETIME</c> value</param>
    private void Set(long dueTime)
    {
        NativeMethods.SetWaitableTimer(this, dueTime, 0, 
                       IntPtr.Zero, IntPtr.Zero, false);
    }

    /// <summary>
    /// Sets the timer to signal at the specified time. Resets the signaled state.
    /// </summary>
    /// <param name="when">The time that this
    /// timer should become signaled.</param>
    public void Set(DateTime when) { Set(when.ToFileTimeUtc()); }

    /// <summary>
    /// Sets the timer to signal after a time span. Resets the signaled state.
    /// </summary>
    /// <param name="when">The time span
    /// after which the timer will become signaled.</param>
    public void Set(TimeSpan when) { Set(-when.Ticks); }
}

Notes:

  • The full framework interop code was modified so it has the same method signatures as the Compact Framework interop code. This enables writing one ManualResetTimer for both platforms.
  • For the resource allocation function (NativeMethods.CreateWaitableTimer), a finally block was used to atomically allocate the resource and assign it into the handle value in the Level 0 type (remember, on the Compact Framework, WaitHandle does not follow the Disposable Design Principle, so it has some aspects of a Level 0 type and other aspects of a Level 1 type). Allocation errors are handled after the finally block; this is not required, but it is good programming practice.
  • For interop functions that take resource handles as arguments, GC.KeepAlive is used to ensure the WaitHandle is not disposed from a finalizer while the handle is being used by unmanaged code. Again, error handling is put off until after GC.KeepAlive; this is not required, but is good practice.

Since WaitHandle is not a Level 1 type on the Compact Framework, this example was more of a special-case scenario. The following examples result in true Level 1 types.

Wrapping Unmanaged Resources - Defining Level 0 Types for Pointers (The Intermediate Case)

The Level 0 type is a straightforward translation from the first article's example:

C#
#if DOTNET_ISCF
internal static partial class NativeMethods
{
    [DllImport("user32.dll", EntryPoint = 
               "CloseWindowStation", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool CloseWindowStation(IntPtr hWinSta);
}
#else
[SecurityPermission(SecurityAction.LinkDemand, 
   Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
    [DllImport("user32.dll", EntryPoint = "CloseWindowStation", 
               SetLastError = true), SuppressUnmanagedCodeSecurity]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool CloseWindowStation(IntPtr hWinSta);
}
#endif

/// <summary>
/// Level 0 type for window station handles.
/// </summary>
public sealed class SafeWindowStationHandle : SafeHandle
{
    public SafeWindowStationHandle() : base(IntPtr.Zero, true) { }
    public override bool IsInvalid
    {
#if !DOTNET_ISCF
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
#endif
        get { return (handle == IntPtr.Zero); }
    }

#if !DOTNET_ISCF
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
    [PrePrepareMethod]
#endif
    protected override bool ReleaseHandle()
    {
        return NativeMethods.CloseWindowStation(handle);
    }
}

There are no significant changes other than the removal of the Code Access Security and Constrained Execution Region attributes, which are not supported on the Compact Framework.

The Level 1 type shows the pattern needed for Compact Framework interop calls:

C#
#if DOTNET_ISCF
internal static partial class NativeMethods
{
    [DllImport("user32.dll", EntryPoint = "OpenWindowStation", 
               CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr DoOpenWindowStation(string lpszWinSta,
        [MarshalAs(UnmanagedType.Bool)] bool fInherit, uint dwDesiredAccess);
    internal static SafeWindowStationHandle 
             OpenWindowStation(string lpszWinSta, 
             bool fInherit, uint dwDesiredAccess)
    {
        SafeWindowStationHandle ret = new SafeWindowStationHandle();
        try { }
        finally
        {
            ret.DangerousSetHandle(DoOpenWindowStation(lpszWinSta, 
                                   fInherit, dwDesiredAccess));
        }
        if (ret.IsInvalid)
            Interop.ThrowExceptionForLastWin32Error();
        return ret;
    }

    [DllImport("user32.dll", EntryPoint = 
               "SetProcessWindowStation", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool DoSetProcessWindowStation(IntPtr hWinSta);
    internal static void SetProcessWindowStation(SafeWindowStationHandle hWinSta)
    {
        bool ok = DoSetProcessWindowStation(hWinSta.DangerousGetHandle());
        GC.KeepAlive(hWinSta);
        if (!ok)
            Interop.ThrowExceptionForLastWin32Error();
    }
}
#else
[SecurityPermission(SecurityAction.LinkDemand, 
 Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
    [DllImport("user32.dll", EntryPoint = "OpenWindowStation", 
        CharSet = CharSet.Auto, BestFitMapping = false,
        ThrowOnUnmappableChar = true, SetLastError = true), 
        SuppressUnmanagedCodeSecurity]
    private static extern SafeWindowStationHandle 
         DoOpenWindowStation(string lpszWinSta,
        [MarshalAs(UnmanagedType.Bool)] bool fInherit, uint dwDesiredAccess);
    internal static SafeWindowStationHandle 
             OpenWindowStation(string lpszWinSta, 
             bool fInherit, uint dwDesiredAccess)
    {
        SafeWindowStationHandle ret = DoOpenWindowStation(lpszWinSta, 
                                      fInherit, dwDesiredAccess);
        if (ret.IsInvalid)
            Interop.ThrowExceptionForLastWin32Error();
        return ret;
    }

    [DllImport("user32.dll", EntryPoint = "SetProcessWindowStation", 
               SetLastError = true), SuppressUnmanagedCodeSecurity]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool DoSetProcessWindowStation(SafeWindowStationHandle hWinSta);
    internal static void SetProcessWindowStation(SafeWindowStationHandle hWinSta)
    {
        if (!DoSetProcessWindowStation(hWinSta))
            Interop.ThrowExceptionForLastWin32Error();
    }
}
#endif

/// <summary>
/// A window station.
/// </summary>
public sealed class WindowStation : IDisposable
{
    /// <summary>
    /// The underlying window station handle.
    /// </summary>
    private SafeWindowStationHandle SafeWindowStationHandle;

    /// <summary>
    /// Implementation of IDisposable: closes the underlying window station handle.
    /// </summary>
    public void Dispose()
    {
        SafeWindowStationHandle.Dispose();
    }

    /// <summary>
    /// Opens an existing window station.
    /// </summary>
    public WindowStation(string name)
    {
        // ("0x37F" is WINSTA_ALL_ACCESS)
        SafeWindowStationHandle = NativeMethods.OpenWindowStation(name, false, 0x37F);
    }

    /// <summary>
    /// Sets this window station as the active one for this process.
    /// </summary>
    public void SetAsActive()
    {
        NativeMethods.SetProcessWindowStation(SafeWindowStationHandle);
    }
}

Notes:

  • The resource allocation function follows a pattern which should now be familiar:
    1. Allocate the return object (initialized to the invalid handle value).
    2. Within a finally block, make the allocation, and atomically assign it into the return object.
    3. Do error checking after the atomic region.
  • Likewise, the functions that take Level 0 type parameters follow a similar pattern:
    1. Call the native function, but do not handle errors yet.
    2. Call GC.KeepAlive to make sure the Level 0 type is not garbage collected while it's being used by the unmanaged code.
    3. Perform error handling.

Wrapping Unmanaged Resources - Defining Level 0 Types for Pointers With Context Data (The "Advanced" Case)

This time, the Level 0 resource deallocation function does have a small twist:

C#
#if DOTNET_ISCF
internal static partial class NativeMethods
{
    [DllImport("kernel32.dll", 
     EntryPoint = "VirtualFreeEx", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool DoVirtualFreeEx(IntPtr hProcess, 
            IntPtr lpAddress, UIntPtr dwSize, uint dwFreeType);
    internal static bool VirtualFreeEx(SafeHandle hProcess, 
             IntPtr lpAddress, UIntPtr dwSize, uint dwFreeType)
    {
        bool ret = DoVirtualFreeEx(hProcess.DangerousGetHandle(), 
                   lpAddress, dwSize, dwFreeType);
        GC.KeepAlive(hProcess);
        return ret;
    }
}
#else
[SecurityPermission(SecurityAction.LinkDemand, 
 Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
    [DllImport("kernel32.dll", EntryPoint = 
      "VirtualFreeEx", SetLastError = true), 
      SuppressUnmanagedCodeSecurity]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool VirtualFreeEx(SafeHandle hProcess, 
             IntPtr lpAddress, UIntPtr dwSize, uint dwFreeType);
}
#endif

/// <summary>
/// Level 0 type for memory allocated in another process.
/// </summary>
public sealed class SafeRemoteMemoryHandle : SafeHandle
{
    public SafeHandle SafeProcessHandle
    {
#if !DOTNET_ISCF
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
#endif
        get;

#if !DOTNET_ISCF
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
#endif
        private set;
    }

    public SafeRemoteMemoryHandle() : base(IntPtr.Zero, true) { }

    public override bool IsInvalid
    {
#if !DOTNET_ISCF
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
#endif
        get { return (handle == IntPtr.Zero); }
    }

#if !DOTNET_ISCF
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
    [PrePrepareMethod]
#endif
    protected override bool ReleaseHandle()
    {
        // (0x8000 == MEM_RELEASE)
        return NativeMethods.VirtualFreeEx(SafeProcessHandle, 
                               handle, UIntPtr.Zero, 0x8000);
    }

    /// <summary>
    /// Overwrites the handle value (without releasing it).
    /// This should only be called from functions acting as constructors.
    /// </summary>
#if !DOTNET_ISCF
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
    [PrePrepareMethod]
#endif
    internal void SetHandle(IntPtr handle_, SafeHandle safeProcessHandle)
    {
        handle = handle_;
        SafeProcessHandle = safeProcessHandle;
    }
}

The resource deallocation function for the memory takes as an argument the process handle (which, remember, really should be a SafeProcessHandle instead of SafeHandle). Because it takes a safe handle as an argument, it has to give it the special GC.KeepAlive treatment.

The interop code for the Level 1 type has the logical changes:

C#
#if DOTNET_ISCF
internal static partial class NativeMethods
{
    [DllImport("kernel32.dll", EntryPoint = "VirtualAllocEx", SetLastError = true)]
    private static extern IntPtr DoVirtualAllocEx(IntPtr hProcess, 
            IntPtr lpAddress, UIntPtr dwSize,
            uint flAllocationType, uint flProtect);
    internal static SafeRemoteMemoryHandle VirtualAllocEx(SafeHandle hProcess, 
             IntPtr lpAddress, UIntPtr dwSize, uint flAllocationType, uint flProtect)
    {
        SafeRemoteMemoryHandle ret = new SafeRemoteMemoryHandle();

        // Atomically get the native handle and assign it into our return object.
        try { }
        finally
        {
            IntPtr address = DoVirtualAllocEx(hProcess.DangerousGetHandle(), 
                             lpAddress, dwSize, flAllocationType, flProtect);
            GC.KeepAlive(hProcess);
            if (address != IntPtr.Zero)
                ret.SetHandle(address, hProcess);
        }

        // Do error handling after the atomic region
        if (ret.IsInvalid)
            Interop.ThrowExceptionForLastWin32Error();
        return ret;
    }

    [DllImport("kernel32.dll", EntryPoint = 
               "WriteProcessMemory", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool DoWriteProcessMemory(IntPtr hProcess, 
            IntPtr lpBaseAddress, IntPtr lpBuffer, UIntPtr nSize, 
            out UIntPtr lpNumberOfBytesWritten);
    internal static void WriteProcessMemory(SafeRemoteMemoryHandle 
                         RemoteMemory, IntPtr lpBuffer, UIntPtr nSize)
    {
        UIntPtr NumberOfBytesWritten;
        bool ok = DoWriteProcessMemory(RemoteMemory.SafeProcessHandle.DangerousGetHandle(), 
                                       RemoteMemory.DangerousGetHandle(),
            lpBuffer, nSize, out NumberOfBytesWritten);
        GC.KeepAlive(RemoteMemory);
        if (!ok)
            Interop.ThrowExceptionForLastWin32Error();
        if (nSize != NumberOfBytesWritten)
            throw new Exception("WriteProcessMemory: " + 
                  "Failed to write all bytes requested");
    }
}
#else
[SecurityPermission(SecurityAction.LinkDemand, 
   Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
    [DllImport("kernel32.dll", EntryPoint = "VirtualAllocEx", 
               SetLastError = true), SuppressUnmanagedCodeSecurity]
    private static extern IntPtr DoVirtualAllocEx(SafeHandle hProcess, 
            IntPtr lpAddress, UIntPtr dwSize, 
            uint flAllocationType, uint flProtect);
    internal static SafeRemoteMemoryHandle 
             VirtualAllocEx(SafeHandle hProcess, IntPtr lpAddress, 
             UIntPtr dwSize, uint flAllocationType, uint flProtect)
    {
        SafeRemoteMemoryHandle ret = new SafeRemoteMemoryHandle();

        // Atomically get the native handle and assign it into our return object.
        RuntimeHelpers.PrepareConstrainedRegions();
        try { }
        finally
        {
            IntPtr address = DoVirtualAllocEx(hProcess, lpAddress, 
                             dwSize, flAllocationType, flProtect);
            if (address != IntPtr.Zero)
                ret.SetHandle(address, hProcess);
        }

        // Do error handling after the CER
        if (ret.IsInvalid)
            Interop.ThrowExceptionForLastWin32Error();
        return ret;
    }

    [DllImport("kernel32.dll", EntryPoint = "WriteProcessMemory", 
               SetLastError = true), SuppressUnmanagedCodeSecurity]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool DoWriteProcessMemory(SafeHandle hProcess, 
            SafeRemoteMemoryHandle lpBaseAddress, IntPtr lpBuffer, 
            UIntPtr nSize, out UIntPtr lpNumberOfBytesWritten);
    internal static void WriteProcessMemory(SafeRemoteMemoryHandle RemoteMemory, 
                         IntPtr lpBuffer, UIntPtr nSize)
    {
        UIntPtr NumberOfBytesWritten;
        if (!DoWriteProcessMemory(RemoteMemory.SafeProcessHandle, 
            RemoteMemory, lpBuffer, nSize, out NumberOfBytesWritten))
            Interop.ThrowExceptionForLastWin32Error();
        if (nSize != NumberOfBytesWritten)
            throw new Exception("WriteProcessMemory: " + 
                  "Failed to write all bytes requested");
    }
}
#endif

The only noteworthy element is the fact that the Compact Framework NativeMethods.VirtualAllocEx does not have to call GC.KeepAlive on the process handle, because it is contained by the SafeRemoteMemoryHandle, which is being kept alive.

The RemoteMemory Level 1 type itself is unchanged from the previous article.

Wrapping Unmanaged Resources - Defining Level 0 Types for Non-Pointer Data (The "Hard" Case)

The Level 0 type is completely straightforward:

C#
#if DOTNET_ISCF
internal static partial class NativeMethods
{
    [DllImport("kernel32.dll", EntryPoint = "DeleteAtom", SetLastError = true)]
    internal static extern ushort DeleteAtom(ushort nAtom);
}
#else
[SecurityPermission(SecurityAction.LinkDemand, 
   Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
    [DllImport("kernel32.dll", EntryPoint = "DeleteAtom", 
               SetLastError = true), SuppressUnmanagedCodeSecurity]
    internal static extern ushort DeleteAtom(ushort nAtom);
}
#endif

/// <summary>
/// Level 0 type for local atoms (casting implementation).
/// </summary>
public sealed class SafeAtomHandle : SafeHandle
{
    /// <summary>
    /// Internal unmanaged handle value, translated to the correct type.
    /// </summary>
    public ushort Handle
    {
#if !DOTNET_ISCF
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
#endif
        get
        {
            return unchecked((ushort)(short)handle);
        }

#if !DOTNET_ISCF
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
#endif
        internal set
        {
            handle = unchecked((IntPtr)(short)value);
        }
    }

    /// <summary>
    /// Default constructor initializing with an invalid handle value.
    /// </summary>
    public SafeAtomHandle() : base(IntPtr.Zero, true) { }

    /// <summary>
    /// Whether or not the handle is invalid.
    /// </summary>
    public override bool IsInvalid
    {
#if !DOTNET_ISCF
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
#endif
        get { return (Handle == 0); }
    }

    /// <summary>
    /// Releases the handle.
    /// </summary>
#if !DOTNET_ISCF
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
    [PrePrepareMethod]
#endif
    protected override bool ReleaseHandle()
    {
        return (NativeMethods.DeleteAtom(Handle) == 0);
    }
}

Finally, the Level 1 interop is actually simpler for the Compact Framework; there are no surprises here:

C#
#if DOTNET_ISCF
internal static partial class NativeMethods
{
    [DllImport("kernel32.dll", EntryPoint = "AddAtom", 
               CharSet = CharSet.Auto, SetLastError = true)]
    private static extern ushort DoAddAtom(string lpString);
    internal static SafeAtomHandle AddAtom(string lpString)
    {
        SafeAtomHandle ret = new SafeAtomHandle();

        // Atomically get the native handle and assign it into our return object.
        try { }
        finally
        {
            ushort atom = DoAddAtom(lpString);
            if (atom != 0)
                ret.Handle = atom;
        }

        // Do error handling after the atomic region
        if (ret.IsInvalid)
            Interop.ThrowExceptionForLastWin32Error();
        return ret;
    }

    [DllImport("kernel32.dll", EntryPoint = "GetAtomName", 
               CharSet = CharSet.Auto, SetLastError = true)]
    private static extern uint DoGetAtomName(ushort nAtom, 
                       StringBuilder lpBuffer, int nSize);
    internal static string GetAtomName(SafeAtomHandle atom)
    {
        // Atom strings have a maximum size of 255 bytes
        StringBuilder sb = new StringBuilder(255);
        uint ret = 0;

        // Note that mucking around with the SafeHandle reference
        // count is not necessary on CF: we just use GC.KeepAlive
        ret = DoGetAtomName(atom.Handle, sb, 256);
        GC.KeepAlive(atom);
        if (ret == 0)
            Interop.ThrowExceptionForLastWin32Error();

        sb.Length = (int)ret;
        return sb.ToString();
    }
}
#else
[SecurityPermission(SecurityAction.LinkDemand, 
       Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
    [DllImport("kernel32.dll", EntryPoint = 
        "AddAtom", CharSet = CharSet.Auto, BestFitMapping = false,
        ThrowOnUnmappableChar = true, SetLastError = true), SuppressUnmanagedCodeSecurity]
    private static extern ushort DoAddAtom(string lpString);
    internal static SafeAtomHandle AddAtom(string lpString)
    {
        SafeAtomHandle ret = new SafeAtomHandle();

        // Atomically get the native handle and assign it into our return object.
        RuntimeHelpers.PrepareConstrainedRegions();
        try { }
        finally
        {
            ushort atom = DoAddAtom(lpString);
            if (atom != 0)
                ret.Handle = atom;
        }

        // Do error handling after the CER
        if (ret.IsInvalid)
            Interop.ThrowExceptionForLastWin32Error();
        return ret;
    }

    [DllImport("kernel32.dll", EntryPoint = "GetAtomName", 
        CharSet = CharSet.Auto, BestFitMapping = false,
        ThrowOnUnmappableChar = true, SetLastError = true), 
     SuppressUnmanagedCodeSecurity]
    private static extern uint DoGetAtomName(ushort nAtom, 
                   StringBuilder lpBuffer, int nSize);
    internal static string GetAtomName(SafeAtomHandle atom)
    {
        // Atom strings have a maximum size of 255 bytes
        StringBuilder sb = new StringBuilder(255);
        uint ret = 0;
        bool success = false;

        // Atomically increment the SafeHandle reference count,
        // call the native function, and decrement the count
        RuntimeHelpers.PrepareConstrainedRegions();
        try { }
        finally
        {
            atom.DangerousAddRef(ref success);
            if (success)
            {
                ret = DoGetAtomName(atom.Handle, sb, 256);
                atom.DangerousRelease();
            }
        }

        // Do error handling after the CER
        if (!success)
            throw new Exception("SafeHandle.DangerousAddRef failed");
        if (ret == 0)
            Interop.ThrowExceptionForLastWin32Error();

        sb.Length = (int)ret;
        return sb.ToString();
    }
}
#endif

Note that for the "hard" cases, where reference counting has to be done manually anyway, it is easier (and perfectly valid) to do it the Compact Framework way. Here's an alternative implementation of GetAtomName for the full framework:

C#
internal static string GetAtomName(SafeAtomHandle atom)
{
    // Atom strings have a maximum size of 255 bytes
    StringBuilder sb = new StringBuilder(255);
    uint ret = 0;

    // We can use the GC.KeepAlive method instead of mucking around
    // with the SafeHandle reference count on the full framework, too!
    ret = DoGetAtomName(atom.Handle, sb, 256);
    GC.KeepAlive(atom);
    if (ret == 0)
        Interop.ThrowExceptionForLastWin32Error();

    sb.Length = (int)ret;
    return sb.ToString();
}

Again, the LocalAtom Level 1 type is exactly the same.

A Note On SafeHandle's Reference Counting

The Compact Framework SafeHandle defined in this article is actually more akin to the full framework CriticalHandle type. I chose to go with the SafeHandle name anyway so that the Level 0 types are at least mostly portable across frameworks.

SafeHandle's reference counting is the reason that they should be used instead of IntPtr in the full framework P/Invoke declarations: the default marshaler will automatically increment the reference count before the call, and decrement it after. The Compact Dramework SafeHandle's lack of reference counting is the reason a GC.KeepAlive is necessary for Level 0 argument types.

For those who may be wondering: it is possible to use CriticalHandle instead of SafeHandle on the full framework. Since it lacks reference counting, GC.KeepAlive must be called when it is used as an argument. It still does get the special (atomic) treatment when used as a return value, though. If this special treatment is ignored, and IntPtr is used on all P/Invoke declarations, then a CriticalHandle-based Disposable Design System could be fully portable between the compact and full frameworks. This technique could be useful for library writers.

However, I still prefer to use SafeHandle on the full framework code, and keep the full and Compact Framework interop code separated. This takes full advantage of the special interop functionality of SafeHandle, simplifying the interop necessary for the full framework platform.

Constrained Execution Regions vs. Finally Blocks for Atomic Execution

Constrained Execution Regions provide atomic execution, even in the face of a rude abortive shutdown. finally blocks only provide atomic execution with graceful abortive shutdowns.

Any process-wide (i.e., cross-AppDomain) resource management type should be resilient to rude abortive shutdowns (i.e., not leak). On the Compact Framework, this requirement is softened (by necessity) to being resilient to graceful abortive shutdowns. AppDomain-centric resource management types only need to be resilient to orderly shutdowns, but these types of resources are very rare (GCHandle is the only one that comes to mind).

It may be tempting to forego the more complex CERs in favor of simpler finally blocks or no code protection at all, but consider carefully your hosting requirements before you do this. If the code will be used only in the default AppDomain of a Windows Forms/Service/Console application, then you may be able to safely assume that any asynchronous exception (ThreadAbortException, CannotUnloadAppDomainException, StackOverflowException, OutOfMemoryException, etc.) is drastic enough that they will result in process termination. In that case, no atomic regions are necessary at all.

However (and this is especially important if one writes general library code), it is possible to write host-agnostic classes by using the strongest atomic execution regions available. The complex CERs, which are meaningless to a Windows Forms application, become critical if the same code is used in ASP.NET.

References and Further Reading

These following links present the case that some code does need to be protected by CERs. On the Compact Framework, this translates into being protected by finally blocks (or unmanaged code, if rude abortive shutdowns really need to be supported).

  • Chris Brumme, Reliability (2003-06-23). Note that this blog post discusses the pre-2.0 .NET platform, so some of the details no longer apply. However, it is an excellent discussion of the difficulties of reliability. Near the end, he mentions "Perhaps 0.01% of code will be especially hardened using not-yet-available techniques. This code will be used to guarantee that all resources are cleaned up during an AppDomain unload..." - this quote refers to the modern CERs for the full framework and the finally block for the Compact Framework.
  • MSDN, Reliability Best Practices.

Afterword

The usual thanks apply: thanks to Mandy for proofreading (especially with the wedding planning craziness now), and thanks to God for everything!

I plan to cover shutdown scenarios in more detail in my next article, and introduce Level 2 types which support shutdown logic. In all honesty, though, I'm getting married on October 4th and taking the following week off, so don't look for my third article anytime soon. :)

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
Software Developer (Senior)
United States United States
Stephen Cleary is a Christian, husband, father, and programmer living in Northern Michigan.

Personal home page (including blog): http://www.stephencleary.com/

Comments and Discussions

 
GeneralReferences and Further Reading Pin
Ricardo Casquete10-Dec-08 18:32
Ricardo Casquete10-Dec-08 18:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.