Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

GCHandle: when to use GCHandleType.Normal explicitly?

Reading Richter J book's section "Monitoring and Controlling the Lifetime of Objects Manually". Jeffrey says, that there are two ways how to control the lifetime of the object using GCHandle class:

  • call Alloc method with GCHandleType.Normal (GC cannot delete objs even there may be no references from the application code)
  • call Alloc method with GCHandleType.Pinned (additionally to Normal, GC cannot move such objects)

He says, that both ways can be used to pass managed object to unmanaged code. And he tries to explain, when developers should call Alloc with GCHandleType.Normal flag. I don't really understand the explanation about Normal flag usage. In both ways we don't allow GC to collect objects, which have such flags in GC descriptors table, but in case of Pinned we additionally prevent such objects to be moved during garbage collection. As I understood, in case of Normal mode, not the direct reference (memory address) is passed to unmanaged code, but just index from GC-descriptors table. And that when unmanaged code called back to managed code, then this index will be converted to the current/actual address. Mess in my head, and almost no detail information in Google and Microsoft, only copy-paste.

My questions:

  1. Some application root (not weak) references the object in the managed heap, and no more roots. Does it mean, that the corresponding entry in the GC-descriptors table will be with GCHandleType.Normal flag? Looks like no, due to Jeffrey says, that "GC cannot delete objs even there may be no are no references from the application code". But if no, which flag this table entry would have? Again, MyClass mc = new MyClass(), does corresponding entry for mc in GC-descriptors table has Normal flag, if no, so which?
  2. When (and how, please short code) developers really need to use GCHandleType.Normal flag? Pinned is more clear for me.
like image 650
trickbz Avatar asked Aug 12 '14 21:08

trickbz


3 Answers

From the documentation for GCHandle:

Provides a way to access a managed object from unmanaged memory.

If you intend to access the object from unmanaged code you need the object to be pinned. In order to get a pinned object it must be possible to marshal it to unmanaged memory.

If you only need an opaque handle to be passed to the unmanaged code so that the unmanaged code can pass it back again without accessing it, then you do not need a pinned object, but you still need to make sure it is not deleted by the garbage collector.

Consider this class:

public class MyClass
{
    DateTime dt = DateTime.Now;
}

If you try to get a pinned handle to it like this:

MyClass o = new MyClass();
GCHandle h = GCHandle.Alloc(o, GCHandleType.Pinned);

you will get an exception with the message:

Object contains non-primitive or non-blittable data.

This is because the returned handle allows you to get the address of the pinned object. In order to use this address from unmanaged code the object must be marshalled from managed to unmanaged memory.

This code does not throw an exception:

MyClass o = new MyClass();
GCHandle h = GCHandle.Alloc(o, GCHandleType.Normal);

Because you cannot use the returned handle to get the address.

So to answer your questions:

  1. A manged object (MyClass mc = new MyClass()) does not have an entry in the GC-descriptors table. It will be garbage collected when there are no references to it from managed code (I think that must be what Jeffrey Richter refers to as application code . I haven't read the book).
  2. I use GCHandleType.Normal when I need to pass an opaque handle to unmanaged code.

One scenario is a pure C API for a managed assembly. The API might look something like this:

MYHANDLE h1 = MyLib_CreateComponent();
MYHANDLE h2 = MyLib_CreateComponent();

MyLib_SetX(h1, 9.81);
double y1 = MYLib_CalcY(h1);
MyLib_SetX(h2, 3.14);
double y2 = MyLib_CalcY(h2);

printf("z = %f\n", y1 + y2);

MyLib_DestroyComponent(h1);
MyLib_DestroyComponent(h2); 

There is no direct access to the object from the C-code.

The C# implementation for the function MyLib_CreateComponent() would look like this:

 public static int CreateComponent()
 {
     MyClass instance = new MyClass();
     GCHandle gch = GCHandle.Alloc(instance, GCHandleType.Normal);
     IntPtr ip = GCHandle.ToIntPtr(h);
     h = ip.ToInt32();
     return h;
 }

Inside the managed code I would make a method to get hold of the object using the handle:

static MyClass GetObjectFromHandle(int hComp)
{
    IntPtr ip = new IntPtr(hComp);
    GCHandle h = GCHandle.FromIntPtr(ip);
    MyClass comp = h.Target as MyClass;
    return comp;
}
like image 148
Nils Lande Avatar answered Nov 11 '22 14:11

Nils Lande


If it wasn't safe passing an object reference to native code before creating a handle with GCHandleType.Normal, it will not be safe after creating such a handle either because unmanaged code requires a stable pointer. Therefore, a handle with GCHandleType.Normal does nothing with regards to unmanaged code. I believe it is a documentation bug to suggest otherwise.

GCHandleType.Normal is used by managed code to create objects that don't die. For example, some Timer classes keep instances of themselves alive so that the timer doesn't stop when you drop the last reference to it.

As I understood, in case of Normal mode, not the direct reference (memory address) is passed to unmanaged code, but just index from GC-descriptors table.

That can't be true because at the point where the PInvoke is happening there is not enough information available to tell whether a GCHandle is associated with the object you want to pass or not. The mashaler couldn't do this even if it wanted to. Also, what would unmanaged code do with a handle table entry? It does not understand it. The handle table is a CLR internal.

Some application root (not weak) references the object in the managed heap, and no more roots. Does it mean, that the corresponding entry in the GC-descriptors table will be with GCHandleType.Normal flag? Looks like no, due to Jeffrey says, that "GC cannot delete objs even there may be no are no references from the application code". But if no, which flag this table entry would have?

It has the flag that you passed in when you created that GCHandle. There are only entries in that table for which there is a GCHandle. Normal objects are not tracked.

like image 45
usr Avatar answered Nov 11 '22 15:11

usr


As a rule of thumb, you use Normal when you expect pointer to be passed back from native code (not accessed). And use Pinned when you expect pointer to be accessed from native code.

That's because when you pass Normal object into native code, you can only pass IntPtr to GCHandle (retrieved via GCHandle.ToIntPt). And it can only be resurrected back in the managed code via GCHandle.FromIntPt.

A good explanation on how it works can be found here: https://blogs.msdn.microsoft.com/jmstall/2006/10/09/gchandle-tointptr-vs-gchandle-addrofpinnedobject/

like image 3
Vasiliy Avatar answered Nov 11 '22 15:11

Vasiliy