Modèle jetable (principe de conception jetable) pt.2


SafeHandle / CriticalHandle / SafeBuffer / types dérivés


Je sens que je vais ouvrir la boîte de Pandore pour toi. Parlons des types spéciaux: SafeHandle, CriticalHandle et leurs types dérivés.


Ceci est la dernière chose à propos du modèle d'un type qui donne accès à une ressource non gérée. Mais d'abord, énumérons tout ce que nous obtenons habituellement du monde non géré:


La première chose et évidente est les poignées. Cela peut être un mot vide de sens pour un développeur .NET, mais c'est un composant très important du monde du système d'exploitation. Un descripteur est un nombre 32 ou 64 bits par nature. Il désigne une session ouverte d'interaction avec un système d'exploitation. Par exemple, lorsque vous ouvrez un fichier, vous obtenez un descripteur de la fonction WinApi. Ensuite, vous pouvez travailler avec lui et effectuer des opérations de recherche , de lecture ou d' écriture . Ou, vous pouvez ouvrir un socket pour l'accès au réseau. Encore une fois, un système d'exploitation vous passera une poignée. Dans .NET, les descripteurs sont stockés en tant que type IntPtr ;


Ce chapitre a été traduit du russe conjointement par l'auteur et par des traducteurs professionnels . Vous pouvez nous aider avec la traduction du russe ou de l'anglais dans n'importe quelle autre langue, principalement en chinois ou en allemand.

Aussi, si vous voulez nous remercier, la meilleure façon de le faire est de nous donner une étoile sur github ou sur fork repository github / sidristij / dotnetbook .

  • La deuxième chose est les tableaux de données. Vous pouvez travailler avec des tableaux non gérés via un code dangereux (dangereux est un mot clé ici) ou utiliser SafeBuffer qui encapsulera un tampon de données dans une classe .NET appropriée. Notez que la première méthode est plus rapide (par exemple, vous pouvez optimiser considérablement les boucles), mais la seconde est beaucoup plus sûre, car elle est basée sur SafeHandle;
  • Allez ensuite les cordes. Les chaînes sont simples car nous devons déterminer le format et l'encodage de la chaîne que nous capturons. Il est ensuite copié pour nous (une chaîne est une classe immuable) et nous ne nous en soucions plus.
  • La dernière chose est les ValueTypes qui sont juste copiés, donc nous n'avons pas besoin d'y penser du tout.

SafeHandle est une classe CLR .NET spéciale qui hérite de CriticalFinalizerObject et doit encapsuler les poignées d'un système d'exploitation de la manière la plus sûre et la plus confortable.


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

Pour comprendre l'utilité des classes dérivées de SafeHandle, vous devez vous rappeler pourquoi les types .NET sont si importants: GC peut collecter leurs instances automatiquement. Lorsque SafeHandle est géré, la ressource non gérée qu'il a encapsulée hérite de toutes les caractéristiques du monde géré. Il contient également un compteur interne de références externes qui ne sont pas disponibles pour CLR. Je veux dire des références de code dangereux. Vous n'avez pas du tout besoin d'incrémenter ou de décrémenter un compteur manuellement. Lorsque vous déclarez un type dérivé de SafeHandle en tant que paramètre d'une méthode non sûre, le compteur s'incrémente lors de la saisie de cette méthode ou diminue après la sortie. La raison en est que lorsque vous accédez à un code dangereux en y passant un handle, vous pouvez obtenir ce SafeHandle collecté par GC, en réinitialisant la référence à ce handle dans un autre thread (si vous traitez un handle à partir de plusieurs threads). Les choses fonctionnent encore plus facilement avec un compteur de référence: SafeHandle ne sera pas créé tant que le compteur ne sera pas remis à zéro. C'est pourquoi vous n'avez pas besoin de changer le compteur manuellement. Ou, vous devez le faire très soigneusement en le renvoyant lorsque cela est possible.


Le deuxième objectif d'un compteur de références est de définir l'ordre de finalisation de CriticalFinalizerObject qui se référencent mutuellement. Si un type basé sur SafeHandle en référence un autre, vous devez également incrémenter un compteur de référence dans le constructeur du type de référence et diminuer le compteur dans la méthode ReleaseHandle. Ainsi, votre objet existera jusqu'à ce que l'objet auquel votre objet fait référence ne soit pas détruit. Cependant, il vaut mieux éviter de telles perplexités. Utilisons les connaissances sur SafeHandlers et écrivons la dernière variante de notre 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 } 

En quoi est-ce différent? Si vous définissez un type basé sur SafeHandle (y compris le vôtre) comme valeur de retour dans la méthode DllImport, Marshal créera et initialisera correctement ce type et définira un compteur à 1. Sachant cela, nous définissons le type SafeFileHandle comme type de retour pour la fonction du noyau CreateFile. Lorsque nous l'obtenons, nous l'utilisons exactement pour appeler ReadFile et WriteFile (comme une valeur de compteur incrémente lors de l'appel et diminue à la sortie, elle garantira que le handle existe toujours pendant la lecture et l'écriture dans un fichier). Il s'agit d'un type correctement conçu et il fermera de manière fiable un descripteur de fichier si un thread est abandonné. Cela signifie que nous n'avons pas besoin d'implémenter notre propre finaliseur et tout ce qui s'y rapporte. Le type entier est simplifié.


L'exécution d'un finaliseur lorsque les méthodes d'instance fonctionnent


Il existe une technique d'optimisation utilisée lors de la récupération de place qui est conçue pour collecter plus d'objets en moins de temps. Regardons le code suivant:


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

D'une part, le code semble sûr, et on ne sait pas tout de suite pourquoi s'en soucier. Cependant, si vous vous souvenez qu'il existe des classes qui encapsulent des ressources non managées, vous comprendrez qu'une classe mal conçue peut provoquer une exception du monde non managé. Cette exception signale qu'un descripteur précédemment obtenu n'est pas actif:


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

Admettez que ce code semble plus ou moins décent. De toute façon, il ne semble pas y avoir de problème. En fait, il y a un sérieux problème. Un finaliseur de classe peut tenter de fermer un fichier pendant sa lecture, ce qui conduit presque inévitablement à une erreur. Parce que dans ce cas, l'erreur est explicitement renvoyée ( IntPtr == -1 ), nous ne le verrons pas. Le _handle sera défini sur zéro, la _handle suivante ne parviendra pas à fermer le fichier et la ressource fuira. Pour résoudre ce problème, vous devez utiliser SafeHandle , CriticalHandle , SafeBuffer et leurs classes dérivées. Outre que ces classes ont des compteurs d'utilisation en code non managé, ces compteurs incrémentent également automatiquement lors du passage avec les paramètres des méthodes au monde non managé et décrémentent en le quittant.


Ce charper traduit du russe comme de la langue de l'auteur par des traducteurs professionnels . Vous pouvez nous aider à créer une version traduite de ce texte dans n'importe quelle autre langue, y compris le chinois ou l'allemand, en utilisant les versions russe et anglaise du texte comme source.

De plus, si vous voulez dire "merci", la meilleure façon que vous pouvez choisir est de nous donner une étoile sur github ou un référentiel de forking https://github.com/sidristij/dotnetbook

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


All Articles