|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Introduction - Recommended ReadingA lot of the information regarding .NET internals in this article was gleaned from "CLR via C#", 2nd Ed., by Jeffrey Richter of Wintellect, Microsoft Press. If you don't already own it, buy it. Now. Seriously. It's an essential resource for any C# programmer. The Boring Stuff - IDisposable Gone AwryThis article is divided into two parts: the first part is an evaluation of many of the problems inherent with Deterministic Resource Deallocation - Its NecessityDuring more than 20 years of coding, I have occasionally found it necessary to design my own small programming languages for certain tasks. These have varied from imperative scripting languages to a specialized regex for trees. There are many choices to make in language design: a few simple rules should never be broken, and there are many general guidelines. One of these guidelines is:
Guess which guideline the .NET runtime (and, by extension, every .NET language) does not follow? This article will examine some of the consequences of this choice, and then propose some best practices to try to work with this situation. The reason for the guideline is that deterministic resource deallocation is necessary to produce usable programs with maintainable code. Deterministic resource deallocation provides a certain point at which a coder may know that the resource has been deallocated. There are two approaches to writing reliable software: the traditional approach deallocates resources as soon as possible, while a modern approach lets resource deallocation occur at an indeterminate time. The advantage to the modern approach is that programmers do not have to deallocate resources explicitly. The disadvantage is that it becomes much more difficult to write reliable software; an entire new set of difficult-to-test error conditions is added to the allocation logic. Unfortunately, the .NET runtime was designed around the modern approach. The .NET runtime's support for indeterministic resource deallocation is through the In C#, a "poor man's deterministic deallocation" may be implemented through the use of The IDisposable Solution
IDisposable's Difficulties - Usability
C++ is slightly better in this regard. They support stack semantics for reference types, which has a logic equivalent to inserting a This problem with Depending on IDisposable's Difficulties - Backwards CompatibilityAdding Microsoft ran into this problem themselves. Is this the end of the world? Probably not, but it does open up some questions about how the second-class citizen status of IDisposable's Difficulties - DesignThe single greatest drawback caused by If an interface does not inherit from This "slicing" problem is the generalization of the problem Microsoft had when updating For some interfaces, it's rather obvious whether their derived types will need In short, There is one rather unattractive solution: have every interface and every class descend from The final difficulty regarding the IDisposable's Difficulties - Additional Error StateAnother difficulty with Instead of checking for the disposed state and throwing an exception, I recommend supporting "undefined behaviour". Accessing a disposed object is the modern equivalent of accessing deallocated memory. IDisposable's Difficulties - No GuaranteesSince IDisposable's Difficulties - Complexity of ImplementationMicrosoft has a code pattern for implementing
In addition, the following facts are often overlooked:
The naming convention for the recommended This is one area where C++ again has a bit of an edge on C#. The C++ compiler does much of the work of implementing Even for the perfect programmer, the complexity of the recommended Sometimes, for simplicity or by accident, IDisposable's Difficulties - Impossibility of Shutdown Logic (Managed Finalization)Shutdown logic is a common need in any real-world application. This is particularly true in asynchronous programming models. For example, a class that has its own child thread would wish to stop that thread by setting a In order to better understand the restrictions on finalizers, we must understand the garbage collector. [Note: The description of garbage collection and finalization given here is a simplification; the actual implementation is considerably more complicated. Generations, resurrection, weak references, and several other topics are ignored. For the purpose of this article, however, this logical description is correct and reasonably complete.] The .NET garbage collector utilizes a mark/sweep algorithm. Specifically, it does the logical equivalent of the following:
In parallel with the garbage collection above, finalization is also constantly running in the background:
The reason that finalizers may not access managed objects is because they do not know what other finalizers have been run. Any object that has fields to other objects may access those fields, since the other objects will still be in memory, but nothing may be done with them, since they may have already had their finalizers run (thus disposing them). Even calling There are a handful of exceptions where finalizers may access managed objects:
Generally speaking, finalizers may not access managed objects. However, support for shutdown logic is necessary for reasonably-complex software. The Microsoft came up against this problem, too. The A Brief Hiatus - Where We Are
<sarcasm strength="mild">Ahh, yes. .NET sure has made it easy. Why, unmanaged destructors in C++ are so much more complicated than all of this.</sarcasm> Seriously, this article could be much longer if it included the really complex issues such as resurrection and the restrictions on finalizers that descend from I'd like to take a moment to sing some praises of .NET and C#. Although I do disagree with a couple of Microsoft's decisions, on the whole, they've done an excellent job. I'm a huge fan of any language that brings the procedural and functional families together in a more synergetic union, and C# does an excellent job of that. The .NET Framework and runtime have a few rough corners, but overall, they're better than what has come before, and they're obviously the way things are going. So far, I've been pointing out the problems caused by The fact is, The (Hopefully) Not-So-Boring Stuff - IDisposable RegulatedThe first part of this article discussed the difficulties of Solving IDisposable's Difficulties - Minimize Use Cases of IDisposable by Utilizing the Disposable Design PrincipleOne reason for the complexity of Microsoft's recommended
The Disposable Design Principle is built on these ideas:
To expound on this design principle, the small
Implementing
However, Solving IDisposable's Difficulties - Helper Classes for Avoiding Implementing IDisposable DirectlyIt is very common to have to write an unmanaged resource wrapper class for an unmanaged resource that is a pointer to some data structure. For this common use case, a higher-level abstraction is available through Microsoft-provided helper classes. Level 0 types, in the Disposable Design Principle, should always derive from The relationship between This means there are four possibilities when having to write a new unmanaged resource wrapper (in the order of ease of implementation):
Note that when creating hierarchies of Level 1 types, it is common practice to declare a Wrapping Unmanaged Resources - Using Existing Level 0 Types (The Easy Case)To define a new Level 1 type that uses a Level 0 type, extend an existing Level 1 hierarchy, if possible. The example for using existing Level 0 ( Note that [SecurityPermission(SecurityAction.LinkDemand,
Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
[DllImport("kernel32.dll", EntryPoint = "CreateWaitableTimer",
CharSet = CharSet.Auto, BestFitMapping = false,
ThrowOnUnmappableChar = true, SetLastError = true),
SuppressUnmanagedCodeSecurity]
private static extern SafeWaitHandle DoCreateWaitableTimer(IntPtr lpTimerAttributes,
[MarshalAs(UnmanagedType.Bool)] bool bManualReset, string lpTimerName);
internal static SafeWaitHandle CreateWaitableTimer(IntPtr lpTimerAttributes,
bool bManualReset, string lpTimerName)
{
SafeWaitHandle ret = DoCreateWaitableTimer(lpTimerAttributes,
bManualReset, lpTimerName);
if (ret.IsInvalid)
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
return ret;
}
[DllImport("kernel32.dll", EntryPoint = "CancelWaitableTimer",
SetLastError = true),
SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DoCancelWaitableTimer(SafeWaitHandle hTimer);
internal static void CancelWaitableTimer(SafeWaitHandle hTimer)
{
if (!DoCancelWaitableTimer(hTimer))
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}
[DllImport("kernel32.dll", EntryPoint = "SetWaitableTimer",
SetLastError = true), SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DoSetWaitableTimer(SafeWaitHandle hTimer,
[In] ref long pDueTime, int lPeriod,
IntPtr pfnCompletionRoutine, IntPtr lpArgToCompletionRoutine,
[MarshalAs(UnmanagedType.Bool)] bool fResume);
internal static void SetWaitableTimer(SafeWaitHandle hTimer, long pDueTime,
int lPeriod, IntPtr pfnCompletionRoutine,
IntPtr lpArgToCompletionRoutine, bool fResume)
{
if (!DoSetWaitableTimer(hTimer, ref pDueTime, lPeriod,
pfnCompletionRoutine, lpArgToCompletionRoutine, fResume))
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}
}
/// <summary>
/// A manual-reset, non-periodic, waitable timer.
/// </summary>
public sealed class ManualResetTimer : WaitHandle
{
/// <summary>
/// Creates a new <see cref="ManualResetTimer"/>.
/// </summary>
public ManualResetTimer()
{
SafeWaitHandle =
NativeMethods.CreateWaitableTimer(IntPtr.Zero, true, null);
}
/// <summary>
/// Cancels the timer. This does not change the signalled state.
/// </summary>
public void Cancel()
{
NativeMethods.CancelWaitableTimer(SafeWaitHandle);
}
/// <summary>
/// Sets the timer to signal at the specified time,
/// which may be an absolute time or a relative (negative) time.
/// </summary>
/// <param name="dueTime">The time, interpreted
/// as a <see cref="FILETIME"/> value</param>
private void Set(long dueTime)
{
NativeMethods.SetWaitableTimer(SafeWaitHandle, dueTime, 0,
IntPtr.Zero, IntPtr.Zero, false);
}
/// <summary>
/// Sets the timer to signal at the specified time. Resets the signalled state.
/// </summary>
/// <param name="when">The time that this
/// timer should become signaled.</param>
public void Set(DateTime when) { Set(when.ToFileTimeUtc()); }
/// <summary>
/// Sets the timer to signal after a time span. Resets the signaled state.
/// </summary>
/// <param name="when">The time span after
/// which the timer will become signaled.</param>
public void Set(TimeSpan when) { Set(-when.Ticks); }
}
Note the following:
Wrapping Unmanaged Resources - Defining Level 0 Types for Pointers (The Intermediate Case)There are many cases where a suitable Level 0 type doesn't exist. These situations require defining a Level 0 type and then defining a Level 1 type (or type hierarchy). Defining Level 0 types is more complicated than defining Level 1 types. The example for defining simple Level 0 types is a window station object. This is one of the many resources that is represented by a single [SecurityPermission(SecurityAction.LinkDemand,
Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
[DllImport("user32.dll", EntryPoint = "CloseWindowStation",
SetLastError = true), SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CloseWindowStation(IntPtr hWinSta);
}
/// <summary>
/// Level 0 type for window station handles.
/// </summary>
public sealed class SafeWindowStationHandle : SafeHandle
{
public SafeWindowStationHandle() : base(IntPtr.Zero, true) { }
public override bool IsInvalid
{
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
get { return (handle == IntPtr.Zero); }
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[PrePrepareMethod]
protected override bool ReleaseHandle()
{
return NativeMethods.CloseWindowStation(handle);
}
}
Notes on the code:
Since a Level 0 type's Once the Level 0 type is completed, then the Level 1 type may be defined: [SecurityPermission(SecurityAction.LinkDemand,
Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
[DllImport("user32.dll", EntryPoint = "OpenWindowStation",
CharSet = CharSet.Auto, BestFitMapping = false,
ThrowOnUnmappableChar = true, SetLastError = true),
SuppressUnmanagedCodeSecurity]
private static extern SafeWindowStationHandle
DoOpenWindowStation(string lpszWinSta,
[MarshalAs(UnmanagedType.Bool)] bool fInherit,
uint dwDesiredAccess);
internal static SafeWindowStationHandle
OpenWindowStation(string lpszWinSta,
bool fInherit, uint dwDesiredAccess)
{
SafeWindowStationHandle ret =
DoOpenWindowStation(lpszWinSta, fInherit, dwDesiredAccess);
if (ret.IsInvalid)
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
return ret;
}
[DllImport("user32.dll", EntryPoint = "SetProcessWindowStation",
SetLastError = true), SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool
DoSetProcessWindowStation(SafeWindowStationHandle hWinSta);
internal static void SetProcessWindowStation(SafeWindowStationHandle hWinSta)
{
if (!DoSetProcessWindowStation(hWinSta))
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}
}
/// <summary>
/// A window station.
/// </summary>
public sealed class WindowStation : IDisposable
{
/// <summary>
/// The underlying window station handle.
/// </summary>
private SafeWindowStationHandle SafeWindowStationHandle;
/// <summary>
/// Implementation of IDisposable: closes the underlying window station handle.
/// </summary>
public void Dispose()
{
SafeWindowStationHandle.Dispose();
}
/// <summary>
/// Opens an existing window station.
/// </summary>
public WindowStation(string name)
{
// ("0x37F" is WINSTA_ALL_ACCESS)
SafeWindowStationHandle = NativeMethods.OpenWindowStation(name, false, 0x37F);
}
/// <summary>
/// Sets this window station as the active one for this process.
/// </summary>
public void SetAsActive()
{
NativeMethods.SetProcessWindowStation(SafeWindowStationHandle);
}
}
Notes:
Since the window station is a simple example, there is only one Level 1 class rather than a hierarchy of Level 1 classes. To define a hierarchy, the following code pattern should be used: /// <summary>
/// A base class for window station types.
/// </summary>
public abstract class WindowStationBase : IDisposable
{
/// <summary>
/// The underlying window station handle.
/// </summary>
protected SafeWindowStationHandle SafeWindowStationHandle { get; set; }
/// <summary>
/// Implementation of IDisposable: closes the underlying window station handle.
/// </summary>
public void Dispose()
{
DisposeManagedResources();
}
/// <summary>
/// Disposes managed resources in this class and derived classes.
/// When overriding this in a derived class,
/// be sure to call base.DisposeManagedResources()
/// </summary>
protected virtual void DisposeManagedResources()
{
SafeWindowStationHandle.Dispose();
}
}
/// <summary>
/// A window station.
/// </summary>
public sealed class WindowStation : WindowStationBase
{
/// <summary>
/// Opens an existing window station.
/// </summary>
public WindowStation(string name)
{
// ("0x37F" is WINSTA_ALL_ACCESS)
SafeWindowStationHandle =
NativeMethods.OpenWindowStation(name, false, 0x37F);
}
/// <summary>
/// Sets this window station as the active one for this process.
/// </summary>
public void SetAsActive()
{
NativeMethods.SetProcessWindowStation(SafeWindowStationHandle);
}
}
Notes:
Wrapping Unmanaged Resources - Defining Level 0 Types for Pointers with Context Data (The Advanced Case)Sometimes an unmanaged API requires additional context information in order to deallocate a resource. This requires a Level 0 type that has some additional information attached to it, and this always requires more complex interop code. The example for defining advanced Level 0 types is allocating memory in the context of another process. The other process' handle needs to be associated with the allocated memory, and it needs to be passed to the deallocation function. First, the Level 0 type: [SecurityPermission(SecurityAction.LinkDemand,
Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
[DllImport("kernel32.dll", EntryPoint = "VirtualFreeEx",
SetLastError = true), SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool VirtualFreeEx(SafeHandle hProcess,
IntPtr lpAddress, UIntPtr dwSize, uint dwFreeType);
}
/// <summary>
/// Level 0 type for memory allocated in another process.
/// </summary>
public sealed class SafeRemoteMemoryHandle : SafeHandle
{
public SafeHandle SafeProcessHandle
{
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
get;
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
private set;
}
private bool ReleaseSafeProcessHandle;
public SafeRemoteMemoryHandle() : base(IntPtr.Zero, true) { }
public override bool IsInvalid
{
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
get { return (handle == IntPtr.Zero); }
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[PrePrepareMethod]
protected override bool ReleaseHandle()
{
// (0x8000 == MEM_RELEASE)
bool ret = NativeMethods.VirtualFreeEx(SafeProcessHandle,
handle, UIntPtr.Zero, 0x8000);
if (ReleaseSafeProcessHandle)
SafeProcessHandle.DangerousRelease();
return ret;
}
/// <summary>
/// Overwrites the handle value (without releasing it).
/// This should only be called from functions acting as constructors.
/// </summary>
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
internal void SetHandle
(IntPtr handle_, SafeHandle safeProcessHandle, ref bool success)
{
handle = handle_;
SafeProcessHandle = safeProcessHandle;
SafeProcessHandle.DangerousAddRef(ref ReleaseSafeProcessHandle);
success = ReleaseSafeProcessHandle;
}
}
Notes:
The Level 1 type reveals the additional complexity needed for creating [SecurityPermission(SecurityAction.LinkDemand,
Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
[DllImport("kernel32.dll", EntryPoint = "VirtualAllocEx",
SetLastError = true), SuppressUnmanagedCodeSecurity]
private static extern IntPtr DoVirtualAllocEx(SafeHandle hProcess,
IntPtr lpAddress, UIntPtr dwSize,
uint flAllocationType, uint flProtect);
internal static SafeRemoteMemoryHandle VirtualAllocEx(SafeHandle hProcess,
IntPtr lpAddress, UIntPtr dwSize,
uint flAllocationType, uint flProtect)
{
SafeRemoteMemoryHandle ret = new SafeRemoteMemoryHandle();
bool success = false;
// Atomically get the native handle
// and assign it into our return object.
RuntimeHelpers.PrepareConstrainedRegions();
try { }
finally
{
IntPtr address = DoVirtualAllocEx(hProcess, lpAddress,
dwSize, flAllocationType, flProtect);
if (address != IntPtr.Zero)
ret.SetHandle(address, hProcess, ref success);
if (!success)
ret.Dispose();
}
// Do error handling after the CER
if (!success)
throw new Exception("Failed to set handle value");
if (ret.IsInvalid)
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
return ret;
}
[DllImport("kernel32.dll", EntryPoint = "WriteProcessMemory",
SetLastError = true), SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DoWriteProcessMemory(SafeHandle hProcess,
SafeRemoteMemoryHandle lpBaseAddress,
IntPtr lpBuffer, UIntPtr nSize, out UIntPtr lpNumberOfBytesWritten);
internal static void WriteProcessMemory(SafeRemoteMemoryHandle RemoteMemory,
IntPtr lpBuffer, UIntPtr nSize)
{
UIntPtr NumberOfBytesWritten;
if (!DoWriteProcessMemory(RemoteMemory.SafeProcessHandle, RemoteMemory,
lpBuffer, nSize, out NumberOfBytesWritten))
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
if (nSize != NumberOfBytesWritten)
throw new Exception
("WriteProcessMemory: Failed to write all bytes requested");
}
}
/// <summary>
/// Memory allocated in another process.
/// </summary>
public sealed class RemoteMemory : IDisposable
{
/// <summary>
/// The underlying remote memory handle.
/// </summary>
private SafeRemoteMemoryHandle SafeRemoteMemoryHandle;
/// <summary>
/// The associated process handle.
/// </summary>
public SafeHandle SafeProcessHandle
{ get { return SafeRemoteMemoryHandle.SafeProcessHandle; } }
/// <summary>
/// Implementation of IDisposable: closes the underlying remote memory handle.
/// </summary>
public void Dispose()
{
SafeRemoteMemoryHandle.Dispose();
}
/// <summary>
/// Allocates memory from another process.
/// </summary>
public RemoteMemory(SafeHandle process, UIntPtr size)
{
// ("0x3000" is MEM_COMMIT | MEM_RESERVE)
// ("0x04" is PAGE_READWRITE)
SafeRemoteMemoryHandle =
NativeMethods.VirtualAllocEx(process, IntPtr.Zero, size, 0x3000, 0x04);
}
/// <summary>
/// Writes to memory in another process.
/// Note: at least <paramref name="size"/> bytes starting
/// at <paramref name="buffer"/> must be pinned in memory.
/// </summary>
public void Write(IntPtr buffer, UIntPtr size)
{
NativeMethods.WriteProcessMemory(SafeRemoteMemoryHandle, buffer, size);
}
}
Notes:
Notes on how this example is simplified:
Wrapping Unmanaged Resources - Defining Level 0 Types for Non-Pointer Data (The Hard Case)There are a handful of unmanaged APIs whose handle types are not pointers. Each of these handle types may either be converted to an The example for non-pointer Level 0 types is the local atom table. There is no real reason to use this antiquated API in a modern program, but this example will illustrate how to handle APIs of this nature. The First, the Level 0 type for atoms, storing the [SecurityPermission(SecurityAction.LinkDemand,
Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
[DllImport("kernel32.dll", EntryPoint = "DeleteAtom",
SetLastError = true), SuppressUnmanagedCodeSecurity]
internal static extern ushort DeleteAtom(ushort nAtom);
}
/// <summary>
/// Level 0 type for local atoms (casting implementation).
/// </summary>
public sealed class SafeAtomHandle : SafeHandle
{
/// <summary>
/// Internal unmanaged handle value, translated to the correct type.
/// </summary>
public ushort Handle
{
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
get
{
return unchecked((ushort)(short)handle);
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
internal set
{
handle = unchecked((IntPtr)(short)value);
}
}
/// <summary>
/// Default constructor initializing with an invalid handle value.
/// </summary>
public SafeAtomHandle() : base(IntPtr.Zero, true) { }
/// <summary>
/// Whether or not the handle is invalid.
/// </summary>
public override bool IsInvalid
{
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
get { return (Handle == 0); }
}
/// <summary> | ||||||||||||||||||||