
SafeHandle / CriticalHandle / SafeBuffer / tipos derivados
Siento que voy a abrir la caja de Pandora para ti. Hablemos de tipos especiales: SafeHandle, CriticalHandle y sus tipos derivados.
Esto es lo último sobre el patrón de un tipo que da acceso a un recurso no administrado. Pero primero, enumeremos todo lo que usualmente obtenemos del mundo no administrado:
Lo primero y obvio son las manijas. Esta puede ser una palabra sin sentido para un desarrollador de .NET, pero es un componente muy importante del mundo del sistema operativo. Un identificador es un número de 32 o 64 bits por naturaleza. Designa una sesión abierta de interacción con un sistema operativo. Por ejemplo, cuando abre un archivo, obtiene un identificador de la función WinApi. Luego puede trabajar con él y realizar operaciones de Búsqueda , Lectura o Escritura . O puede abrir un socket para acceder a la red. Una vez más, un sistema operativo le pasará un control. En .NET, los identificadores se almacenan como tipo IntPtr ;
Este capítulo fue traducido del ruso conjuntamente por el autor y por traductores profesionales . Puede ayudarnos con la traducción del ruso o el inglés a cualquier otro idioma, principalmente al chino o al alemán.
Además, si quieres agradecernos, la mejor manera de hacerlo es darnos una estrella en Github o bifurcar el repositorio
github / sidristij / dotnetbook .
- Lo segundo son las matrices de datos. Puede trabajar con matrices no administradas a través de un código inseguro (inseguro es una palabra clave aquí) o usar SafeBuffer que envolverá un búfer de datos en una clase adecuada de .NET. Tenga en cuenta que la primera forma es más rápida (por ejemplo, puede optimizar los bucles en gran medida), pero la segunda es mucho más segura, ya que se basa en SafeHandle;
- Luego ve a las cuerdas. Las cadenas son simples ya que necesitamos determinar el formato y la codificación de la cadena que capturamos. Luego se copia para nosotros (una cadena es una clase inmutable) y ya no nos preocupamos por eso.
- Lo último son los ValueTypes que se acaban de copiar, por lo que no necesitamos pensar en ellos en absoluto.
SafeHandle es una clase especial de .NET CLR que hereda CriticalFinalizerObject y debe envolver los controladores de un sistema operativo de la manera más segura y cómoda.
[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(); }
Para comprender la utilidad de las clases derivadas de SafeHandle, debe recordar por qué los tipos .NET son tan geniales: GC puede recopilar sus instancias automáticamente. A medida que SafeHandle se administra, el recurso no administrado que envuelve hereda todas las características del mundo administrado. También contiene un contador interno de referencias externas que no están disponibles para CLR. Me refiero a referencias de código inseguro. No es necesario que incremente o disminuya un contador manualmente. Cuando declara un tipo derivado de SafeHandle como parámetro de un método inseguro, el contador aumenta al ingresar ese método o disminuye después de salir. La razón es que cuando va a un código inseguro al pasar un identificador allí, puede obtener este SafeHandle recopilado por GC, restableciendo la referencia a este identificador en otro subproceso (si trata con un identificador de varios subprocesos). Las cosas funcionan aún más fácilmente con un contador de referencia: SafeHandle no se creará hasta que el contador se ponga a cero. Es por eso que no necesita cambiar el contador manualmente. O bien, debe hacerlo con mucho cuidado devolviéndolo cuando sea posible.
El segundo propósito de un contador de referencia es establecer el orden de finalización de CriticalFinalizerObject
que se refieren entre sí. Si un tipo basado en SafeHandle hace referencia a otro, entonces necesita incrementar adicionalmente un contador de referencia en el constructor del tipo de referencia y disminuir el contador en el método ReleaseHandle. Por lo tanto, su objeto existirá hasta que el objeto al que hace referencia su objeto no se destruya. Sin embargo, es mejor evitar tales perplejidades. Usemos el conocimiento sobre SafeHandlers y escriba la variante final de nuestra clase:
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 }
Como es diferente Si configura cualquier tipo basado en SafeHandle (incluido el suyo) como el valor de retorno en el método DllImport, Marshal creará e inicializará correctamente este tipo y establecerá un contador en 1. Sabiendo esto, configuraremos el tipo SafeFileHandle como un tipo de retorno para la función del kernel CreateFile. Cuando lo obtengamos, lo usaremos exactamente para llamar a ReadFile y WriteFile (a medida que el valor del contador aumente al llamar y disminuya al salir, se asegurará de que el identificador siga existiendo durante la lectura y la escritura en un archivo). Este es un tipo correctamente diseñado y cerrará de manera confiable un identificador de archivo si se aborta un hilo. Esto significa que no necesitamos implementar nuestro propio finalizador y todo lo relacionado con él. Todo el tipo está simplificado.
La ejecución de un finalizador cuando los métodos de instancia funcionan
Hay una técnica de optimización utilizada durante la recolección de basura que está diseñada para recolectar más objetos en menos tiempo. Veamos el siguiente código:
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(); }
Por un lado, el código parece seguro, y no está claro de inmediato por qué debería importarnos. Sin embargo, si recuerda que hay clases que envuelven recursos no administrados, comprenderá que una clase diseñada incorrectamente puede causar una excepción del mundo no administrado. Esta excepción informará que un identificador obtenido previamente no está activo:
// 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); }
Admita que este código parece decente más o menos. De todos modos, no parece que haya un problema. De hecho, hay un problema grave. Un finalizador de clase puede intentar cerrar un archivo mientras lo lee, lo que casi inevitablemente conduce a un error. Como en este caso el error se devuelve explícitamente ( IntPtr == -1
) no lo veremos. El _handle
se establecerá en cero, el siguiente Dispose
no podrá cerrar el archivo y el recurso se perderá. Para resolver este problema, debe usar SafeHandle
, CriticalHandle
, SafeBuffer
y sus clases derivadas. Además de que estas clases tienen contadores de uso en código no administrado, estos contadores también se incrementan automáticamente al pasar los parámetros de los métodos al mundo no administrado y disminuyen al abandonarlo.
Este traductor traducido del ruso como del idioma del autor por traductores profesionales . Puede ayudarnos a crear una versión traducida de este texto a cualquier otro idioma, incluido el chino o el alemán, utilizando las versiones de texto en ruso e inglés como fuente.
Además, si quiere decir "gracias", la mejor manera de elegir es dándonos una estrella en github o bifurcando repositorio
https://github.com/sidristij/dotnetbook