65.9K
CodeProject 正在变化。 阅读更多。
Home

IDisposable:也适用于 CF 开发人员

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (5投票s)

2008 年 9 月 24 日

BSD

13分钟阅读

viewsIcon

22320

在 .NET Compact Framework 上实现可处置设计模式。

引言

在我第一篇文章中,我介绍了可处置设计模式。这是微软在 .NET 2.0 中采用的模型,取代了旧的IDisposable 代码模式(实际上,微软是以向后兼容的方式完成的,而我推荐完全重写)。但是,为了支持 .NET Compact Framework(微软并未将其迁移到可处置设计模式),需要进行一些更改。

简要了解关闭

在运行框架代码时,可能发生三种类型的关闭:

  • 有序关闭 - 当向正常运行的代码发出关闭请求时,例如 Application.Exit
  • 优雅的强制关闭 - 当卸载 AppDomain 时:所有线程都会被中止,然后运行所有析构函数。
  • 粗暴的强制关闭 - 当粗暴卸载 AppDomain 时:所有线程(语义与优雅的强制关闭不同)都会被中止,然后运行析构函数。

我的下一篇文章将详细介绍关闭情况以及何时会发生。就本文而言,只需了解以下事实:

  • 在优雅的强制关闭期间中止线程时,运行时会等待线程退出受限执行区域、非托管代码或 finally 块。
  • 在粗暴的强制关闭期间中止线程时,运行时会等待线程退出受限执行区域或非托管代码,但不会等待普通 finally 块。

回顾可处置设计模式

这是与我第一篇文章中相同的可处置设计模式描述,除了我更改了一些措辞以将其应用于 Compact Framework(并添加了一条注释,说明 Level 0 类型构造函数必须在原子执行区域内调用)。

可处置设计模式将资源管理职责分为 Level 0 类型(处理非托管资源)和 Level 1 类型(仍然是小型包装类,与本机 API 非常相似,但仅处理托管资源)。

  1. “原子执行区域”定义为以下之一(按从最强保证到最弱的顺序):
    1. 非托管代码 - 在这种情况下,可处置设计模式为任何类型的关闭提供了无泄漏保证。
    2. 受限执行区域 - 在这种情况下,可处置设计模式为任何类型的关闭提供了无泄漏保证。
    3. finally 块 - 在这种情况下,可处置设计模式为有序关闭和优雅的强制关闭提供了无泄漏保证,但在粗暴的强制关闭时可能会发生泄漏。
    4. 普通托管代码 - 在这种情况下,可处置设计模式为有序关闭提供了无泄漏保证,但在优雅的强制关闭或粗暴的强制关闭时可能会发生泄漏。
  2. Level 0 类型直接包装非托管资源,并且仅关注其资源的释放。
    1. Level 0 类型要么是 abstract 的,要么是 sealed 的。
    2. Level 0 类型必须设计成在原子执行区域内完全执行。
      • 对于受限执行区域,这意味着 Level 0 类型必须是 CriticalFinalizerObject 的子类。
      • 对于 finally 块,这意味着 Level 0 类型必须继承自一个单独定义的基类型,该基类型实现了 IDisposable 以显式释放非托管资源(可能在 finally 块的上下文中调用)或从析构函数中释放。
    3. Level 0 类型的构造函数必须在原子执行区域内调用。
      • SafeHandle 返回值的特殊全框架互操作处理被认为是 것입니다(因此具有最强的保证)。
  3. Level 1 类型仅处理托管资源。
    1. Level 1 类型通常是 sealed 的,除非它们定义了一个 Level 1 层级的基础 Level 1 类型。
    2. Level 1 类型继承自 Level 1 类型或直接继承自 IDisposable;它们不继承自 CriticalFinalizerObject 或 Level 0 类型。
    3. Level 1 类型可以包含 Level 0 或 Level 1 类型的字段。
    4. Level 1 类型通过调用其所有 Level 0 和 Level 1 字段的 Dispose 方法,然后调用 base.Dispose(如果适用)来实现 IDisposable.Dispose
    5. Level 1 类型没有析构函数。
    6. 在定义 Level 1 类型层级时,abstract 的根基类型应定义一个 protected 属性,其名称和类型与关联的 Level 0 类型相同。

在未来的文章中,我将介绍 Level 2 类型,这些类型允许运行(几乎)任意的关闭代码,但对最终用户提出了额外的要求。

.NET Compact Framework 限制

在 .NET Compact Framework 上开发 Level 0 类型的主要困难是缺少 SafeHandle。本文将提供一个 Compact Framework 版本的 SafeHandle,但它在互操作场景中使用时需要特殊的额外代码。由于 Compact Framework 和 Full Framework 在互操作支持方面的差异(例如,DllImportAttribute.BestFitMappingSuppressUnmanagedCodeSecurityAttribute 等),通常最好不要在两个平台之间共享任何互操作代码。

另一个困难是缺少受限执行区域,尽管事实证明,这个困难实际上是塞翁失马。

简化假设

由于 Compact Framework 上没有受限执行区域,因此无法编写可以处理粗暴强制关闭的托管 Level 0 类型。

.NET Compact Framework 缺少受限执行区域,这使得编码人员面临以下两种可能性之一:

  1. 为每个非托管资源分配函数编写一个非托管包装函数,该函数可以直接操作 SafeHandle(或派生类型)实现。或者,可以编写一个非托管包装函数,接受一个委托(作为函数指针),但这会引入额外的开销,并且无法处理非 IntPtr 的非托管资源类型。
  2. 假设在紧凑平台上,如果发生粗暴的强制关闭,则整个进程将退出。

本文采纳了简化假设。在紧凑平台上,大多数 .NET 进程只有一个 AppDomain,因此此假设成立。本文开发的 Level 0 类型将支持优雅的强制关闭,因此它们也支持进程内的多个 AppDomain前提是它们的关闭始终是优雅的。

我第一篇文章中的 Level 0 类型通过使用受限执行区域支持粗暴的强制关闭。本文中的 Level 0 类型将通过使用 finally 块支持优雅的强制关闭。

.NET Compact Framework 的最小 SafeHandle

以下代码是一个简化的 SafeHandle

  • 没有引用计数,因此 Compact Framework 互操作代码必须正确使用 GC.KeepAlive 来进行补偿。
  • SafeHandle 实例始终拥有其句柄。
  • 句柄没有单独的“已关闭”状态;当 SafeHandle 被销毁时,句柄将被关闭。
// .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

注释

  • DOTNET_ISCF 是一个个人约定,表示正在为 Compact Framework 编译。
  • 该类型位于 System.Runtime.InteropServices 命名空间中。其他人可能不同意这个决定;我这样做只是为了能够尽可能多地在紧凑和常规框架平台之间共享代码。
  • 该类型确实需要跟踪它是否已被处置,以允许多次调用 Dispose 而不会产生副作用。
  • GC.SuppressFinalizeGC.KeepAlive 可防止在调用 Dispose(或将要调用)时运行析构函数。因此,当析构函数运行时,它会知道 Dispose 从未被调用过,也永远不会被调用。
  • IsInvalidReleaseHandle 不在受限执行区域内运行。但是,由于它们可能在析构函数上下文运行,因此许多相同的限制适用。例如,它们不应引发异常或访问任何托管对象。

现在可以使用此简化的 SafeHandle 来定义 Level 1 类型。在此之前,有几个辅助函数可以帮助在 Compact Framework 上进行 P/Invoke。

/// <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
}

包装非托管资源 - 使用现有的“Level 0”类型(简单情况)

不幸的是,在 Compact Framework 上,WaitHandle 不是真正的 Level 1 类型。但是,通过派生自它,我们仍然可以创建一个 ManualResetTimer,其行为类似于 Level 1 类型。

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

注释

  • Full Framework 的互操作代码已修改,使其与 Compact Framework 的互操作代码具有相同的方法签名。这使得可以为两个平台编写一个 ManualResetTimer
  • 对于资源分配函数(NativeMethods.CreateWaitableTimer),使用了 finally 块来原子地分配资源并将其分配给 Level 0 类型中的句柄值(请记住,在 Compact Framework 上,WaitHandle 不遵循可处置设计模式,因此它具有 Level 0 类型和 Level 1 类型的一些方面)。分配错误在 finally 块之后处理;这不是必需的,但这是良好的编程实践。
  • 对于接受资源句柄作为参数的互操作函数,使用 GC.KeepAlive 来确保 WaitHandle 在被非托管代码使用时不会被析构函数处置。同样,错误处理被推迟到 GC.KeepAlive 之后;这不是必需的,但这是良好的实践。

由于 WaitHandle 在 Compact Framework 上不是 Level 1 类型,因此此示例更像是一个特殊情况。以下示例产生了真正的 Level 1 类型。

包装非托管资源 - 为指针定义 Level 0 类型(中级情况)

Level 0 类型是第一篇文章示例的直接翻译。

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

除了移除 Code Access Security 和 Constrained Execution Region 属性(这些属性在 Compact Framework 上不受支持)之外,没有重大更改。

Level 1 类型展示了 Compact Framework 互操作调用所需的模式。

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

注释

  • 资源分配函数遵循一个现在应该熟悉的模式:
    1. 分配返回对象(初始化为无效句柄值)。
    2. finally 块中,进行分配,并原子地将其分配给返回对象。
    3. 在原子区域之后进行错误检查。
  • 同样,接受 Level 0 类型参数的函数遵循类似的模式:
    1. 调用本机函数,但尚未处理错误。
    2. 调用 GC.KeepAlive 以确保 Level 0 类型在使用非托管代码时不会被垃圾回收。
    3. 执行错误处理。

包装非托管资源 - 为带有上下文数据的指针定义 Level 0 类型(“高级”情况)

这一次,Level 0 资源释放函数确实有一个小技巧:

#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;
    }
}

内存的资源释放函数以进程句柄(请记住,这实际上应该是 SafeProcessHandle 而不是 SafeHandle)作为参数。因为它接受一个安全句柄作为参数,所以它必须赋予它特殊的 GC.KeepAlive 处理。

Level 1 类型的互操作代码具有逻辑上的更改。

#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

唯一值得注意的方面是 Compact Framework NativeMethods.VirtualAllocEx 不必调用进程句柄上的 GC.KeepAlive,因为它被 SafeRemoteMemoryHandle 所包含,而 SafeRemoteMemoryHandle 正在保持活动状态。

RemoteMemory Level 1 类型本身与上一篇文章相同。

包装非托管资源 - 为非指针数据定义 Level 0 类型(“困难”情况)

Level 0 类型完全简单明了。

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

最后,对于 Compact Framework 来说,Level 1 互操作实际上更简单;这里没有意外。

#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

请注意,对于“困难”情况,其中引用计数必须手动完成,以 Compact Framework 的方式进行(并且完全有效)更容易。以下是 Full Framework 的 GetAtomName 的替代实现:

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

同样,LocalAtom Level 1 类型完全相同。

关于 SafeHandle 引用计数的说明

本文定义的 Compact Framework SafeHandle 实际上更类似于 Full Framework 的 CriticalHandle 类型。我仍然选择使用 SafeHandle 这个名字,以便 Level 0 类型至少在框架之间具有大部分可移植性。

SafeHandle 的引用计数是它们应该在 Full Framework P/Invoke 声明中使用的原因,而不是 IntPtr:默认封送器会在调用前自动增加引用计数,在调用后减少它。Compact Framework SafeHandle 缺乏引用计数是为什么 Level 0 参数类型需要 GC.KeepAlive 的原因。

对于可能在想的人:可以在 Full Framework 上使用 CriticalHandle 而不是 SafeHandle。由于它缺乏引用计数,因此在将其用作参数时必须调用 GC.KeepAlive。不过,当它用作返回值时,它仍然会获得特殊的(原子)处理。如果忽略了这种特殊处理,并在所有 P/Invoke 声明中使用 IntPtr,那么基于 CriticalHandle 的可处置设计系统可以在紧凑和 Full Framework 之间完全移植。这种技术对于库作者来说可能很有用。

但是,我仍然倾向于在 Full Framework 代码中使用 SafeHandle,并保持 Full Framework 和 Compact Framework 互操作代码的分离。这充分利用了 SafeHandle 的特殊互操作功能,简化了 Full Framework 平台所需的互操作。

受限执行区域与 finally 块的原子执行

受限执行区域在面对粗暴强制关闭时提供原子执行。 finally 块仅在优雅的强制关闭时提供原子执行。

任何进程范围的(即跨 AppDomain)资源管理类型都应能抵抗粗暴强制关闭(即不泄漏)。在 Compact Framework 上,由于必要性,此要求已放宽为能抵抗优雅的强制关闭。AppDomain 范围的资源管理类型只需要能抵抗有序关闭,但这些类型的资源非常罕见(GCHandle 是我能想到的唯一一个)。

可能会诱使人们放弃更复杂的 CER,而选择更简单的 finally 块或根本不进行代码保护,但在这样做之前,请仔细考虑您的托管要求。如果代码仅在 Windows Forms/Service/Console 应用程序的默认 AppDomain 中使用,那么您可以安全地假设任何异步异常(ThreadAbortExceptionCannotUnloadAppDomainExceptionStackOverflowExceptionOutOfMemoryException 等)都足够严重,会导致进程终止。在这种情况下,根本不需要原子区域。

但是(这一点尤其重要,如果您编写通用库代码),通过使用最强的原子执行区域,可以编写主机无关的类。对于 Windows Forms 应用程序毫无意义的复杂 CER,在将相同代码用于 ASP.NET 时变得至关重要。

参考文献与延伸阅读

以下链接提出了某些代码需要由 CERs 保护的论点。在 Compact Framework 上,这相当于受到 finally 块(或非托管代码,如果确实需要支持粗暴强制关闭)的保护。

  • Chris Brumme,Reliability (2003-06-23)。请注意,这篇博文讨论的是 2.0 之前的 .NET 平台,因此一些细节不再适用。但是,它对可靠性的困难进行了出色的讨论。在接近结尾时,他提到了“也许 0.01% 的代码将使用尚未提供的技术进行特殊加固。这些代码将用于确保在 AppDomain 卸载期间所有资源都已清理干净……”——此引用指的是 Full Framework 的现代 CER 和 Compact Framework 的 finally 块。
  • MSDN,Reliability Best Practices

后记

常规的感谢适用于:感谢 Mandy 的校对(尤其是在婚礼策划如此疯狂的时候),以及感谢上帝赐予的一切!

我计划在我的下一篇文章中更详细地介绍关闭场景,并介绍支持关闭逻辑的 Level 2 类型。说实话,我将在 10 月 4 日结婚,并在接下来的一个星期休假,所以请不要指望很快看到我的第三篇文章。 :)

IDisposable:也适用于 CF 开发人员 - CodeProject - 代码之家
© . All rights reserved.