代码改变世界

.net 垃圾回收学习[http://www.codeproject.com/KB/dotnet/idisposable.aspx][翻译&&学习][2]

2011-09-18 12:18  一一九九  阅读(252)  评论(0编辑  收藏  举报

From :http://www.codeproject.com/KB/dotnet/idisposable.aspx

注: 非一字一句的翻译,纯属过程中的记录和学习。

A Brief Hiatus - Where We Are

"But, of course, this is all far too messy. It runs counter to the goals for our new managed platform to force developers to worry about this sort of thing." (Chris Brumme, "Lifetime, GC.KeepAlive, handle recycling", blog post 2003-04-19)

<sarcasm strength="mild">Ahh, yes. .NET sure has made it easy. Why, unmanaged destructors in C++ are so much more complicated than all of this.</sarcasm> Seriously, this article could be much longer if it included the really complex issues such as resurrection and the restrictions on finalizers that descend from CriticalFinalizerObject.

I'd like to take a moment to sing some praises of .NET and C#. Although I do disagree with a couple of Microsoft's decisions, on the whole, they've done an excellent job. I'm a huge fan of any language that brings the procedural and functional families together in a more synergetic union, and C# does an excellent job of that. The .NET Framework and runtime have a few rough corners, but overall, they're better than what has come before, and they're obviously the way things are going. So far, I've been pointing out the problems caused by IDisposable, and from this point forward, I'll start looking at solving a couple of these problems.

The fact is, IDisposable is now built-in to .NET languages (though not the runtime), and any solution needs to make use of this interface. We're stuck with it, so to speak, so let's make the best of it.

The (Hopefully) Not-So-Boring Stuff - IDisposable Regulated


微软推荐的的IDisposable模式的比较复杂的一个原因是他们试图覆盖太多的情况了,设计一个IDisposable的类需要遵循以下的原则:

  • 对于每一个非托管的资源,创建一个IDisposable class(也许是Internal),这个类负责资源的释放。微软在BCL的实现中遵循了这条原则。注意,对一个非托管资源的包装类是一个托管资源。
  • 永远不要从一个非托管资源包装类中继承。
  • 创建其他的托管IDisposable类型,这些类型拥有托管资源或者从一个拥有托管资源的类继承而来。
  • 当实现IDisposable的时候,不要同时考虑托管资源和非托管资源的释放问题,这会极大的简化实现,降低可能的错误。

上述原则是基于以下的思想建立的:

  • Level 0 Type直接包装非托管资源。这些类型通常是sealed.
  • Level 1 Type 从Level 1下继承下来或者包含Level 0或者level 1的类型。

详细解释一下这个设计原则。 私有的或者内部的包装了非托管资源的Class应该尽可能的类似原生的API,并且自己不需要关心资源是否正确的释放的问题。所有其他的API应该在那些有Level0 字段的Level1 Class上提供,这会形成两个松耦合的关联类: 一个类仅仅负责包装非托管资源,另外一个类仅仅需要引用托管资源。这会使得我们使用IDisposable的情况变得只有如下两种:

  • Level 0 type: 只需要处理非托管资源
  • Level 1 type:只需要处理托管资源。(通过一个基类型或者字段来定义)。

在Level1上实现IDisposable非常的简单:仅仅调用那些IDisposable的字段的Dispose即可,而且,如果这个类型是从IDisposable继承下来的,调用base.Dispose.  这里不是编写shutdown逻辑的地方。注意一下几点:

  • Dispose可以被安全的调用多次,因为调用IDisposable.Dispose多次是安全的,而且的确是。
  • Level 1 type 不应该包含finalizers; 因为托管代码不能访问到,所以不能做任何事情。
  • 在Dispose的最后不需要调用Gc.KeepAlive(this)。即使当Dispose方法在运行的时候,Gc可能会回收这个对象,由于所有的都被释放掉的资源都是托管的,并且这个类型或者任何自类型都没有finalizer方法,此时没有什么危险想。
  • 调用Gc.suppressfinalize(this)同样是没有必要的的,因为这个类型或者所有的自类型都没有finalizer.

However, IDisposable is still difficult to implement correctly for the first use case. Due to the complexities of properly implementing IDisposable for unmanaged resources, it's actually best if we don't implement it altogether. This can be accomplished through the diligent use of base types that handle the common logic, or through the use of helper classes that often remove the need for IDisposable.

Solving IDisposable's Difficulties - Helper Classes for Avoiding Implementing IDisposable Directly


注: 看不大明白。

One reason for the complexity of Microsoft's recommended IDisposable code pattern is because they try to cover too many use cases. A bit of discipline in the design of IDisposable classes will go a long way:

  • For each unmanaged resource, create exactly one (possibly internal) IDisposable class that is responsible for freeing it. Microsoft followed this principle thoroughly in the BCL implementation. Note that a wrapper type for an unmanaged resource is considered a managed resource.
  • Never derive from an unmanaged resource wrapper type.
  • Create other managed IDisposable types that either own managed resources and/or derive from a type that owns managed resources.
  • Under no circumstances create a type that has to consider both managed and unmanaged resources, when implementing IDisposable. This greatly simplifies the implementation, reducing possible errors.

The Disposable Design Principle is built on these ideas:

  • Level 0 types directly wrap unmanaged resources. These types are generally sealed.
  • Level 1 types are types that derive from Level 1 types and/or contain field members that are Level 0 or Level 1 types.

To expound on this design principle, the small private or internal Level 0 classes that wrap unmanaged resources should be as close to the native API as possible, and should only concern themselves with disposing the resource correctly. All other APIs should be provided in a Level 1 class that has a Level 0 field member. This would result in two loosely-related classes (or class hierarchies): one is only responsible for wrapping the unmanaged resource, and the other only has to refer to a managed resource. This reduces our use cases for IDisposable to only two:

  1. Level 0 types: only deal with unmanaged resources.
  2. Level 1 types: only deal with managed resources (defined by a base type and/or in fields).

Implementing IDisposable on Level 1 types is rather simple: just implement IDisposable.Dispose as calling Dispose on any IDisposable field, and then, if this type is derived from an IDisposable type, call base.Dispose. This is not the place for general shutdown logic. Note the following for this simple implementation:

  • Dispose is safe to be called multiple times because it is safe to call IDisposable.Dispose multiple times, and that's all it does.
  • Level 1 type should not have finalizers; they wouldn't be able to do anything anyway, since managed code cannot be accessed.
  • It is not necessary to call GC.KeepAlive(this) at the end of Dispose. Even though it is possible for the garbage collector to collect this object while Dispose is still running, this is not dangerous since all the resources being disposed are managed, and neither this type nor any derived types have finalizers.
  • Calling GC.SuppressFinalize(this) is likewise unnecessary because neither this type nor any derived types have finalizers.

However, IDisposable is still difficult to implement correctly for the first use case. Due to the complexities of properly implementing IDisposable for unmanaged resources, it's actually best if we don't implement it altogether. This can be accomplished through the diligent use of base types that handle the common logic, or through the use of helper classes that often remove the need for IDisposable.

Solving IDisposable's Difficulties - Helper Classes for Avoiding Implementing IDisposable Directly

It is very common to have to write an unmanaged resource wrapper class for an unmanaged resource that is a pointer to some data structure. For this common use case, a higher-level abstraction is available through Microsoft-provided helper classes. System.Runtime.InteropServices.SafeHandle, System.Runtime.InteropServices.CriticalHandle, and the classes in Microsoft.Win32.SafeHandles allow writing very simple unmanaged resource wrappers if the unmanaged resource may be treated as an IntPtr. However, these are not supported on the .NET Compact Framework; on that platform, I recommend writing your own version of these extremely useful classes.

Level 0 types, in the Disposable Design Principle, should always derive from SafeHandle, if it is available on the target platform. SafeHandle and its derived classes have special P/Invoke support, which helps prevent leaking resources in some rare situations. Interop code should define function parameters, and return types as SafeHandle (or derived types) rather than IntPtr. The CriticalHandle class, in spite of the name, is actually less safe to use than SafeHandle, and should generally be avoided.

The relationship between SafeWaitHandle and WaitHandle is a perfect example of the Disposable Design Principle: SafeWaitHandle is the Level 0 class, and WaitHandle is the Level 1 class that provides the normal end-user API. SafeWaitHandle is in the SafeHandle hierarchy, implementing SafeHandle.ReleaseHandle as a call to the Win32 CloseHandle function; it only concerns itself with how to free the resource. The Level 1 WaitHandle class, in contrast, is not in the SafeHandle hierarchy; and its hierarchy exposes a full API for waitable handles, such as WaitOne.

This means there are four possibilities when having to write a new unmanaged resource wrapper (in the order of ease of implementation):

  1. There is already a Level 0 type for the unmanaged resource. In other words, the unmanaged resource is a pointer type that is already covered by a class derived from SafeHandle. Microsoft has supplied several classes already, including SafeFileHandle, SafePipeHandle, and SafeWaitHandle, among others. In this case, the programmer only needs to create a new Level 1 type.
  2. The unmanaged resource is a pointer type, but doesn't have a suitable Level 0 type already defined. In this case, the programmer needs to create two classes, one Level 0 and one Level 1.
  3. The unmanaged resource that needs wrapping is a simple pointer type along with some additional information (such as a secondary pointer or integral "context" value). In this case, the programmer must also create two classes, but the implementation details of the Level 0 type are more complex.
  4. The unmanaged resource is not a pointer type at all. In this case, the programmer must create two classes, and the implementation details of both are much more complex.

Note that when creating hierarchies of Level 1 types, it is common practice to declare a protected property in the (possibly abstract) base Level 1 type, and this field should have the type and name of the related Level 0 type. For example, the Level 1 abstract base type WaitHandle establishes the Level 1 hierarchy for waitable handles, and it has a protected property named SafeWaitHandle of type SafeWaitHandle.

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

To define a new Level 1 type that uses a Level 0 type, extend an existing Level 1 hierarchy, if possible.

The example for using existing Level 0 (SafeHandle-derived) types is ManualResetTimer (named to match the existing ManualResetEvent). Of the many timers provided by the .NET framework, they did not include a WaitHandle-based timer that gets signalled when the timer goes off. This "Waitable Timer", as it is called by the SDK, is commonly used by asynchronous programs. For simplicity, this sample does not support periodic timers or timers with asynchronous callback functions.

Note that ManualResetTimer derives from WaitHandle (the Level 1 hierarchy) because the Level 0 SafeWaitHandle already correctly disposes of the unmanaged resource. Because of the Level 0/Level 1 class hierarchy division already in place, implementing ManualResetTimer is quite straightforward.

Collapse | Copy Code

[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 SafeWaitHandle CreateWaitableTimer(IntPtr lpTimerAttributes, 
             bool bManualReset, string lpTimerName)
    {
        SafeWaitHandle ret = DoCreateWaitableTimer(lpTimerAttributes, 
                             bManualReset, lpTimerName);
        if (ret.IsInvalid)
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
        return ret;
    }

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

    [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(SafeWaitHandle hTimer, long pDueTime, 
             int lPeriod, IntPtr pfnCompletionRoutine,
             IntPtr lpArgToCompletionRoutine, bool fResume)
    {
        if (!DoSetWaitableTimer(hTimer, ref pDueTime, lPeriod, 
                 pfnCompletionRoutine, lpArgToCompletionRoutine, fResume))
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
    }
}

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

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

    /// <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 <see cref="FILETIME"/> value</param>
    private void Set(long dueTime)
    {
        NativeMethods.SetWaitableTimer(SafeWaitHandle, dueTime, 0, 
                                       IntPtr.Zero, IntPtr.Zero, false);
    }

    /// <summary>
    /// Sets the timer to signal at the specified time. Resets the signalled 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); }
}

Note the following:

  • Always use SafeHandle or derived types as parameters and return values for interop functions. For example, this sample code uses SafeWaitHandle instead of IntPtr. This prevents resource leaks if a thread is unexpectedly aborted.
  • Since a Level 1 hierarchy is already in place, ManualResetTimer doesn't have to deal with disposing, even of its managed resources. This is all handled by the WaitHandle base type.
Wrapping Unmanaged Resources - Defining Level 0 Types for Pointers (The Intermediate Case)

There are many cases where a suitable Level 0 type doesn't exist. These situations require defining a Level 0 type and then defining a Level 1 type (or type hierarchy). Defining Level 0 types is more complicated than defining Level 1 types.

The example for defining simple Level 0 types is a window station object. This is one of the many resources that is represented by a single IntPtr handle. First, the Level 0 type must be defined:

Collapse | Copy Code

[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);
}

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

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

Notes on the code:

  • The unmanaged resource deallocation function (in this case, NativeMethods.CloseWindowStation) does take a regular IntPtr (not a SafeWindowStationHandle) to deallocate the resource.
  • Since SafeHandle derives from CriticalFinalizerObject, both IsInvalid and ReleaseHandle may be run in a Constrained Execution Region, meaning:
    • They cannot allocate objects, box values, acquire locks, or call methods through delegates, function pointers, or Reflection.
    • They should be decorated with a ReliabilityContractAttribute and a PrePrepareMethodAttribute.
  • Both IsInvalid and ReleaseHandle may be run from a finalizer during system shutdown, so they may not access any managed objects whatsoever.

Since a Level 0 type's ReleaseHandle only P/Invokes its resource cleanup function and returns, the Constrained Execution Region and finalizer restraints are not troublesome in practice. The only awkwardness is in the additional attributes that are necessary.

Once the Level 0 type is completed, then the Level 1 type may be defined:

Collapse | Copy Code

[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)
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
        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))
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
    }
}

/// <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 unmanaged native methods now all use SafeWindowStationHandle for their return values and arguments, rather than IntPtr. Only the resource deallocation function is passed an IntPtr.
  • For simplicity, NativeMethods.OpenWindowStation takes a uint as its desired access mask, rather than a proper enumeration. A real enumeration should be used in production code.
  • The implementation of IDisposable.Dispose is straightforward: dispose of the underlying handle.
  • A finalizer is not necessary because SafeWindowStationHandle has its own finalizer (inherited from SafeHandle) which will dispose of the underlying handle.

Since the window station is a simple example, there is only one Level 1 class rather than a hierarchy of Level 1 classes. To define a hierarchy, the following code pattern should be used:

Collapse | Copy Code

/// <summary>
/// A base class for window station types.
/// </summary>
public abstract class WindowStationBase : IDisposable
{
    /// <summary>
    /// The underlying window station handle.
    /// </summary>
    protected SafeWindowStationHandle SafeWindowStationHandle { get; set; }

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

    /// <summary>
    /// Disposes managed resources in this class and derived classes.
    /// When overriding this in a derived class,
    /// be sure to call base.DisposeManagedResources()
    /// </summary>
    protected virtual void DisposeManagedResources()
    {
        SafeWindowStationHandle.Dispose();
    }
}

/// <summary>
/// A window station.
/// </summary>
public sealed class WindowStation : WindowStationBase
{
    /// <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:

  • SafeWindowStationHandle is now a protected property. This should be a set by derived classes, usually in their constructors. Note that this may also be a public property (e.g. Microsoft chose to make WaitHandle.SafeWaitHandle public); however, I believe protected is the better choice.
  • When implementing IDisposable in the base class, I assume the Disposable Design Principle instead of using Microsoft's IDisposable code pattern. As a result:
    • Types derived from WindowStationBase may not directly own unmanaged resources, i.e., they must be Level 1 types. Note that they may own Level 0 types, which may own unmanaged resources; they just can't be Level 0 types themselves.
    • There is no need for WindowStationBase (or any derived type) to have a finalizer. Implementing Microsoft's IDisposable code pattern requires a finalizer.
    • I chose to name the resource disposing function DisposeManagedResources, which is logically equivalent to the Dispose(true) of Microsoft's IDisposable code pattern.
Wrapping Unmanaged Resources - Defining Level 0 Types for Pointers with Context Data (The Advanced Case)

Sometimes an unmanaged API requires additional context information in order to deallocate a resource. This requires a Level 0 type that has some additional information attached to it, and this always requires more complex interop code.

The example for defining advanced Level 0 types is allocating memory in the context of another process. The other process' handle needs to be associated with the allocated memory, and it needs to be passed to the deallocation function. First, the Level 0 type:

Collapse | Copy Code

[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);
}

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

        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
        private set;
    }
    private bool ReleaseSafeProcessHandle;

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

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

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

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

Notes:

  • This is very similar to the Level 0 type defined earlier; only this class also keeps a SafeHandle reference to the remote process, which must be passed to VirtualFreeEx.
  • A Level 0 type may contain a reference to another Level 0 type (in this example, SafeRemoteMemoryHandle has a field of type SafeHandle). However, it must explicitly control the field's reference count, which requires an additional boolean field (ReleaseSafeProcessHandle).
  • The process handle is held as a SafeHandle, not an IntPtr. This is because SafeHandle internally implements reference counting to prevent premature deallocation. This is useful both while being held as a field in SafeRemoteMemoryHandle and being passed to VirtualFreeEx.
  • Since SafeProcessHandle may be accessed during CERs, its accessors need the ReliabilityContract and PrePrepareMethod attributes.
  • There is also an additional method, SafeRemoteMemoryHandle.SetHandle, which is designed to execute within a Constrained Execution Region, so it can atomically set both the remote process handle and the unmanaged handle together.
  • Once again, proper enumerations are skipped for simplicity.
  • Also, a more proper handling of the remote process handle would require defining a SafeProcessHandle, and using that in place of the SafeHandle in this sample. This sample has completely correct behavior, but does not provide full type safety.

The Level 1 type reveals the additional complexity needed for creating SafeRemoteMemoryHandle objects:

Collapse | Copy Code

[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();
        bool success = false;

        // 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, ref success);
            if (!success)
                ret.Dispose();
        }

        // Do error handling after the CER
        if (!success)
            throw new Exception("Failed to set handle value");
        if (ret.IsInvalid)
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
        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))
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
        if (nSize != NumberOfBytesWritten)
            throw new Exception
		("WriteProcessMemory: Failed to write all bytes requested");
    }
}

/// <summary>
/// Memory allocated in another process.
/// </summary>
public sealed class RemoteMemory : IDisposable
{
    /// <summary>
    /// The underlying remote memory handle.
    /// </summary>
    private SafeRemoteMemoryHandle SafeRemoteMemoryHandle;

    /// <summary>
    /// The associated process handle.
    /// </summary>
    public SafeHandle SafeProcessHandle 
           { get { return SafeRemoteMemoryHandle.SafeProcessHandle; } }

    /// <summary>
    /// Implementation of IDisposable: closes the underlying remote memory handle.
    /// </summary>
    public void Dispose()
    {
        SafeRemoteMemoryHandle.Dispose();
    }

    /// <summary>
    /// Allocates memory from another process.
    /// </summary>
    public RemoteMemory(SafeHandle process, UIntPtr size)
    {
        // ("0x3000" is MEM_COMMIT | MEM_RESERVE)
        // ("0x04" is PAGE_READWRITE)
        SafeRemoteMemoryHandle = 
          NativeMethods.VirtualAllocEx(process, IntPtr.Zero, size, 0x3000, 0x04);
    }

    /// <summary>
    /// Writes to memory in another process.
    /// Note: at least <paramref name="size"/> bytes starting
    /// at <paramref name="buffer"/> must be pinned in memory.
    /// </summary>
    public void Write(IntPtr buffer, UIntPtr size)
    {
        NativeMethods.WriteProcessMemory(SafeRemoteMemoryHandle, buffer, size);
    }
}

Notes:

  • The first thing that should stand out is how much more complicated the allocation function is. NativeMethods.VirtualAllocEx is designed to partially run within an explicit Constrained Execution Region. Specifically:
    • It does all the necessary allocations before the CER. In this example, it only needs to allocate the returned SafeRemoteMemoryHandle object.
    • The call to RuntimeHelpers.PrepareConstrainedRegions followed by the empty try block is the way of declaring the finally block to be an explicit Constrained Execution Region. See MSDN for more details on this method.
    • It performs error checking, including throwing exceptions (which may allocate memory) after the CER.
  • The CER provides atomic execution: It guarantees that the IntPtr returned from the unmanaged VirtualAllocEx is wrapped in a SafeRemoteMemoryHandle object, even in the presence of asynchronous exceptions (e.g., if Thread.Abort is called on a thread in a CER, the CLR will wait until the CER is completed before asynchronously raising the ThreadAbortException).
  • CERs were not necessary for the simpler examples because SafeHandle is treated specially when returned from an unmanaged function: the returned value (actually an IntPtr) is used to construct a new SafeHandle atomically. In other words, the CLR supports this behavior for SafeHandle automatically, but now we have to force the same behavior using CERs.
  • Another important note is that the interop code should continue to reference the Level 0 type (e.g., SafeRemoteMemoryHandle) instead of just an IntPtr; this keeps SafeHandle's reference counting involved. Passing the context data (e.g., SafeHandle or SafeProcessHandle) along with a plain IntPtr would be incorrect.
  • The RemoteMemory Level 1 type does expose the additional context property (as RemoteMemory.SafeProcessHandle). This is not required, but often useful.

Notes on how this example is simplified:

  • For simplicity, this example only provides a single Level 1 class instead of a class hierarchy. See the previous example for an example of the Level 1 hierarchy pattern.
  • Again, the process SafeHandle should really be a SafeProcessHandle, and proper enumerations have been omitted.
  • This sample also does not expose a very user-friendly API; it should include both reading and writing at various offsets, and should accept byte arrays instead of pre-pinned memory.
  • Exceptions of type Exception should not be thrown directly; this should be of a more specific type.
Wrapping Unmanaged Resources - Defining Level 0 Types for Non-Pointer Data (The Hard Case)

There are a handful of unmanaged APIs whose handle types are not pointers. Each of these handle types may either be converted to an IntPtr (if they are smaller or equal to the IntPtr type) or treated as additional context data for a fake IntPtr.

The example for non-pointer Level 0 types is the local atom table. There is no real reason to use this antiquated API in a modern program, but this example will illustrate how to handle APIs of this nature. The ATOM type is an unsigned 16-bit integer, and for illustration purposes, the sample is implemented twice: once widening the ushort to IntPtr, and the other treating the ushort as context data for a fake IntPtr.

First, the Level 0 type for atoms, storing the ushort unmanaged handle value inside the IntPtr SafeHandle.handle field:

Collapse | Copy Code

[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);
}

/// <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
    {
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
        get
        {
            return unchecked((ushort)(short)handle);
        }

        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
        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
    {
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
        get { return (Handle == 0); }
    }

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

The only difference of note is the addition of the Handle property, which provides access to the handle, treating it as a ushort. Note the necessity of the ReliabilityContract and PrePrepareMethod attributes on the property accessors. The IsInvalid and ReleaseHandle implementations use Handle instead of handle for ease of implementation.

The additional complexity comes into play with the interop code used with the Level 1 class:

Collapse | Copy Code

[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)
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
        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)
            Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

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

/// <summary>
/// Atom in the local atom table.
/// </summary>
public sealed class LocalAtom : IDisposable
{
    /// <summary>
    /// The underlying atom handle.
    /// </summary>
    private SafeAtomHandle SafeAtomHandle;

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

    /// <summary>
    /// Adds a string to the atom table, setting this local atom to point to it.
    /// </summary>
    public void Add(string name)
    {
        SafeAtomHandle = NativeMethods.AddAtom(name);
    }

    public string Name
    {
        get
        {
            return NativeMethods.GetAtomName(SafeAtomHandle);
        }
    }
}

The primary difference between this example and the last one is the need for CERs in every single interop call. The automatic reference counting from SafeHandle is no longer automatic, so it must be done by hand. Every time the underlying unmanaged handle needs to be passed to an unmanaged function, the example of NativeMethods.GetAtomName should be followed:

  1. Initialize return values (in this case, a return buffer) and any error condition variables.
  2. Use a CER to atomically increment the SafeHandle reference count, call the unmanaged function, and decrement the SafeHandle count. Note that incrementing the SafeHandle reference count may fail, which should abort the call. [Alternatively, the incrementing and unmanaged function call may be placed within the try block, but the decrementing must remain in the finally block.]
  3. Perform all error testing: both the SafeHandle increment as well as the unmanaged function result must be considered. Remember that throwing Exception is not recommended in production code; a more specific type should be selected instead.

The second implementation (using context values instead of casting to/from IntPtr) may be chosen if the casting would be awkward, or if the unmanaged handle type won't fit into a single IntPtr field. It is possible to make the SafeHandle.handle field almost meaningless by only assigning it 0 (for invalid handle values) or -1 (indicating the handle - including the context values - is valid):

Collapse | Copy Code

/// <summary>
/// Level 0 type for local atoms (context implementation).
/// </summary>
public sealed class SafeAtomHandle : SafeHandle
{
    public ushort Handle
    {
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
        get;

        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
        private set;
    }

    /// <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
    {
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
        [PrePrepareMethod]
        get { return (handle == IntPtr.Zero); }
    }

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

    /// <summary>
    /// Overwrites the handle value (without releasing it).
    /// This should only be called from functions acting as constructors.
    /// </summary>
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
    [PrePrepareMethod]
    internal void SetHandle(ushort handle_)
    {
        Handle = handle_;
        handle = (IntPtr)(-1);
    }
}

Notes:

  • The Handle property is now a context, stored separately from handle.
  • The IsInvalid property tests the handle, which now only has values of 0 or -1, but the ReleaseHandle method still uses Handle for convenience.
  • The Handle setter has been replaced by the SetHandle method. This is done in the example to reflect the fact that most of the time contexts are used, SetHandle will need to take more than one argument.

The only change necessary in the rest of the example is the change in how the handles are set in the NativeMethods.AddAtom constructor method:

Collapse | Copy Code

ret.Handle = atom;

should be:

Collapse | Copy Code

ret.SetHandle(atom);

Remember that in a real-world situation, SetHandle would be taking more than one argument.

Summary

To summarize, prefer using the Disposable Design Principle. The DDP 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. 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 derive from SafeHandle (which derives from CriticalFinalizerObject).
      • For finally blocks, this means that Level 0 types must derive from a separately-defined SafeHandle 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).
    4. Level 0 types may refer to other Level 0 types, but must increment the count of the referred-to object as long as the reference is needed.
  2. 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.

Using the Disposable Design Principle (instead of Microsoft's IDisposable code pattern) will make software more reliable and easier to use.

References and Further Reading

Afterword

In a future article, I hope to address one further drawback to IDisposable: the lack of support for shutdown logic; and provide a (partial) solution. This was originally intended to be part of this article, but it's already too long. I also hope to look at the SafeHandle alternatives for the .NET Compact Framework, which sadly does not support SafeHandle or Constrained Execution Regions.

I'd like to thank my loving almost-wife Mandy Snell, for patiently proofreading this article. On October 4th, 2008, she will officially become Mandy Cleary. :) I also must state that everything good in my life comes from Jesus Christ; He is the source of all wisdom, and I thank Him for all His gifts. "For God giveth to a man that is good in his sight wisdom, and knowledge, and joy" (Ecc. 2:26).

History

  • 2008-09-27 - Fixed bug in the advanced sample, rewrote the summary of the DDP, and added the reference to Microsoft's rationale to not support reference counting
  • 2008-09-22 - Added the References and History sections
  • 2008-09-21 - Initial publication

License

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

About the Author

Stephen Cleary

Software Developer (Senior)
Nito Programs
United States United States
Member

Stephen Cleary is a computer programmer living in Northern Michigan. He enjoys reading and is actively involved in his church. On October 4th, 2008, he married Amanda Snell, and he is happy about that. Smile | :)
Programming blog: http://nitoprograms.blogspot.com/
(including the TCP/IP .NET Sockets FAQ: http://nitoprograms.blogspot.com/2009/04/tcpip-net-sockets-faq.html )
CodePlex project leads on:
Nito Async Library: http://nitoasync.codeplex.com/
Nito MVVM Library: http://nitomvvm.codeplex.com/
Personal home page: http://www.stephencleary.com/