Padrão descartável (Princípio do projeto descartável) pt. 2


SafeHandle / CriticalHandle / SafeBuffer / tipos derivados


Sinto que vou abrir a caixa da Pandora para você. Vamos falar sobre tipos especiais: SafeHandle, CriticalHandle e seus tipos derivados.


Esta é a última coisa sobre o padrão de um tipo que dá acesso a um recurso não gerenciado. Mas primeiro, vamos listar tudo o que geralmente obtemos do mundo não gerenciado:


A primeira coisa e óbvia são alças. Essa pode ser uma palavra sem sentido para um desenvolvedor .NET, mas é um componente muito importante do mundo dos sistemas operacionais. Um identificador é um número de 32 ou 64 bits por natureza. Designa uma sessão aberta de interação com um sistema operacional. Por exemplo, quando você abre um arquivo, obtém um identificador da função WinApi. Em seguida, você pode trabalhar com ele e realizar operações de busca , leitura ou gravação . Ou, você pode abrir um soquete para acesso à rede. Novamente, um sistema operacional passará para você um identificador. No .NET, os identificadores são armazenados como tipo IntPtr ;


Este capítulo foi traduzido do russo em conjunto pelo autor e por tradutores profissionais . Você pode nos ajudar com a tradução do russo ou do inglês para qualquer outro idioma, principalmente para chinês ou alemão.

Além disso, se você quiser nos agradecer, a melhor maneira de fazer isso é nos dar uma estrela no github ou no fork do repositório github / sidristij / dotnetbook .

  • A segunda coisa são matrizes de dados. Você pode trabalhar com matrizes não gerenciadas por meio de código não seguro (inseguro é uma palavra-chave aqui) ou usar o SafeBuffer, que envolverá um buffer de dados em uma classe .NET adequada. Observe que a primeira maneira é mais rápida (por exemplo, você pode otimizar muito os loops), mas a segunda é muito mais segura, pois é baseada no SafeHandle;
  • Então vá cordas. As strings são simples, pois precisamos determinar o formato e a codificação da string que capturamos. Ele é copiado para nós (uma string é uma classe imutável) e não nos preocupamos mais com isso.
  • A última coisa são os ValueTypes que são copiados, para que não precisemos pensar neles.

SafeHandle é uma classe .NET CLR especial que herda CriticalFinalizerObject e deve envolver as alças de um sistema operacional da maneira mais segura e confortável.


[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 entender a utilidade das classes derivadas do SafeHandle, você precisa se lembrar por que os tipos .NET são tão bons: o GC pode coletar suas instâncias automaticamente. Como o SafeHandle é gerenciado, o recurso não gerenciado que ele agrupa herda todas as características do mundo gerenciado. Ele também contém um contador interno de referências externas que não estão disponíveis para o CLR. Quero dizer referências de código inseguro. Você não precisa incrementar ou diminuir manualmente um contador. Quando você declara um tipo derivado do SafeHandle como parâmetro de um método não seguro, o contador é incrementado ao inserir esse método ou diminuído após a saída. O motivo é que, quando você acessa um código inseguro passando um identificador para lá, você pode obter esse SafeHandle coletado pelo GC, redefinindo a referência a esse identificador em outro segmento (se você lidar com um identificador de vários segmentos). As coisas funcionam ainda mais facilmente com um contador de referência: o SafeHandle não será criado até que o contador seja zerado. É por isso que você não precisa alterar o contador manualmente. Ou você deve fazê-lo com muito cuidado, devolvendo-o quando possível.


O segundo objetivo de um contador de referência é definir a ordem de finalização do CriticalFinalizerObject que faz referência um ao outro. Se um tipo baseado no SafeHandle fizer referência a outro, você precisará incrementar adicionalmente um contador de referência no construtor do tipo de referência e diminuir o contador no método ReleaseHandle. Portanto, seu objeto existirá até que o objeto ao qual o objeto se refere não seja destruído. No entanto, é melhor evitar essas intrigas. Vamos usar o conhecimento sobre o SafeHandlers e escrever a variante final da nossa classe:


 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 é diferente? Se você definir qualquer tipo baseado no SafeHandle (incluindo o seu) como o valor de retorno no método DllImport, o Marshal criará e inicializará corretamente esse tipo e definirá um contador como 1. Sabendo disso, definimos o tipo SafeFileHandle como um tipo de retorno para a função do kernel CreateFile. Quando obtê-lo, usá-lo-ei exatamente para chamar ReadFile e WriteFile (como o valor do contador aumenta quando chama e diminui ao sair, isso garante que o identificador ainda exista durante a leitura e gravação em um arquivo). Este é um tipo projetado corretamente e fechará com segurança um identificador de arquivo se um encadeamento for interrompido. Isso significa que não precisamos implementar nosso próprio finalizador e tudo relacionado a ele. Todo o tipo é simplificado.


A execução de um finalizador quando os métodos de instância funcionam


Existe uma técnica de otimização usada durante a coleta de lixo, projetada para coletar mais objetos em menos tempo. Vejamos o seguinte 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 um lado, o código parece seguro e não está claro imediatamente por que devemos nos importar. No entanto, se você se lembrar de que existem classes que agrupam recursos não gerenciados, você entenderá que uma classe projetada incorretamente pode causar uma exceção no mundo não gerenciado. Esta exceção relatará que um identificador obtido anteriormente não está ativo:


 // 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 esse código parece decente mais ou menos. Enfim, não parece que há um problema. De fato, há um problema sério. Um finalizador de classe pode tentar fechar um arquivo enquanto o lê, o que quase inevitavelmente leva a um erro. Como nesse caso o erro é retornado explicitamente ( IntPtr == -1 ), não veremos isso. O _handle será definido como zero, o seguinte Dispose falhará ao fechar o arquivo e o recurso vazará. Para resolver esse problema, você deve usar SafeHandle , CriticalHandle , SafeBuffer e suas classes derivadas. Além de que essas classes têm contadores de uso em código não gerenciado, esses contadores também aumentam automaticamente ao passar os parâmetros dos métodos para o mundo não gerenciado e diminuem ao deixá-lo.


Este charper traduzido do russo como idioma do autor por tradutores profissionais . Você pode nos ajudar a criar a versão traduzida deste texto para qualquer outro idioma, incluindo chinês ou alemão, usando as versões russa e inglesa do texto como fonte.

Além disso, se você quiser dizer "obrigado", a melhor maneira de escolher é dar uma estrela no repositório do github ou do fork https://github.com/sidristij/dotnetbook

Source: https://habr.com/ru/post/pt443960/


All Articles