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:
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:
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:
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;
}
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.
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/
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