W ostatnim poście pisałem jak prawidłowo wykonać finalizację obiektu jeśli mowa o zasobach niezarządzanych, których zwolnienie jest krytyczne. Dzisiaj o obiekcie, który jest bardzo często wykorzystywany w sytuacjach gdzie należy przechowywać wskaźnik do zasobów niezarządzanych. Zacznijmy od jego definicji:
[SecurityPermissionAttribute(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
public abstract class SafeHandle : CriticalFinalizerObject, IDisposable
Co to oznacza? Wszystkie rzeczy jakie daje nam CriticalFinalizerObject (patrz poprzedni post), SafeHandle również zawiera. Zatem mamy do dyspozycji CER co oznacza w praktyce bezpieczny finalizer. Oczywiście klasa również implementuje IDisposable wiec możemy korzystać z metody Dispose lub using dzięki czemu mamy większa kontrolę nad zasobami. Klasa jest jednak abstrakcyjna więc bezpośrednio nie możemy jej użyć. W praktyce jednak korzystać będziemy z innych klas, które dziedziczą po SafeHandle i implementują potrzebne metody. Przyjrzymy się teraz abstrakcyjnym elementom SafeHandle:
public abstract class SafeHandle : CriticalFinalizerObject, IDisposable
{
protected abstract Boolean ReleaseHandle();
public abstract Boolean IsInvalid{get;}
//... (reszta kodu)
}
W ReleaseHandle umieszczamy kod, który zwalnia zasoby, z kolei IsInvalid zwraca flagę określającą czy uchwyt (wskaźnik) do zasobów niezarządzanych jest prawidłowy. Zwykle uchwyty są nieprawidłowe gdy mają wartość 0 lub –1 – tak działa WinApi. Z tego względu, .NET dostarcza dodatkową klasę SafeHandleZeroOrMinusOneIsInvalid, która przeładowuje w odpowiedni sposób IsInvalid:
public abstract class SafeHandleZeroOrMinusOneIsInvalid : SafeHandle
{
//...
// Implementaca Invalid prawdopodobnie wygląda następująco
public override Boolean IsInvalid
{
get
{
if (base.handle == IntPtr.Zero) return true;
if (base.handle == (IntPtr) (-1)) return true;
return false;
}
}
//...
}
Z tego względu, jeśli musimy napisać jakiś własny SafeHandle wtedy lepiej skorzystać z powyższej klasy ponieważ prawie zawsze implementacja Invalid będzie wyglądać jak przedstawiono to w powyższym przykładzie. Oczywiście klasa jest wciąż abstrakcyjna ponieważ należy przeładować i zaimplementować ReleaseHandle. Microsoft dostarcza kilka implementacji:
System.Object
System.Runtime.ConstrainedExecution.CriticalFinalizerObject
System.Runtime.InteropServices.SafeHandle
Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid
Microsoft.Win32.SafeHandles.SafeFileHandle
Microsoft.Win32.SafeHandles.SafeMemoryMappedFileHandle
Microsoft.Win32.SafeHandles.SafeNCryptHandle
Microsoft.Win32.SafeHandles.SafePipeHandle
Microsoft.Win32.SafeHandles.SafeRegistryHandle
Microsoft.Win32.SafeHandles.SafeWaitHandle
System.Runtime.InteropServices.SafeBuffer
System.Security.Authentication.ExtendedProtection.ChannelBinding
Jak widać, mamy implementacje takie jak SafeFileHandle, SafeWaitHandle czy SafeRegistryHandle. Zajrzyjmy do implementacji ReleaseHandle klasy SafeFileHandle (można użyć do tego np. .NET Reflector):
[SecurityCritical]
protected override bool ReleaseHandle()
{
return Win32Native.CloseHandle(base.handle);
}
Jak wygląda SafeWaitHandle?
[SecurityCritical]
protected override bool ReleaseHandle()
{
if (!this.bIsMutex || Environment.HasShutdownStarted)
{
return Win32Native.CloseHandle(base.handle);
}
bool flag = false;
bool bHandleObtained = false;
try
{
if (!this.bIsReservedMutex)
{
Mutex.AcquireReservedMutex(ref bHandleObtained);
}
flag = Win32Native.CloseHandle(base.handle);
}
finally
{
if (bHandleObtained)
{
Mutex.ReleaseReservedMutex();
}
}
return flag;
}
Jak widać, wygląda dość podobnie… Win32Native.CloseHandle służy do zwolnienia uchwytu. Microsoft zdecydował się na taki krok ze względu na przejrzystość API i aby uniemożliwić przekazanie SafeFileHandle gdy metoda spodziewa się SafeWaitHandle.
Jeśli powyższe wprowadzenie teoretyczne jest niejasne, to warto przeanalizować praktyczny przykład i wtedy powrócić ponownie do opisanych mechanizmów. Załóżmy, że chcemy skorzystać z niezarządzanego mutex’a, który znajduje się w kernel32.dll. Zaglądając do dokumentacji, dowiemy się o sygnaturze metody CreateMutex:
HANDLE WINAPI CreateMutex(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_ BOOL bInitialOwner,
_In_opt_ LPCTSTR lpName
);
Widzimy, że funkcja zwraca uchwyt do nowego mutex’a. Jeśli chcemy napisać brzydki kod, wtedy moglibyśmy zaimportować funkcję następująco:
internal class Program
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "CreateMutex")]
private static extern IntPtr CreateUglyMutex(IntPtr lpMutexAttributes, bool bInitialOwner, string lpName);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ReleaseMutex")]
public static extern bool ReleaseUglyMutex(IntPtr hMutex);
public static void Main()
{
IntPtr mutex = IntPtr.Zero;
try
{
mutex = CreateUglyMutex(IntPtr.Zero, true, "Nazwa");
}
finally
{
if (mutex != IntPtr.Zero)
ReleaseUglyMutex(mutex);
}
}
}
Powyższa implementacja używa czystego wskaźnika (IntPtr) i z tego względu może okazać się to niebezpieczne. Co w przypadku gdy CreateUglyMutex stworzy obiekt ale nie przepisze go zmiennej mutex? Sytuacja jest jak najbardziej możliwa w przypadku ThreadAbortException. Z tego względu lepiej korzystać z SafeHandle. Jeśli takie niebezpieczeństwa nie są zrozumiałe, zachęcam do przeczytania poprzednich wpisów o CriticalFinalizerObject oraz CER. Dużo lepsze rozwiązanie to:
internal class Program
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "CreateMutex")]
private static extern SafeWaitHandle CreateMutex(IntPtr lpMutexAttributes, bool bInitialOwner, string lpName);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ReleaseMutex")]
public static extern bool ReleaseMutex(SafeWaitHandle hMutex);
public static void Main()
{
SafeWaitHandle mutex = null;
try
{
mutex = CreateMutex(IntPtr.Zero, true, "Nazwa");
}
finally
{
if (mutex !=null&&!mutex.IsInvalid)
ReleaseMutex(mutex);
}
}
}
Kod jest dużo bardziej bezpieczny. SafeHandle implementuje IDisposable oraz destruktory (finalizers). Z tego względu, zawsze zostanie uchwyt zwolniony bo w końcu GC musi w pewnym momencie zebrać każdy niedostępny obiekt.
Ponadto, SafeHandle posiada pewne zabezpieczenie gdy przekazuje uchwyt do niezarządzanej funkcji, o której GC nic w końcu nie wie. Może okazać się , że przekazujemy uchwyt do takiej funkcji a potem jakiś inny wątek chce zwolnić uchwyt. W momencie gdy SafeHandle jest przekazywany do niezarządzanej funkcji, wewnętrzny licznik jest zwiększany, co oznacza, że obiekt jest gdzieś wciąż używany i nie może zostać usunięty z pamięci. Gdy funkcja kończy swoje działanie, licznik jest z powrotem zmniejszany o jeden. Licznik można kontrolować również samemu poprzez metody DangerousAddRef, DangerousRelease oraz DangerousGetHandle. Dostępna jest również klasa CriticalHandle, która nie implementuje przedstawionego mechanizmu z wewnętrznym licznikiem. Skutkuje to większą wydajnością ale mniejszym bezpieczeństwem. Microsoft sugeruje aby w pierwszej kolejności używać SafeHandle z licznikiem.