SafeHandle / CriticalHandle / SafeBuffer / derived types
I feel I’m going to open the Pandora’s box for you. Let’s talk about special types: SafeHandle, CriticalHandle and their derived types.
This is the last thing about the pattern of a type that gives access to an unmanaged resource. But first, let’s list everything we usually get from unmanaged world:
The first and obvious thing is handles. This may be an meaningless word for a .NET developer, but it is a very important component of the operating system world. A handle is a 32- or 64-bit number by nature. It designates an opened session of interaction with an operating system. For example, when you open a file you get a handle from the WinApi function. Then you can work with it and do Seek, Read or Write operations. Or, you may open a socket for network access. Again an operating system will pass you a handle. In .NET handles are stored as IntPtr type;
This chapter was translated from Russian jointly by author and by professional translators. You can help us with translation from Russian or English into any other language, primarily into Chinese or German.
Also, if you want thank us, the best way you can do that is to give us a star on github or to fork repository github/sidristij/dotnetbook.
- The second thing is data arrays. You can work with unmanaged arrays either through unsafe code (unsafe is a key word here) or use SafeBuffer which will wrap a data buffer into a suitable .NET class. Note that the first way is faster (e.g. you can optimize loops greatly), but the second one is much safer, as it is based on SafeHandle;
- Then go strings. Strings are simple as we need to determine the format and encoding of the string we capture. It is then copied for us (a string is an immutable class) and we don’t worry about it anymore.
- The last thing is ValueTypes that are just copied so we don’t need to think about them at all.
SafeHandle is a special .NET CLR class that inherits CriticalFinalizerObject and should wrap the handles of an operating system in the safest and most comfortable way.
[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();
}
To understand the usefulness of the classes derived from SafeHandle you need to remember why .NET types are so great: GC can collect their instances automatically. As SafeHandle is managed, the unmanaged resource it wrapped inherits all characteristics of the managed world. It also contains an internal counter of external references which are unavailable to CLR. I mean references from unsafe code. You don’t need to increment or decrement a counter manually at all. When you declare a type derived from SafeHandle as a parameter of an unsafe method, the counter increments when entering that method or decrements after exiting. The reason is that when you go to an unsafe code by passing a handle there, you may get this SafeHandle collected by GC, by resetting the reference to this handle in another thread (if you deal with one handle from several threads). Things work even easier with a reference counter: SafeHandle will not be created until the counter is zeroed. That’s why you don’t need to change the counter manually. Or, you should do it very carefully by returning it when possible.
The second purpose of a reference counter is to set the order of finalization of CriticalFinalizerObject
that reference each other. If one SafeHandle-based type references another, then you need to additionally increment a reference counter in the constructor of the referencing type and decrease the counter in the ReleaseHandle method. Thus, your object will exist until the object to which your object references is not destroyed. However, it's better to avoid such puzzlements. Let’s use the knowledge about SafeHandlers and write the final variant of our class:
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
}
How is it different? If you set any SafeHandle-based type (including your own) as the return value in the DllImport method, then Marshal will correctly create and initialize this type and set a counter to 1. Knowing this we set the SafeFileHandle type as a return type for the CreateFile kernel function. When we get it, we will use it exactly to call ReadFile and WriteFile (as a counter value increments when calling and decrements when exiting it will ensure that the handle still exist throughout reading from and writing to a file). This is a correctly designed type and it will reliably close a file handle if a thread is aborted. This means we don’t need to implement our own finalizer and everything connected with it. The whole type is simplified.
The execution of a finalizer when instance methods work
There is one optimization technique used during garbage collection that is designed to collect more objects in less time. Let’s look at the following code:
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();
}
On the one hand, the code looks safe, and it’s not clear straightaway why should we care. However, if you remember that there are classes that wrap unmanaged resources, you will understand that an incorrectly designed class may cause an exception from the unmanaged world. This exception will report that a previously obtained handle is not active:
// 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);
}
Admit that this code looks decent more or less. Anyway, it doesn’t look like there is a problem. In fact, there is a serious problem. A class finalizer may attempt to close a file while reading it, which almost inevitably leads to an error. Because in this case the error is explicitly returned (IntPtr == -1
) we will not see this. The _handle
will be set to zero, the following Dispose
will fail to close the file and the resource will leak. To solve this problem, you should use SafeHandle
, CriticalHandle
, SafeBuffer
and their derived classes. Besides that these classes have counters of usage in unmanaged code, these counters also automatically increment when passing with methods' parameters to the unmanaged world and decrement when leaving it.
This charper translated from Russian as from language of author by professional translators. You can help us with creating translated version of this text to any other language including Chinese or German using Russian and English versions of text as source.
Also, if you want to say «thank you», the best way you can choose is giving us a star on github or forking repository https://github.com/sidristij/dotnetbook