Introduction
In my first article, I introduced the Disposable Design Principle. This is the model that Microsoft has moved to for .NET 2.0, in preference over the old IDisposable code pattern (actually, Microsoft did this in a backwards-compatible way, whereas I recommend a complete break). However, some changes are necessary to support the .NET Compact Framework (which Microsoft did not move over to the Disposable Design Principle).
A Brief Look at Shutdowns
There are three types of shutdowns possible when running framework code:
- Orderly shutdown - when a shutdown request is issued to code running normally, e.g.,
Application.Exit
. - Graceful abortive shutdown - when an
AppDomain
is unloaded: all threads are aborted and then all finalizers are run. - Rude abortive shutdown - when an
AppDomain
is rudely unloaded: all threads are aborted (with different semantics than the graceful abortive shutdown) and then finalizers are run.
My next article will go into more detail regarding shutdown situations and when to expect them. For the purposes of this article, only the following facts are necessary:
- When threads are aborted during a graceful abortive shutdown, the runtime will wait for threads to exit a Constrained Execution Region, unmanaged code, or a
finally
block. - When threads are aborted during a rude abortive shutdown, the runtime will wait for threads to exit a Constrained Execution Region or unmanaged code, but not an ordinary
finally
block.
Review of the Disposable Design Principle
This is the same description of the Disposable Design Principle as in my first article, except I've changed some of the wording to apply it to the Compact Framework (and added a note that Level 0 type constructors must be called from within an atomic execution region).
The Disposable Design Principle splits up resource management responsibilities into Level 0 types (which handle unmanaged resources), and Level 1 types (which are still small wrapper classes that closely resemble the native API, but only handle managed resources):
- An "atomic execution region" is defined as one of (in order from strongest guarantee to weakest):
- Unmanaged code - in this case, the Disposable Design Principle provides a no-leak guarantee for any type of shutdown.
- A Constrained Execution Region - in this case, the Disposable Design Principle provides a no-leak guarantee for any type of shutdown.
- A
finally
block - in this case, the Disposable Design Principle provides a no-leak guarantee for orderly and graceful abortive shutdowns, but may leak on a rude abortive shutdown. - Ordinary managed code - in this case, the Disposable Design Principle provides a no-leak guarantee for an orderly shutdown, but may leak on graceful abortive or rude abortive shutdowns.
- Level 0 types directly wrap unmanaged resources, and are only concerned with deallocation of their resource.
- Level 0 types are either
abstract
or sealed
. - Level 0 types must be designed to execute completely within an atomic execution region.
- For Constrained Execution Regions, this means that Level 0 types must be a descendant of
CriticalFinalizerObject
. - For
finally
blocks, this means that Level 0 types must derive from a separately-defined base type which implements IDisposable
to deallocate the unmanaged resource explicitly (possibly called in the context of a finally
block) or from a finalizer.
- Constructors for Level 0 types must be called from within an atomic execution region.
- The special full framework interop handling of
SafeHandle
return values is considered unmanaged code (and therefore an atomic execution region of the strongest guarantee).
- Level 1 types only deal with managed resources.
- Level 1 types are generally
sealed
unless they are defining a base Level 1 type for a Level 1 hierarchy. - Level 1 types derive from Level 1 types or from
IDisposable
directly; they do not derive from CriticalFinalizerObject
or Level 0 types. - Level 1 types may have fields that are Level 0 or Level 1 types.
- Level 1 types implement
IDisposable.Dispose
by calling Dispose
on each of its Level 0 and Level 1 fields, and then calling base.Dispose
, if applicable. - Level 1 types do not have finalizers.
- When defining a Level 1 type hierarchy, the
abstract
root base type should define a protected
property with the name and type of the associated Level 0 type.
In a future article, I will introduce Level 2 types, which allow running (almost) arbitrary shutdown code, but place additional requirements on end-users.
.NET Compact Framework Restrictions
The main difficulty in developing Level 0 types for the .NET Compact Framework is the lack of SafeHandle
. This article will present a Compact Framework version of SafeHandle
, but it will require special additional code when used in interop situations. Due to the differences between the compact and full framework support for interop (e.g., DllImportAttribute.BestFitMapping
, SuppressUnmanagedCodeSecurityAttribute
, etc.), it is generally best if no interop code is shared between the two platforms anyway.
Another difficulty is the lack of Constrained Execution Regions, although it turns out, this difficulty is really a blessing in disguise.
The Simplifying Assumption
Because there are no Constrained Execution Regions on the Compact Framework, it is not possible to write managed Level 0 types that can handle rude abortive shutdowns.
The lack of Constrained Execution Regions on the .NET Compact Framework leaves coders with one of two possibilities:
- Write an unmanaged wrapper function for every unmanaged resource allocation function, which can directly manipulate the
SafeHandle
(or derived type) implementation. Alternatively, a single unmanaged wrapper function may be written, taking a delegate (as a function pointer), but this introduces additional overhead and does not handle non-IntPtr
unmanaged resource types. - Assume that on compact platforms, if a rude abortive shutdown happens, then the whole process is going to exit.
This article makes the simplifying assumption. On the compact platform, most .NET processes have exactly one AppDomain
, so this assumption holds true. The Level 0 types developed in this article will support graceful abortive shutdowns, so they also support multiple AppDomain
s per process if their shutdowns are always graceful.
The Level 0 types in my first article supported rude abortive shutdowns through the use of Constrained Execution Regions. The Level 0 types in this article will support graceful abortive shutdowns through the use of finally
blocks.
Minimal SafeHandle for the .NET Compact Framework
The following code is a simplified SafeHandle
:
- There is no reference counting, so Compact Framework interop code must use
GC.KeepAlive
correctly to compensate. SafeHandle
instances always own their handles.- The handle does not have a separate "closed" state; the handle is closed when the
SafeHandle
is destroyed.
#if DOTNET_ISCF
namespace System.Runtime.InteropServices
{
public abstract class SafeHandle : IDisposable
{
protected IntPtr handle;
private bool disposed;
public SafeHandle(IntPtr invalidValue, bool owned)
{
handle = invalidValue;
}
private void DoDispose()
{
if (!IsInvalid)
ReleaseHandle();
}
public IntPtr DangerousGetHandle()
{
return handle;
}
public void DangerousSetHandle(IntPtr Handle)
{
handle = Handle;
}
public abstract bool IsInvalid { get; }
protected abstract bool ReleaseHandle();
public void Dispose()
{
if (!disposed)
DoDispose();
disposed = true;
GC.SuppressFinalize(this);
GC.KeepAlive(this);
}
~SafeHandle()
{
DoDispose();
}
}
}
#endif
Notes:
- The
DOTNET_ISCF
is a personal convention, indicating that compiling is being done for the Compact Framework. - The type is placed in the
System.Runtime.InteropServices
namespace. Others may disagree with this decision; I did it simply so I could share as much code as possible between the compact and regular framework platforms. - This type does have to keep track of whether or not it has been disposed, to allow
Dispose
to be called multiple times without side effects. GC.SuppressFinalize
and GC.KeepAlive
prevent the finalizer from running if Dispose
is called (or will be called). Therefore, when the finalizer runs, it knows that Dispose
was never called and will never be called.IsInvalid
and ReleaseHandle
do not run inside Constrained Execution Regions. However, since they may be run in a finalizer context, many of the same restrictions apply. E.g., they should not raise exceptions or access any managed objects.
This simplified SafeHandle
may now be used to define Level 1 types. Before starting, there are a handful of helper functions that help out with P/Invoke on the Compact Framework:
public static class Interop
{
public static int HRFromWin32Error(int win32Error)
{
if (win32Error <= 0)
return win32Error;
uint ret = unchecked((uint)win32Error);
ret &= 0xFFFF;
ret |= (7 << 16);
ret |= 0x80000000;
return unchecked((int)ret);
}
#if DOTNET_ISCF
public static void ThrowExceptionForLastWin32Error()
{
Marshal.ThrowExceptionForHR(HRFromWin32Error(Marshal.GetLastWin32Error()));
}
#else
public static void ThrowExceptionForLastWin32Error()
{
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}
#endif
}
Wrapping Unmanaged Resources - Using Existing "Level 0" Types (The Easy Case)
Unfortunately, on the Compact Framework, WaitHandle
is not a true Level 1 type. By deriving from it, however, we can still create a ManualResetTimer
that acts like a Level 1 type.
#if DOTNET_ISCF
internal static partial class NativeMethods
{
[DllImport("kernel32.dll", EntryPoint =
"CreateWaitableTimer", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr DoCreateWaitableTimer(IntPtr lpTimerAttributes,
[MarshalAs(UnmanagedType.Bool)] bool bManualReset, string lpTimerName);
internal static void CreateWaitableTimer(IntPtr lpTimerAttributes,
bool bManualReset, string lpTimerName, WaitHandle ret)
{
try { }
finally
{
ret.Handle = DoCreateWaitableTimer(lpTimerAttributes,
bManualReset, lpTimerName);
}
if (ret.Handle == IntPtr.Zero)
Interop.ThrowExceptionForLastWin32Error();
}
[DllImport("kernel32.dll", EntryPoint =
"CancelWaitableTimer", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DoCancelWaitableTimer(IntPtr hTimer);
internal static void CancelWaitableTimer(WaitHandle hTimer)
{
bool ret = DoCancelWaitableTimer(hTimer.Handle);
GC.KeepAlive(hTimer);
if (!ret)
Interop.ThrowExceptionForLastWin32Error();
}
[DllImport("kernel32.dll", EntryPoint =
"SetWaitableTimer", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DoSetWaitableTimer(IntPtr hTimer,
[In] ref long pDueTime, int lPeriod,
IntPtr pfnCompletionRoutine, IntPtr lpArgToCompletionRoutine,
[MarshalAs(UnmanagedType.Bool)] bool fResume);
internal static void SetWaitableTimer(WaitHandle hTimer,
long pDueTime, int lPeriod, IntPtr pfnCompletionRoutine,
IntPtr lpArgToCompletionRoutine, bool fResume)
{
bool ret = DoSetWaitableTimer(hTimer.Handle, ref pDueTime,
lPeriod, pfnCompletionRoutine, lpArgToCompletionRoutine, fResume);
GC.KeepAlive(hTimer);
if (!ret)
Interop.ThrowExceptionForLastWin32Error();
}
}
#else
[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 void CreateWaitableTimer(IntPtr lpTimerAttributes,
bool bManualReset, string lpTimerName, WaitHandle ret)
{
ret.SafeWaitHandle = DoCreateWaitableTimer(lpTimerAttributes,
bManualReset, lpTimerName);
if (ret.SafeWaitHandle.IsInvalid)
Interop.ThrowExceptionForLastWin32Error();
}
[DllImport("kernel32.dll", EntryPoint = "CancelWaitableTimer",
SetLastError = true), SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DoCancelWaitableTimer(SafeWaitHandle hTimer);
internal static void CancelWaitableTimer(WaitHandle hTimer)
{
if (!DoCancelWaitableTimer(hTimer.SafeWaitHandle))
Interop.ThrowExceptionForLastWin32Error();
}
[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(WaitHandle hTimer,
long pDueTime, int lPeriod, IntPtr pfnCompletionRoutine,
IntPtr lpArgToCompletionRoutine, bool fResume)
{
if (!DoSetWaitableTimer(hTimer.SafeWaitHandle, ref pDueTime,
lPeriod, pfnCompletionRoutine, lpArgToCompletionRoutine, fResume))
Interop.ThrowExceptionForLastWin32Error();
}
}
#endif
public sealed class ManualResetTimer : WaitHandle
{
public ManualResetTimer()
{
NativeMethods.CreateWaitableTimer(IntPtr.Zero, true, null, this);
}
public void Cancel()
{
NativeMethods.CancelWaitableTimer(this);
}
private void Set(long dueTime)
{
NativeMethods.SetWaitableTimer(this, dueTime, 0,
IntPtr.Zero, IntPtr.Zero, false);
}
public void Set(DateTime when) { Set(when.ToFileTimeUtc()); }
public void Set(TimeSpan when) { Set(-when.Ticks); }
}
Notes:
- The full framework interop code was modified so it has the same method signatures as the Compact Framework interop code. This enables writing one
ManualResetTimer
for both platforms. - For the resource allocation function (
NativeMethods.CreateWaitableTimer
), a finally
block was used to atomically allocate the resource and assign it into the handle value in the Level 0 type (remember, on the Compact Framework, WaitHandle
does not follow the Disposable Design Principle, so it has some aspects of a Level 0 type and other aspects of a Level 1 type). Allocation errors are handled after the finally
block; this is not required, but it is good programming practice. - For interop functions that take resource handles as arguments,
GC.KeepAlive
is used to ensure the WaitHandle
is not disposed from a finalizer while the handle is being used by unmanaged code. Again, error handling is put off until after GC.KeepAlive
; this is not required, but is good practice.
Since WaitHandle
is not a Level 1 type on the Compact Framework, this example was more of a special-case scenario. The following examples result in true Level 1 types.
Wrapping Unmanaged Resources - Defining Level 0 Types for Pointers (The Intermediate Case)
The Level 0 type is a straightforward translation from the first article's example:
#if DOTNET_ISCF
internal static partial class NativeMethods
{
[DllImport("user32.dll", EntryPoint =
"CloseWindowStation", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CloseWindowStation(IntPtr hWinSta);
}
#else
[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);
}
#endif
public sealed class SafeWindowStationHandle : SafeHandle
{
public SafeWindowStationHandle() : base(IntPtr.Zero, true) { }
public override bool IsInvalid
{
#if !DOTNET_ISCF
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
#endif
get { return (handle == IntPtr.Zero); }
}
#if !DOTNET_ISCF
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[PrePrepareMethod]
#endif
protected override bool ReleaseHandle()
{
return NativeMethods.CloseWindowStation(handle);
}
}
There are no significant changes other than the removal of the Code Access Security and Constrained Execution Region attributes, which are not supported on the Compact Framework.
The Level 1 type shows the pattern needed for Compact Framework interop calls:
#if DOTNET_ISCF
internal static partial class NativeMethods
{
[DllImport("user32.dll", EntryPoint = "OpenWindowStation",
CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr DoOpenWindowStation(string lpszWinSta,
[MarshalAs(UnmanagedType.Bool)] bool fInherit, uint dwDesiredAccess);
internal static SafeWindowStationHandle
OpenWindowStation(string lpszWinSta,
bool fInherit, uint dwDesiredAccess)
{
SafeWindowStationHandle ret = new SafeWindowStationHandle();
try { }
finally
{
ret.DangerousSetHandle(DoOpenWindowStation(lpszWinSta,
fInherit, dwDesiredAccess));
}
if (ret.IsInvalid)
Interop.ThrowExceptionForLastWin32Error();
return ret;
}
[DllImport("user32.dll", EntryPoint =
"SetProcessWindowStation", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DoSetProcessWindowStation(IntPtr hWinSta);
internal static void SetProcessWindowStation(SafeWindowStationHandle hWinSta)
{
bool ok = DoSetProcessWindowStation(hWinSta.DangerousGetHandle());
GC.KeepAlive(hWinSta);
if (!ok)
Interop.ThrowExceptionForLastWin32Error();
}
}
#else
[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)
Interop.ThrowExceptionForLastWin32Error();
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))
Interop.ThrowExceptionForLastWin32Error();
}
}
#endif
public sealed class WindowStation : IDisposable
{
private SafeWindowStationHandle SafeWindowStationHandle;
public void Dispose()
{
SafeWindowStationHandle.Dispose();
}
public WindowStation(string name)
{
SafeWindowStationHandle = NativeMethods.OpenWindowStation(name, false, 0x37F);
}
public void SetAsActive()
{
NativeMethods.SetProcessWindowStation(SafeWindowStationHandle);
}
}
Notes:
- The resource allocation function follows a pattern which should now be familiar:
- Allocate the return object (initialized to the invalid handle value).
- Within a
finally
block, make the allocation, and atomically assign it into the return object. - Do error checking after the atomic region.
- Likewise, the functions that take Level 0 type parameters follow a similar pattern:
- Call the native function, but do not handle errors yet.
- Call
GC.KeepAlive
to make sure the Level 0 type is not garbage collected while it's being used by the unmanaged code. - Perform error handling.
Wrapping Unmanaged Resources - Defining Level 0 Types for Pointers With Context Data (The "Advanced" Case)
This time, the Level 0 resource deallocation function does have a small twist:
#if DOTNET_ISCF
internal static partial class NativeMethods
{
[DllImport("kernel32.dll",
EntryPoint = "VirtualFreeEx", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DoVirtualFreeEx(IntPtr hProcess,
IntPtr lpAddress, UIntPtr dwSize, uint dwFreeType);
internal static bool VirtualFreeEx(SafeHandle hProcess,
IntPtr lpAddress, UIntPtr dwSize, uint dwFreeType)
{
bool ret = DoVirtualFreeEx(hProcess.DangerousGetHandle(),
lpAddress, dwSize, dwFreeType);
GC.KeepAlive(hProcess);
return ret;
}
}
#else
[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);
}
#endif
public sealed class SafeRemoteMemoryHandle : SafeHandle
{
public SafeHandle SafeProcessHandle
{
#if !DOTNET_ISCF
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
#endif
get;
#if !DOTNET_ISCF
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
#endif
private set;
}
public SafeRemoteMemoryHandle() : base(IntPtr.Zero, true) { }
public override bool IsInvalid
{
#if !DOTNET_ISCF
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
#endif
get { return (handle == IntPtr.Zero); }
}
#if !DOTNET_ISCF
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[PrePrepareMethod]
#endif
protected override bool ReleaseHandle()
{
return NativeMethods.VirtualFreeEx(SafeProcessHandle,
handle, UIntPtr.Zero, 0x8000);
}
#if !DOTNET_ISCF
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
#endif
internal void SetHandle(IntPtr handle_, SafeHandle safeProcessHandle)
{
handle = handle_;
SafeProcessHandle = safeProcessHandle;
}
}
The resource deallocation function for the memory takes as an argument the process handle (which, remember, really should be a SafeProcessHandle
instead of SafeHandle
). Because it takes a safe handle as an argument, it has to give it the special GC.KeepAlive
treatment.
The interop code for the Level 1 type has the logical changes:
#if DOTNET_ISCF
internal static partial class NativeMethods
{
[DllImport("kernel32.dll", EntryPoint = "VirtualAllocEx", SetLastError = true)]
private static extern IntPtr DoVirtualAllocEx(IntPtr 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();
try { }
finally
{
IntPtr address = DoVirtualAllocEx(hProcess.DangerousGetHandle(),
lpAddress, dwSize, flAllocationType, flProtect);
GC.KeepAlive(hProcess);
if (address != IntPtr.Zero)
ret.SetHandle(address, hProcess);
}
if (ret.IsInvalid)
Interop.ThrowExceptionForLastWin32Error();
return ret;
}
[DllImport("kernel32.dll", EntryPoint =
"WriteProcessMemory", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DoWriteProcessMemory(IntPtr hProcess,
IntPtr lpBaseAddress, IntPtr lpBuffer, UIntPtr nSize,
out UIntPtr lpNumberOfBytesWritten);
internal static void WriteProcessMemory(SafeRemoteMemoryHandle
RemoteMemory, IntPtr lpBuffer, UIntPtr nSize)
{
UIntPtr NumberOfBytesWritten;
bool ok = DoWriteProcessMemory(RemoteMemory.SafeProcessHandle.DangerousGetHandle(),
RemoteMemory.DangerousGetHandle(),
lpBuffer, nSize, out NumberOfBytesWritten);
GC.KeepAlive(RemoteMemory);
if (!ok)
Interop.ThrowExceptionForLastWin32Error();
if (nSize != NumberOfBytesWritten)
throw new Exception("WriteProcessMemory: " +
"Failed to write all bytes requested");
}
}
#else
[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();
RuntimeHelpers.PrepareConstrainedRegions();
try { }
finally
{
IntPtr address = DoVirtualAllocEx(hProcess, lpAddress,
dwSize, flAllocationType, flProtect);
if (address != IntPtr.Zero)
ret.SetHandle(address, hProcess);
}
if (ret.IsInvalid)
Interop.ThrowExceptionForLastWin32Error();
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))
Interop.ThrowExceptionForLastWin32Error();
if (nSize != NumberOfBytesWritten)
throw new Exception("WriteProcessMemory: " +
"Failed to write all bytes requested");
}
}
#endif
The only noteworthy element is the fact that the Compact Framework NativeMethods.VirtualAllocEx
does not have to call GC.KeepAlive
on the process handle, because it is contained by the SafeRemoteMemoryHandle
, which is being kept alive.
The RemoteMemory
Level 1 type itself is unchanged from the previous article.
Wrapping Unmanaged Resources - Defining Level 0 Types for Non-Pointer Data (The "Hard" Case)
The Level 0 type is completely straightforward:
#if DOTNET_ISCF
internal static partial class NativeMethods
{
[DllImport("kernel32.dll", EntryPoint = "DeleteAtom", SetLastError = true)]
internal static extern ushort DeleteAtom(ushort nAtom);
}
#else
[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);
}
#endif
public sealed class SafeAtomHandle : SafeHandle
{
public ushort Handle
{
#if !DOTNET_ISCF
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
#endif
get
{
return unchecked((ushort)(short)handle);
}
#if !DOTNET_ISCF
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
#endif
internal set
{
handle = unchecked((IntPtr)(short)value);
}
}
public SafeAtomHandle() : base(IntPtr.Zero, true) { }
public override bool IsInvalid
{
#if !DOTNET_ISCF
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[PrePrepareMethod]
#endif
get { return (Handle == 0); }
}
#if !DOTNET_ISCF
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
[PrePrepareMethod]
#endif
protected override bool ReleaseHandle()
{
return (NativeMethods.DeleteAtom(Handle) == 0);
}
}
Finally, the Level 1 interop is actually simpler for the Compact Framework; there are no surprises here:
#if DOTNET_ISCF
internal static partial class NativeMethods
{
[DllImport("kernel32.dll", EntryPoint = "AddAtom",
CharSet = CharSet.Auto, SetLastError = true)]
private static extern ushort DoAddAtom(string lpString);
internal static SafeAtomHandle AddAtom(string lpString)
{
SafeAtomHandle ret = new SafeAtomHandle();
try { }
finally
{
ushort atom = DoAddAtom(lpString);
if (atom != 0)
ret.Handle = atom;
}
if (ret.IsInvalid)
Interop.ThrowExceptionForLastWin32Error();
return ret;
}
[DllImport("kernel32.dll", EntryPoint = "GetAtomName",
CharSet = CharSet.Auto, SetLastError = true)]
private static extern uint DoGetAtomName(ushort nAtom,
StringBuilder lpBuffer, int nSize);
internal static string GetAtomName(SafeAtomHandle atom)
{
StringBuilder sb = new StringBuilder(255);
uint ret = 0;
ret = DoGetAtomName(atom.Handle, sb, 256);
GC.KeepAlive(atom);
if (ret == 0)
Interop.ThrowExceptionForLastWin32Error();
sb.Length = (int)ret;
return sb.ToString();
}
}
#else
[SecurityPermission(SecurityAction.LinkDemand,
Flags = SecurityPermissionFlag.UnmanagedCode)]
internal static partial class NativeMethods
{
[DllImport("kernel32.dll", EntryPoint =
"AddAtom", CharSet = CharSet.Auto, BestFitMapping = false,
ThrowOnUnmappableChar = true, SetLastError = true), SuppressUnmanagedCodeSecurity]
private static extern ushort DoAddAtom(string lpString);
internal static SafeAtomHandle AddAtom(string lpString)
{
SafeAtomHandle ret = new SafeAtomHandle();
RuntimeHelpers.PrepareConstrainedRegions();
try { }
finally
{
ushort atom = DoAddAtom(lpString);
if (atom != 0)
ret.Handle = atom;
}
if (ret.IsInvalid)
Interop.ThrowExceptionForLastWin32Error();
return ret;
}
[DllImport("kernel32.dll", EntryPoint = "GetAtomName",
CharSet = CharSet.Auto, BestFitMapping = false,
ThrowOnUnmappableChar = true, SetLastError = true),
SuppressUnmanagedCodeSecurity]
private static extern uint DoGetAtomName(ushort nAtom,
StringBuilder lpBuffer, int nSize);
internal static string GetAtomName(SafeAtomHandle atom)
{
StringBuilder sb = new StringBuilder(255);
uint ret = 0;
bool success = false;
RuntimeHelpers.PrepareConstrainedRegions();
try { }
finally
{
atom.DangerousAddRef(ref success);
if (success)
{
ret = DoGetAtomName(atom.Handle, sb, 256);
atom.DangerousRelease();
}
}
if (!success)
throw new Exception("SafeHandle.DangerousAddRef failed");
if (ret == 0)
Interop.ThrowExceptionForLastWin32Error();
sb.Length = (int)ret;
return sb.ToString();
}
}
#endif
Note that for the "hard" cases, where reference counting has to be done manually anyway, it is easier (and perfectly valid) to do it the Compact Framework way. Here's an alternative implementation of GetAtomName
for the full framework:
internal static string GetAtomName(SafeAtomHandle atom)
{
StringBuilder sb = new StringBuilder(255);
uint ret = 0;
ret = DoGetAtomName(atom.Handle, sb, 256);
GC.KeepAlive(atom);
if (ret == 0)
Interop.ThrowExceptionForLastWin32Error();
sb.Length = (int)ret;
return sb.ToString();
}
Again, the LocalAtom
Level 1 type is exactly the same.
A Note On SafeHandle's Reference Counting
The Compact Framework SafeHandle
defined in this article is actually more akin to the full framework CriticalHandle
type. I chose to go with the SafeHandle
name anyway so that the Level 0 types are at least mostly portable across frameworks.
SafeHandle
's reference counting is the reason that they should be used instead of IntPtr
in the full framework P/Invoke declarations: the default marshaler will automatically increment the reference count before the call, and decrement it after. The Compact Dramework SafeHandle
's lack of reference counting is the reason a GC.KeepAlive
is necessary for Level 0 argument types.
For those who may be wondering: it is possible to use CriticalHandle
instead of SafeHandle
on the full framework. Since it lacks reference counting, GC.KeepAlive
must be called when it is used as an argument. It still does get the special (atomic) treatment when used as a return value, though. If this special treatment is ignored, and IntPtr
is used on all P/Invoke declarations, then a CriticalHandle
-based Disposable Design System could be fully portable between the compact and full frameworks. This technique could be useful for library writers.
However, I still prefer to use SafeHandle
on the full framework code, and keep the full and Compact Framework interop code separated. This takes full advantage of the special interop functionality of SafeHandle
, simplifying the interop necessary for the full framework platform.
Constrained Execution Regions vs. Finally Blocks for Atomic Execution
Constrained Execution Regions provide atomic execution, even in the face of a rude abortive shutdown. finally
blocks only provide atomic execution with graceful abortive shutdowns.
Any process-wide (i.e., cross-AppDomain
) resource management type should be resilient to rude abortive shutdowns (i.e., not leak). On the Compact Framework, this requirement is softened (by necessity) to being resilient to graceful abortive shutdowns. AppDomain
-centric resource management types only need to be resilient to orderly shutdowns, but these types of resources are very rare (GCHandle
is the only one that comes to mind).
It may be tempting to forego the more complex CERs in favor of simpler finally
blocks or no code protection at all, but consider carefully your hosting requirements before you do this. If the code will be used only in the default AppDomain
of a Windows Forms/Service/Console application, then you may be able to safely assume that any asynchronous exception (ThreadAbortException
, CannotUnloadAppDomainException
, StackOverflowException
, OutOfMemoryException
, etc.) is drastic enough that they will result in process termination. In that case, no atomic regions are necessary at all.
However (and this is especially important if one writes general library code), it is possible to write host-agnostic classes by using the strongest atomic execution regions available. The complex CERs, which are meaningless to a Windows Forms application, become critical if the same code is used in ASP.NET.
References and Further Reading
These following links present the case that some code does need to be protected by CERs. On the Compact Framework, this translates into being protected by finally
blocks (or unmanaged code, if rude abortive shutdowns really need to be supported).
- Chris Brumme, Reliability (2003-06-23). Note that this blog post discusses the pre-2.0 .NET platform, so some of the details no longer apply. However, it is an excellent discussion of the difficulties of reliability. Near the end, he mentions "Perhaps 0.01% of code will be especially hardened using not-yet-available techniques. This code will be used to guarantee that all resources are cleaned up during an AppDomain unload..." - this quote refers to the modern CERs for the full framework and the
finally
block for the Compact Framework. - MSDN, Reliability Best Practices.
Afterword
The usual thanks apply: thanks to Mandy for proofreading (especially with the wedding planning craziness now), and thanks to God for everything!
I plan to cover shutdown scenarios in more detail in my next article, and introduce Level 2 types which support shutdown logic. In all honesty, though, I'm getting married on October 4th and taking the following week off, so don't look for my third article anytime soon. :)
Stephen Cleary is a Christian, husband, father, and programmer living in Northern Michigan.
Personal home page (including blog): http://www.stephencleary.com/