After reading about both, including a high voted answer on this site, I still find this a bit unclear.
Since my understanding of the matter might be wrong, I'll first post a synopsis of what I know so I can be corrected if I'm wrong, and then post my specific questions:
Sometimes when coding managed code, we have to pass an address to unmanaged code. That's what an IntPtr is for. However, we try to make sure of two opposite things: a) Keep that pointer (to an address) alive from the GC. b) Release it when it's not needed (even if we forget to do that explicitly).
HandleRef does the first, and SafeHandle the second. (I'm actually referring here to derivations of SafeHandle listed here).
My questions:
someObject.Handle
might
be GC'd, while a free standing IntPtr
will not. But an IntPtr
itself
is managed!I think you're confusing pointers (IntPtr
or void*
) with handles (a reference to a Windows object). Unfortunately, handles can be represented with an IntPtr
type, which can be confusing.
SafeHandle
is for dealing with handles specifically. A handle is not a pointer but an index in a system-provided table (sort of - it's meant to be opaque). For instance, the CreateFile
function returns a HANDLE
, which would be suitable to use with a SafeFileHandle
. The SafeHandle
class is itself a wrapper around a Windows handle, it will free the Windows handle when the SafeHandle
is finalized. So you have to make sure a reference to the SafeHandle
object is kept as long as you want to use the handle.
A pointer is just a value. It's the address of an object in memory. IntPtr
is a struct
, and the struct
semantics will make it be passed by value (that is, every time you pass an IntPtr
to a function you actually make a copy of the IntPtr
). Unless boxed, the GC won't even know about your IntPtr
s.
The important part of the HandleRef
docs is this:
The
HandleRef
constructor takes two parameters: anObject
representing the wrapper, and anIntPtr
representing the unmanaged handle. The interop marshaler passes only the handle to unmanaged code, and guarantees that the wrapper (passed as the first parameter to the constructor of the HandleRef) remains alive for the duration of the call.
Let's take the MSDN example:
FileStream fs = new FileStream("HandleRef.txt", FileMode.Open);
HandleRef hr = new HandleRef(fs, fs.SafeFileHandle.DangerousGetHandle());
StringBuilder buffer = new StringBuilder(5);
int read = 0;
// platform invoke will hold reference to HandleRef until call ends
LibWrap.ReadFile(hr, buffer, 5, out read, 0);
Console.WriteLine("Read with struct parameter: {0}", buffer);
LibWrap.ReadFile2(hr, buffer, 5, out read, null);
Console.WriteLine("Read with class parameter: {0}", buffer);
This is equivalent to:
FileStream fs = new FileStream("HandleRef.txt", FileMode.Open);
var hf = fs.SafeFileHandle.DangerousGetHandle();
StringBuilder buffer = new StringBuilder(5);
int read = 0;
LibWrap.ReadFile(hf, buffer, 5, out read, 0);
Console.WriteLine("Read with struct parameter: {0}", buffer);
LibWrap.ReadFile2(hf, buffer, 5, out read, null);
Console.WriteLine("Read with class parameter: {0}", buffer);
// Since we no more have a HandleRef, the following line is needed:
GC.KeepAlive(fs);
But a better solution in this particular case would be:
using(FileStream fs = new FileStream("HandleRef.txt", FileMode.Open))
{
StringBuilder buffer = new StringBuilder(5);
int read = 0;
LibWrap.ReadFile(fs.SafeFileHandle, buffer, 5, out read, 0);
Console.WriteLine("Read with struct parameter: {0}", buffer);
LibWrap.ReadFile2(fs.SafeFileHandle, buffer, 5, out read, null);
Console.WriteLine("Read with class parameter: {0}", buffer);
}
To sum up:
For handles, use SafeHandle
and make sure it's reachable until you don't need it anymore, at which point you either let the GC collect it or you dispose it explicitly (by calling the Dispose()
method).
For pointers, you make sure the pointed-to memory is pinned the whole time the native code can access it. You can use the fixed
keyword or a pinned GCHandle
to achieve this.
IntPtr
is a struct
, as stated above, so it's not collected by the GC.
It's not the IntPtr
that's collected, it's the HWnd
object that's exposing it that's no longer reachable at this point and is collectable by the GC. When finalized, it disposes the handle.
The code from the referenced answer is:
HWnd a = new HWnd();
IntPtr h = a.Handle;
// The GC can kick in at this point and collect HWnd,
// because it's not referenced after this line.
// If it does, HWnd's finalizer could run.
// If it runs, HWnd will dispose the handle.
// If the handle is disposed, h will hold a freed handle value,
// which is invalid. It still has the same numerical value, but
// Windows will already have freed the underlying object.
// Being a value type, h itself has nothing to do with the GC.
// It's not collectable. Think of it like it were an int.
B.SendMessage(h, ...);
// Adding GC.KeepAlive(a) here solves this issue.
As for the object reachability rules, an object is considered as no longer used as soon as there's no more reachable references to the object. In the previous example, just after the IntPtr h = a.Handle;
line, there is no other later usage of the a
variable, therefore it is assumed this object is no longer used and can be freed anytime. GC.KeepAlive(a)
creates such an usage, so the object remains alive (the real thing is a bit more involved since usage tracking is done by the JIT but this is good enough for this explanation).
SafeHandle does not include a safety measure like HandleRef. Correct?
Good question. I suppose the P/Invoke marshaler will keep the handle alive for the duration of the call, but its owning object (like HWnd
) could still dispose it explicitly during the call if it's finalized. This is the safety measure that HandleRef
provides, and you won't get it with SafeHandle
alone. You need to ensure the handle owner (HWnd
in the previous example) is kept alive yourself.
But the primary goal of HandleRef
is to wrap an IntPtr
, which is the old method of storing a handle value. Now, SafeHandle
is preferred to IntPtr
for handle storage anyway. You just have to ensure the handle owner won't dispose the handle explicitly during the P/Invoke call.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With