
SafeHandle / CriticalHandle / SafeBuffer /派生类型
我觉得我要为您打开潘多拉盒子。 让我们谈谈特殊类型:SafeHandle,CriticalHandle及其派生类型。
这是提供对非托管资源的访问的类型模式的最后一件事。 但是首先,让我们列出通常从不受管理的世界中获得的一切:
首先,很明显的是手柄。 对于.NET开发人员来说,这可能是一个毫无意义的词,但它是操作系统世界中非常重要的组成部分。 句柄本质上是32位或64位数字。 它指定与操作系统进行交互的打开的会话。 例如,当您打开文件时,您将从WinApi函数获得一个句柄。 然后,您可以使用它并执行搜索 , 读取或写入操作。 或者,您可以打开用于网络访问的套接字。 同样,操作系统将为您传递一个句柄。 在.NET中,句柄存储为IntPtr类型。
本章由作者和专业翻译员共同译自俄语。 您可以帮助我们将俄语或英语翻译成任何其他语言,主要是中文或德语。
另外,如果您想感谢我们,最好的方法是在github上给我们加星号或分支存储库
github / sidristij / dotnetbook
- 第二件事是数据数组。 您可以通过不安全的代码(此处不安全是关键字)来使用非托管数组,也可以使用SafeBuffer来将数据缓冲区包装到合适的.NET类中。 请注意,第一种方法更快(例如,您可以极大地优化循环),但是第二种方法更安全,因为它基于SafeHandle。
- 然后去弦。 字符串很简单,因为我们需要确定捕获的字符串的格式和编码。 然后为我们复制它(字符串是一个不可变的类),我们不再担心它。
- 最后一件事是ValueType,它们只是被复制,因此我们根本不需要考虑它们。
SafeHandle是一个特殊的.NET CLR类,它继承了CriticalFinalizerObject并应以最安全,最舒适的方式包装操作系统的句柄。
[SecurityCritical, SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode=true)] public abstract class SafeHandle : CriticalFinalizerObject, IDisposable { protected IntPtr handle; // The handle from OS private int _state; // State (validity, the reference counter) private bool _ownsHandle; // The flag for the possibility to release the handle. // It may happen that we wrap somebody else's handle // have no right to release. private bool _fullyInitialized; // The initialized instance [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] protected SafeHandle(IntPtr invalidHandleValue, bool ownsHandle) { } // The finalizer calls Dispose(false) with a pattern [SecuritySafeCritical] ~SafeHandle() { Dispose(false); } // You can set a handle manually or automatically with p/invoke Marshal [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected void SetHandle(IntPtr handle) { this.handle = handle; } // This method is necessary to work with IntPtr directly. It is used to // determine if a handle was created by comparing it with one of the previously // determined known values. Pay attention that this method is dangerous because: // // – if a handle is marked as invalid by SetHandleasInvalid, DangerousGetHandle // it will anyway return the original value of the handle. // – you can reuse the returned handle at any place. This can at least // mean, that it will stop work without a feedback. In the worst case if // IntPtr is passed directly to another place, it can go to an unsafe code and become // a vector for application attack by resource substitution in one IntPtr [ResourceExposure(ResourceScope.None), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public IntPtr DangerousGetHandle() { return handle; } // The resource is closed (no more available for work) public bool IsClosed { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] get { return (_state & 1) == 1; } } // The resource is not available for work. You can override the property by changing the logic. public abstract bool IsInvalid { [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] get; } // Closing the resource through Close() pattern [SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public void Close() { Dispose(true); } // Closing the resource through Dispose() pattern [SecuritySafeCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] public void Dispose() { Dispose(true); } [SecurityCritical, ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected virtual void Dispose(bool disposing) { // ... } // You should call this method every time when you understand that a handle is not operational anymore. // If you don't do it, you can get a leak. [SecurityCritical, ResourceExposure(ResourceScope.None)] [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] [MethodImplAttribute(MethodImplOptions.InternalCall)] public extern void SetHandleAsInvalid(); // Override this method to point how to release // the resource. You should code carefully, as you cannot // call uncompiled methods, create new objects or produce exceptions from it. // A returned value shows if the resource was releases successfully. // If a returned value = false, SafeHandleCriticalFailure will occur // that will enter a breakpoint if SafeHandleCriticalFailure // Managed Debugger Assistant is activated. [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] protected abstract bool ReleaseHandle(); // Working with the reference counter. To be explained further. [SecurityCritical, ResourceExposure(ResourceScope.None)] [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] [MethodImplAttribute(MethodImplOptions.InternalCall)] public extern void DangerousAddRef(ref bool success); public extern void DangerousRelease(); }
要了解从SafeHandle派生的类的有用性,您需要记住.NET类型为何如此出色:GC可以自动收集其实例。 在管理SafeHandle时,它包装的非托管资源继承了托管世界的所有特征。 它还包含一个外部引用的内部计数器,而外部引用对CLR不可用。 我的意思是引用不安全的代码。 您根本不需要手动增加或减少计数器。 当您将派生自SafeHandle的类型声明为不安全方法的参数时,计数器会在进入该方法时增加或在退出后减少。 原因是,当您通过传递一个句柄进入不安全的代码时,可以通过在另一个线程中重置对该句柄的引用来获得GC收集的SafeHandle(如果您从多个线程处理一个句柄)。 使用引用计数器,事情变得更加轻松:在计数器归零之前,不会创建SafeHandle。 这就是为什么您不需要手动更改计数器的原因。 或者,您应该非常小心地将其返回(如果可能)。
引用计数器的第二个目的是设置互相引用的CriticalFinalizerObject
的完成顺序。 如果一个基于SafeHandle的类型引用了另一个,则您需要在引用类型的构造函数中另外增加一个引用计数器,并在ReleaseHandle方法中减少该计数器。 因此,您的对象将一直存在,直到您的对象引用的对象未被破坏为止。 但是,最好避免这种困惑。 让我们使用有关SafeHandlers的知识,并编写类的最终变体:
public class FileWrapper : IDisposable { SafeFileHandle _handle; bool _disposed; public FileWrapper(string name) { _handle = CreateFile(name, 0, 0, 0, 0, 0, IntPtr.Zero); } public void Dispose() { if(_disposed) return; _disposed = true; _handle.Dispose(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckDisposed() { if(_disposed) { throw new ObjectDisposedException(); } } [DllImport("kernel32.dll", EntryPoint = "CreateFile", SetLastError = true)] private static extern SafeFileHandle CreateFile(String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile); /// other methods }
有什么不同? 如果您在DllImport方法中将任何基于SafeHandle的类型(包括您自己的类型)设置为返回值,则Marshal将正确创建并初始化此类型并将计数器设置为1。知道了这一点后,我们将SafeFileHandle类型设置为以下情况的返回类型: CreateFile内核函数。 当我们得到它时,我们将精确地使用它来调用ReadFile和WriteFile(因为调用时计数器值增加,退出时递减计数器值将确保在读写文件时该句柄仍然存在)。 这是一种设计正确的类型,如果线程中止,它将可靠地关闭文件句柄。 这意味着我们不需要实现自己的终结器以及与此相关的所有东西。 整个类型被简化。
实例方法工作时终结器的执行
在垃圾回收过程中使用了一种优化技术,该技术旨在在更短的时间内收集更多的对象。 让我们看下面的代码:
public void SampleMethod() { var obj = new object(); obj.ToString(); // ... // If GC runs at this point, it may collect obj // as it is not used anymore // ... Console.ReadLine(); }
一方面,代码看起来很安全,而且我们为什么还要在意尚不清楚。 但是,如果您记得有些类包装了非托管资源,您将了解设计不正确的类可能会导致非托管世界的异常。 此异常将报告先前获取的句柄未激活:
// The example of an absolutely incorrect implementation void Main() { var inst = new SampleClass(); inst.ReadData(); // inst is not used further } public sealed class SampleClass : CriticalFinalizerObject, IDisposable { private IntPtr _handle; public SampleClass() { _handle = CreateFile("test.txt", 0, 0, IntPtr.Zero, 0, 0, IntPtr.Zero); } public void Dispose() { if (_handle != IntPtr.Zero) { CloseHandle(_handle); _handle = IntPtr.Zero; } } ~SampleClass() { Console.WriteLine("Finalizing instance."); Dispose(); } public unsafe void ReadData() { Console.WriteLine("Calling GC.Collect..."); // I redirected it to the local variable not to // use this after GC.Collect(); var handle = _handle; // The imitation of full GC.Collect GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Console.WriteLine("Finished doing something."); var overlapped = new NativeOverlapped(); // it is not important what we do ReadFileEx(handle, new byte[] { }, 0, ref overlapped, (a, b, c) => {;}); } [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, BestFitMapping = false)] static extern IntPtr CreateFile(String lpFileName, int dwDesiredAccess, int dwShareMode, IntPtr securityAttrs, int dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile); [DllImport("kernel32.dll", SetLastError = true)] static extern bool ReadFileEx(IntPtr hFile, [Out] byte[] lpBuffer, uint nNumberOfBytesToRead, [In] ref NativeOverlapped lpOverlapped, IOCompletionCallback lpCompletionRoutine); [DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr hObject); }
承认这段代码或多或少看起来不错。 无论如何,看起来好像没有问题。 实际上,这是一个严重的问题。 类终结器可能在读取文件时尝试关闭文件,这几乎不可避免地导致错误。 因为在这种情况下,错误已明确返回( IntPtr == -1
),所以我们不会看到此错误。 _handle
将设置为零,以下Dispose
将无法关闭文件,并且资源将泄漏。 要解决此问题,您应该使用SafeHandle
, CriticalHandle
, SafeBuffer
及其派生类。 除了这些类具有在非托管代码中使用的计数器外,这些计数器还会在将方法的参数传递给非托管世界时自动递增,并在离开它时递减。
该章程由专业翻译人员从俄语译为作者的语言 。 您可以帮助我们使用俄语和英语版本的文本作为源来创建该文本到其他任何语言(包括中文或德语)的翻译版本。
另外,如果您想说“谢谢”,那么您可以选择的最好方法是在github或fork库上给我们加星号
https://github.com/sidristij/dotnetbook