النمط القابل للتصرف (مبدأ التصميم القابل للتصرف) الجزء 2


SafeHandle / CriticalHandle / SafeBuffer / الأنواع المشتقة


أشعر أنني ذاهب إلى فتح صندوق باندورا من أجلك. دعونا نتحدث عن أنواع خاصة: SafeHandle و CriticalHandle وأنواعها المشتقة.


هذا هو آخر شيء عن نمط النوع الذي يتيح الوصول إلى مورد غير مُدار. لكن أولاً ، دعنا نورد كل ما نحصل عليه عادة من عالم غير مُدار:


أول شيء واضح هو مقابض. قد تكون هذه كلمة لا معنى لها لمطور .NET ، لكنها عنصر مهم جدًا في عالم نظام التشغيل. المقبض رقم 32 أو 64 بت بطبيعته. يعين جلسة مفتوحة للتفاعل مع نظام التشغيل. على سبيل المثال ، عند فتح ملف ، تحصل على مؤشر من وظيفة WinApi. ثم يمكنك العمل معها والقيام بعمليات البحث أو القراءة أو الكتابة . أو ، يمكنك فتح مأخذ وصول للوصول إلى الشبكة. مرة أخرى سوف يمررك نظام التشغيل. في .NET يتم تخزين مقابض كنوع IntPtr ؛


تمت ترجمة هذا الفصل من اللغة الروسية بالاشتراك مع المؤلفين والمترجمين المحترفين . يمكنك مساعدتنا في الترجمة من الروسية أو الإنجليزية إلى أي لغة أخرى ، في المقام الأول إلى الصينية أو الألمانية.

وأيضًا ، إذا كنت تريد شكراً منا ، فإن أفضل طريقة للقيام بذلك هي منحنا نجمًا على github أو لتخزين المستودع github / sidristij / dotnetbook .

  • الشيء الثاني هو صفائف البيانات. يمكنك العمل مع صفائف غير مُدارة إما من خلال رمز غير آمن (الكلمة غير الآمنة هي الكلمة الأساسية هنا) أو استخدام SafeBuffer الذي سيؤدي إلى التفاف مخزن مؤقت للبيانات في فئة .NET مناسبة. لاحظ أن الطريقة الأولى أسرع (على سبيل المثال ، يمكنك تحسين الحلقات إلى حد كبير) ، ولكن الطريقة الثانية أكثر أمانًا ، حيث إنها تستند إلى SafeHandle ؛
  • ثم اذهب الاوتار. السلاسل بسيطة لأننا نحتاج إلى تحديد تنسيق وترميز السلسلة التي نلتقطها. ثم يتم نسخها بالنسبة لنا (السلسلة عبارة عن فئة غير قابلة للتغيير) ولم نعد قلقين بشأنها بعد الآن.
  • آخر شيء هو ValueTypes التي يتم نسخها فقط لذلك نحن لسنا بحاجة إلى التفكير فيها على الإطلاق.

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 كمعلمة لطريقة غير آمنة ، فإن العداد يزداد عند إدخال هذه الطريقة أو التناقصات بعد الخروج. السبب هو أنه عندما تذهب إلى رمز غير آمن بتمرير مؤشر هناك ، يمكنك الحصول على SafeHandle التي تم جمعها بواسطة GC ، عن طريق إعادة تعيين المرجع إلى هذا المؤشر في مؤشر ترابط آخر (إذا كنت تتعامل مع مؤشر واحد من عدة مؤشرات ترابط). تعمل الأمور بشكل أسهل باستخدام عداد مرجعي: لن يتم إنشاء 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 } 

كيف هو مختلف؟ إذا قمت بتعيين أي نوع يستند إلى SafeHandle (بما في ذلك نوعك الخاص) كقيمة الإرجاع في طريقة DllImport ، فسيقوم المارشال بإنشاء هذا النوع وتهيئته بشكل صحيح وتعيين عداد على 1. مع العلم بأننا قمنا بتعيين نوع SafeFileHandle كنوع إرجاع لـ دالة CreateFile kernel. عندما نحصل عليها ، سوف نستخدمها بالضبط لاستدعاء ReadFile و WriteFile (كزيادات في قيمة العداد عند الاتصال وتناقصات عند الخروج منه ستضمن أن المقبض ما زال موجودًا أثناء القراءة من ملف والكتابة إليه). هذا نوع مصمم بشكل صحيح وسيقوم بإغلاق مقبض الملف بشكل صحيح في حالة إحباط مؤشر ترابط. هذا يعني أننا لسنا بحاجة إلى تطبيق برنامجنا النهائي وكل ما يتعلق به. النوع كله مبسط.


تنفيذ finalizer عندما تعمل أساليب المثيل


هناك أسلوب تحسين واحد يُستخدم أثناء تجميع البيانات المهملة المصممة لجمع المزيد من الكائنات في وقت أقل. لنلقِ نظرة على الكود التالي:


 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 أو مستودع forking https://github.com/sidristij/dotnetbook

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


All Articles