SafeHandle and HandleRef

2020-02-12 08:12发布

问题:

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:

  1. Obviously, I want to confirm both. So how do I get that functionality? (This is the main question.)
  2. From here and From MSDN ("call a managed object") it looks like only someObject.Handle might be GC'd, while a free standing IntPtr will not. But an IntPtr itself is managed!
  3. How can an IntPtr be GC'd before it goes out of scope (as mentioned here)?

回答1:

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 IntPtrs.

The important part of the HandleRef docs is this:

The HandleRef constructor takes two parameters: an Object representing the wrapper, and an IntPtr 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:

  1. 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.

  2. IntPtr is a struct, as stated above, so it's not collected by the GC.

  3. 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.