Managed and Unmanaged Code

In this section, we will examine some of the principles behind the interoperability of managed and unmanaged code, and how managed code can call into unmanaged code. Our focus here is on the underlying principles rather than on coding details.

Principles of Calling Unmanaged Code

We saw in Chapter 2 that it is possible to embed native executable code in an assembly. The trick is quite simply to indicate in a method's signature that this method contains native code rather than IL code. (It is not possible to embed native executable at any other level - it has to be done on a per method basis.) This means that we need a mechanism to call from managed into native code, and vice versa.

There are three ways in which managed code can call into unmanaged code:

  • Internal call

  • P/Invoke and IJW

  • COM Interop

Internal Call

The internal call technique is the most efficient possible way of calling into unmanaged code. It is a technique used inside some of the framework base classes. It's not that well documented, being mentioned briefly in the Partition II document, and it's not something that you can use for your own code. I mention it here because you will see it used a lot if you examine the IL for some Microsoft-defined classes. An internalcall method will be flagged as cil managed internalcall. For example, here's the definition in IL of Object.GetHashCode();

 .method public hidebysig newslot virtual         instance int32 GetHashCode() cil managed internalcall { } 

A method marked as internalcall will simply transfer control to some hard-wired helper function in the CLR itself. Because these functions are hard-wired, they are very efficient, and the CLR can skip most or all of the time-consuming checks on security, etc., that have to be made when using other techniques to call unmanaged code.

As an example, some of the many methods that are internalcall include many methods on System.Object, System.Array, System.Type, and System.Threading.Monitor.

P/Invoke and IJW

The P/Invoke and IJW (It Just Works) techniques are often presented as if they were different, but in fact IJW is simply a variant of P/Invoke that offers some additional benefits for C++ developers (the technique is not available in VB or C#). For both P/Invoke and IJW, a method is defined in the assembly with the pinvokeimpl flag, and which contains native executable code:

 .method public hidebysig static pinvokeimp1("user32.dll" winapi)         int32 MessageBox(int32 hWnd,                          string text,                          string caption,                          unsigned int32 type) cil managed preservesig { } 

As we saw in the last chapter, the pinvokeimpl flag tells the JIT compiler to insert code to perform marshaling, and also to perform certain tasks that are collectively known as setting the execution context. This includes setting up flags so that the garbage collector knows this thread is running unmanaged code (an item called a sentinel is placed on the stack frame - this is recognized by the garbage collector as marking the boundary between managed and unmanaged code). The P/Invoke mechanism also makes arrangements for any unmanaged exceptions that are thrown to be converted into managed exceptions. It will also normally ask the CLR for a .NET security permission, UnmanagedCodePermission, though this step will be skipped if the pinvokeimpl wrapper method is marked with the System.Security.SuppressUnmanagedCodeSecurityAttribute attribute.

Using this attribute improves performance, and you should mark calls to unmanaged code with it if you are certain that there is no way any malicious code could abuse the call, for example by passing in unexpected parameters to public methods in the assembly.

In terms of high-level languages such as C# and VB, the usual way that you code up the P/Invoke technique is to declare a method with the DllImport attribute:

 // C# ; [DllImport("user32.dll")] public static extern int MessageBox(IntPtr hWnd, string text,                                     string caption, uint type); 

This DllImport attribute definition is what compiles into the pinvokeimpl method in the emitted assembly. We saw how to use this technique when coding directly in IL in Chapter 2, when we examined coding with platform invoke. As we saw there, the advantage of explicitly declaring a method in this way is that you can specify what types the pinvokeimpl wrapper method expects, and therefore have some control over how the marshaling is done.

In C++ it is possible to explicitly declare the DllImport method as well, and with very similar syntax to C#. However, C++ also offers as an alternative to the "It Just Works" (IJW) technique. IJW in C++ is literally just that. We don't have to explicitly declare the DllImport wrapper ourselves, but instead we rely on the C++ compiler to automatically supply a pinvokeimpl wrapper method, based on the original unmanaged method declaration, which will of course be located in one of the header files you have #included (or possibly even in the same C++ file). Besides potentially saving you some work, the compiler is also able to supply a more sophisticated definition of the pinvokeimpl method in IL, which will save the CLR some work and so enhance performance when calling the method. On the other hand, you don't get the chance to specify which managed types should be passed in and marshaled: you have to supply unmanaged types to the method in your C++ code whenever you invoke it.

To compare the two approaches, let's quickly see what happens with a simple application that uses P/Invoke or IJW to call the MessageBox() API function. First, here's the P/Invoke version.

 [DllImport ("user32.dll")] extern int MessageBox (IntPtr hWnd, String *text, String *caption,                        unsigned int type); int _tmain(void) {    MessageBox(NULL, S"Hello", S"Hello Dialog", 1);    return 0; } 

If we look at the MessageBox() method that is generated by compiling this code, we see this relatively basic pinvokeimpl function:

 .method public static pinvokeiml ("user32.dll" winapi)         int32  MessageBox(native int hWnd,                           string text,                           string caption,                           unsigned int32 type) cil managed preservesig { } 

But here's what happens if I change the C++ code to use IJW:

 #include "stdafx.h" #include "windows.h" # include <tchar.h> #using <mscorlib,dll> using namespace System; int _tmain(void) {    MessageBox(NULL, "Hello", "Hello Dialog", 1) ;    return 0; } 

I've included all the headers in this code to make it clear that there is no DllImport defintion. The C++ compiler will pick up the definition of MessageBox() from Windows.h.

Now the emitted IL contains this definition of MessageBox():

 .method public static pinvokeimpl(/* No map */)    int32 modopt([mscorlib]System.Runtime.CompilerServices.CallConvStdcall)    MessageBoxA(valuetype HWND__* A_0, int8    modopt([Microsoft.VisualC]Microsoft.VisualC.NoSignSpecifiedModifier)    modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier)* A_1,    int8 modopt([Microsoft.VisualC]Microsoft.VisualC.NoSignSpecifiedModifier)    modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier)* A_2,    unsigned int32 A_3) native unmanaged preservesig {   .custom instance void [mscorlib]System.Security.SuppressUnmanagedCodeSecurityAttribute::.ctor() = ( 01 00 00 00 )   // Embedded native code   // Disassembly of native methods is not supported.   // Managed TargetRVA = 0xa2b8 } // end of method 'Global Functions'::MessageBoxA 

We don't have space to go through all this code in detail. But there are a couple of things I want you to notice about the IJW version of this method:

  • The C++ compiler has done the work of identifying the ANSI version of MessageBox(), MessageBoxA(), as the one to be called. That means the CLR doesn't have to do that work at run time. This is shown both by the function name MessageBoxA(), and by the appended flag preservesig.

  • While both methods are pinvokeimpl, the IJW version is also native unmanaged and contains embedded native code. Unfortunately, ildasm.exe can't show us the native code, but we can make an educated guess that it implements some of the transition stuff that would otherwise have had to be figured out by the CLR.

  • The IJW version declares the SuppressUnmanagedCodeSecurityAttribute.

  • The data types to be passed in with the IJW version are int32, int8, int8, and int32. In other words, exact, isomorphic types, so no marshaling needs to be done by the P/Invoke mechanism.

Note that much of this code is devoted to modopt specifiers. However, the information supplied by modopt is largely aimed at developer tools, not at the CLR - and so isn't of interest to us here.

In conclusion from all this, you shouldn't expect massive performance gains from using IJW because it still ultimately goes through the P/Invoke mechanism, but there may be small improvements, and it may make your coding easier. Bear in mind, however, that performance when going over P/Invoke can easily be swamped by any required data marshaling. For IJW this marshaling does not happen during the method call, but you may have to perform explicit data conversions in your C++ code before calling the pinvokeimpl method. On the other hand, you can probably arrange your data types in your C++ code to minimize any data conversion overhead with better results than for P/Invoke marshaling.

COM Interoperability

The .NET COM Interop facility is designed to allow managed code to call into legacy COM components and vice versa. Strictly speaking, you probably shouldn't view COM interop as an alternative to P/Invoke - it's more correct to view it as an additional layer. At some point when invoking a method on a COM component, the P/Invoke mechanism is still going to get called in to perform the managed to unmanaged transition, but that is all hidden from the developer. Because of this, COM Interop transitions take longer than P/Invoke ones. Microsoft has suggested typically 60-70 native instructions, plus data marshaling time (as compared to 10-30 native instructions plus marshaling for plain P/Invoke).

COM Interop in practice is based on a command-line utility, tlbimp.exe. tlbimp examines a COM type library, and then creates a managed assembly that wraps the library. The assembly contains managed classes and managed interfaces that have similar definitions to the COM equivalents defined in the type library, and implements the methods in the managed classes so that they internally use P/Invoke to call up the corresponding methods in the component (with the constructor in the managed class presumably implemented to call CoCreateInstance()).

We can quickly see what roughly is going on in COM Interop by creating a project in VB6. Here's the code - it's a VB6 Class Module (in other words, a COM component) called Doubler, and provides a means by which you can perform that much sought after and terribly hard to implement task of doubling a number:

 Public Function  DoubleItVB6(x As Integer) As Integer    DoubleItVB6 = x * 2 End Function 

The code sample download for this project includes the source files and the compiled DLL, so you don't have to build the DLL yourself, which means you don't need VB6 installed to run the sample (though you will need to register the DLL with regsvr32.exe if you don't build it on your own machine). Next we run tlbimp on the file by typing in this at the command prompt:

 tlbimp Doubler.dll /out:ManDoubler.dll 

This gives us the managed assembly, which implements all the code required to wrap the unmanaged code in Doubler.dll. If we examine the assembly using ildasm, we find quite a lot there:

click to expand

The reason for the two interfaces is to do with internal implementation details in VB6, which normally generates two COM interfaces for its types, a public one and an internal one prefixed by an underscore. The screenshot shows that tlbimp has wrapped literally everything, and also used some custom attributes to indicate information about the COM components.

Of course, the really interesting thing here is what happens if some managed code uses this wrapper to call the DoubleItVB6() method. Here's the wrapper's implementation of this method:

 .method public hidebysig newslot virtual         instance int16 DoubleItVB6([in] [out] int16& x) runtime managed internalcall {    .custom instance void [mscorlib]System.Runtime.InteropServices.DispIdAttributes::ctor(int32) = ( 01 00 00 00 03 60 00 00    .override ManDoubler._Doubler::DoubleItVB6 } 

The crucial point in this code is that runtime managed internalcall flag on the method definition. In other words, this is a method that is implemented within the CLR itself. The CLR's implementation will essentially use P/Invoke to transfer control to the underling DoubleItVB6() method in the COM component.

Mixing Managed and Unmanaged Types

We will now examine how the C++ compiler and the .NET Framework work allow unmanaged and managed types (as opposed to isolated methods) to coexist in the same assembly and to hold pointers to and invoke methods on each other. In other words, how it is possible, to have this:

 __nogc class UnmanagedClass {    // etc. } __gc class ManagedClass {    UnmanagedClass *pUnmanagedObject;    // etc. 

This is distinct from what we've been looking at up to now - so far we've seen that P/Invoke lets you call unmanaged methods from managed code.

It's important to make the distinction between unmanaged objects and unmanaged methods. A method is of course managed if it contains IL code, and unmanaged if it contains native code. This is controlled in MC++ by the preprocessor directives #pragma managed and #pragma unmanaged. On the other hand, a type is regarded as managed if the type is defined in metadata. This is shown in MC++ for reference and value types respectively by the __gc and __value keywords. By specifying __nogc, on the other hand, we are saying that we don't want any metadata made available to allow managed code in other assemblies to use this class, but we want instead to lay this type out in memory as an unmanaged type and we want to be able to instantiate it either on the stack or on the unmanaged heap. (And obviously we might still want our C++ code in the same project to be able to access the methods on this class and to treat it as an unmanaged type.)

There is no problem with mixing the two concepts, for example having an unmanaged class all of whose methods are implemented as managed code:

 #pragma managed __nogc class MyClass     // Unmanaged class {    void DoSomething()    // will compile to managed code    {       // etc. 

This is indeed the default behavior for C++ code compiled with the /clr flag.

Unmanaged References in Managed Types

Let's see how we can have a managed type hold a reference to a C++ class compiled with __nogc. To understand this, we will need to do some delving into the IL that is emitted to implement an unmanaged type. What actually happens is that for every unmanaged type, the C++ compiler generates a shadow managed value type with the same name. If you compile a C++ project specifying the /clr flag, then every class and struct that you define will cause corresponding metadata for a managed type to be emitted into the assembly. For classes that are marked __gc or __value, you'll get full metadata that lists all the members of that type, just as you would in C# or VB. For other classes, however, the metadata will simply define a shadow value type, which has sufficient space to hold all the fields of that type, but no methods defined. The type will simply look like an opaque blob to the CLR. You might wonder how it's possible for managed code to call the methods on such a class. The answer is that for each method of a __nogc class that you define in the C++ source code, the compiler defines a corresponding global (static) method in IL. This method will contain IL code (for #pragma managed __nogc classes) or will be a pinvokeimpl method that calls up the real native code (for #pragma unmanaged __nogc classes). The next sample illustrates all these concepts. The sample is called the UnmanCombis (unmanaged combinations) sample, and it is a managed C++ VS.NET project that contains a number of simple types that mix different combinations of managed and unmanaged classes and methods, and has these types called from a managed entry point. This is the source code for the sample:

 #include "stdafx.h" #include <tchar.h> #using <mscorlib.dll> #pragma unmanaged __nogc class UnNoTest { public:    UnNoTest(int x) { this->x = x; }    int DoubleIt() { return 2 * x; } private:    int x; }; #pragma: managed __nogc class ManNoTest { public:    ManNoTest(int x) { this->x = x; }    int DoubleIt() { return 2 * x; } private:    int x; }; #pragma managed __gc class ManGCTest { public:    ManGCTest(int x) { this->x = x; }    int DoubleIt() { return 2 * x; } private:    int x; }; #pragma managed int _tmain(void) {    UnNoTest *pUnNoTest = new UnNoTest(3);    Console::WriteLine(pUnNoTest->DoubleIt());    delete pUnNoTest;    ManNoTest *pManNoTest = new ManNoTest(3);    Console::WriteLine(pManNoTest->Doublelt());    delete pManNoTest;    ManGCTest *pManGCTest = new ManGCTest(3);    Console::WriteLine(pManGCTest->DoubleIt());    return 0; } 

As you can see, this project contains three classes that have identical implementations: they store an integer in their constructor, and multiply it by two in a DoubleIt() method. The difference between these classes is that UnNoTest is - at the C++ level - an unmanaged class with native methods, while ManNoTest contains IL methods but is an unmanaged type, and ManGCTest is a fully managed type containing IL methods. The fourth combination, a managed type containing native methods, is not currently supported by the C++ compiler. The entry point method simply instantiates each of these classes in turn and invokes the DoubleIt() method for each of them.

Examining the compiled assembly with ildasm.exe reveals the following:

click to expand

In this screenshot, we can see that the managed type, ManGCTest, has compiled in the way we expect, and details of all its methods are present in the metadata. We won't consider this type further. On the other hand, for each of the two unmanaged classes we see a value type - these value types are the shadow types, which are defined to look like opaque blocks of data to the CLR. For example, here's the definition of ManNoTest:

 .class private sequential ansi sealed ManNoTest        extends [mscorlib]System.ValueType {    .pack 1    .size 4    .custom instance void [Microsoft.VisualC]Microsoft.VisualC.                          DebugInfoInPDBAttribute::.ctor() = ( 01 00 00 00 ) } 

So what's the point of this shadow type? Well, presumably you are going to want managed classes to be able to refer to instances of this class in their methods. The only way that that is possible in IL is if this type does exist in some kind of format that's recognizable to the CLR and can be described by metadata - simply in order to give us a TypeDef token that can be used in the IL stream. Because this class is really intended as an unmanaged class, however, no managed fields within it have been defined in the metadata. Instead, the type has been specified as having size 4 - that means that instances of this type will be big enough to hold the (unmanaged) int member field that has been declared within the type. Details of accessing that int will of course be left to unmanaged code. The C++ compiler has also added an attribute to the type; this is for the benefit of the VS.NET debugger - it indicates where debugging information can be found for this type.

It's important that __nogc C++ classes are compiled to value types. That's because unmanaged C++ allows types to be instantiated on the stack, just as with .NET value types.

On the other hand, for both the unmanaged types, we see the global managed methods declared that represent the instance methods we have declared - the constructor and the DoubleIt() method. The names of these methods contain embedded dots - for example, ManNoTest.DoubleIt() and UnNoTest.DoubleIt(); that's fine because IL syntax allows dots in names. Don't be fooled into thinking that these dots represent some kind of type information - they are there only for legibility to humans. ManNoTest.DoubleIt() really is the name of a global method. Here's what the IL code for this method looks like:

 .method public static int32    modopt([mscorlib]System.Runtime.CompilerServices.CallConvThiscall) ManNoTest.DoubleIt(valuetype ManNoTest*    modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier)    modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier) A_0)    cil managed {   .vtentry 2 : 1   // Code size       5 (0x5)   .maxstack  2   IL_0000:  ldarg.0   IL_0001:  ldind.i4   IL_0002:  ldc.i4.1   IL_0003:  shl   IL_0004:  ret } 

The interesting thing to notice here is that the implicit this pointer that forms the first parameter to an instance method has been explicitly supplied here: the first parameter is a ManNoTest* pointer. And notice the * syntax - this is a genuine unmanaged pointer. To retrieve the x field of the ManNoTest* there's no ldfld instruction - because as far as IL is concerned this type does not contain any field. Instead, the ldind.i4 command just de-references the address of the object. Since x is the only field in ManNoTest, de-referencing the pointer will give us this field.

For reference, here's the corresponding code for UnNoTest.DoubleIt(). This code doesn't tell us much, because we told the compiler to generate native methods for this class:

 .method public static pinvokeimpl(/* No map */) int32    modopt([mscorlib]System.Runtime.CompilerServices.CallConvThiscall) UnNoTest.Doublelt(valuetype UnNoTest*    modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier)    modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier) A_0)    native unmanaged preservesig {    .custom instance void [mscorlib]System.Security.            SuppressUnmanagedCodeSecurityAttribute::.ctor() = ( 01 00 00 00 )    // Embedded native code    // Disassembly of native methods is not supported.    // Managed TargetRVA = 0x1010 } 

This tells us how the CLR manages to pull off the trick of having managed code reference unmanaged types. To summarize: it declares a shadow value type, and uses unmanaged pointers to reference that type. It also declares static methods corresponding to the members of that type - those static methods take an extra unmanaged pointer argument, giving them the same signature as a genuine instance member of the type would have had.

The code for the main() method of this sample is too long to present here in full. However, you'll find it quite revealing if you compile the sample as a release build and examine the IL. If you do, you'll find that a lot of extra IL has been generated around the use of the unmanaged types, mostly to guard against exceptions and so on - an essential safety feature as far as the CLR is concerned, since it has no way of knowing what the unmanaged code is going to do. There are also explicit calls to new() and delete() to assist with construction and destruction of the unmanaged objects. You'll also find that much of the IL code around the ManNoTest type - and in particular the call to ManNoTest.DoubleIt() has been inlined, emphasizing the performance benefits of using C++.

Managed Pointers in Unmanaged Types

We've now seen how easy it is for a managed type to reference what in C++ terms is seen as an unmanaged type. However, going the other way involves a bit more work by the developer - it's not all handled by the compiler.

The fundamental problem with attempting to access a managed type from an 'unmanaged' type is that the garbage collector needs to be able to locate all the references in existence to managed reference types, so that it can update those references when it performs garbage collection. Clearly, even if an unmanaged type were able to determine the address in memory at which some managed type was stored, it would not be sensible simply to hold a pointer to this location, because that pointer might at any time become invalid as the object gets moved. While it is true that you can prevent this by pinning the object, doing so drastically reduces the efficiency of garbage collection. As we've already noted, because of the performance implications, pinning objects is not a good idea - and this is particularly important in this case, because holding onto a reference to a type is something that could be done for a long time continuously - for example, a long-lived instance of an unmanaged type might maintain a pointer to a managed type as a member field. Because of this problem, C++ actually regards it as a syntax error if a managed pointer is declared as a member field of an unmanaged type.

Similar considerations also apply in the case of value types. Although there is no risk of the garbage collector wanting to move value types, it needs to know of them because value types may contain references to reference types that need to get updated when the reference types are moved.

Notice that this is only a problem for member fields. There is no problem for local variables in unmanaged methods, because the lifetime of a local variable cannot last beyond the return of the method. If the garbage collector does get called up while an unmanaged method is executing, the garbage collector will reroute the return address from the unmanaged code so that it can take over as soon as the unmanaged code returns. At that time, any local variables from the unmanaged code will go out of scope and so no longer be relevant to the collection. Hence it is fine for managed pointers to be declared locally to unmanaged methods.

Of course, if you've done much managed C++ programming, you'll be familiar with what happens at the C++ source code level: you can completely solve the problem just by using the gcroot<> template (defined in gcroot.h) when you declare any member references to managed types as member fields, so for example, instead of writing:

 ListBox *listBox = new ListBox; 

you use:

 gcroot<ListBox> *listBox = new ListBox; 

And you can then treat *listBox as if it were a ListBox pointer (even destruction will be handled automatically).

While that code is quite easy to follow (assuming you're used to C++ template syntax), it doesn't give us any idea what's actually happening to solve the garbage collection problem. That's what we'll investigate here.

Since pinning the managed pointer is not really an option, what we need is some mechanism by which unmanaged code can hold a reference to a managed type while letting garbage collection know that this reference exists, and somehow coping if the garbage collector moves the object. Fortunately, Microsoft anticipated this problem and provided a framework base class to deal with it. The class is GCHandle, and is in the System namespace. The purpose of GCHandle is to maintain an Object reference - which can be either to a reference type or to a boxed value type - and to make this reference available to unmanaged code. How can it do this? It takes advantage of a loophole in the restrictions: unmanaged code shouldn't hold any references to managed objects, but there's nothing to stop unmanaged code from calling static members of managed types! Static members will never be touched by the garbage collector, so it's perfectly OK for unmanaged code internally to store pointers to static member methods or fields of managed classes.

GCHandle makes available some static methods, which allow unmanaged code to reference a handle - a System.IntPtr instance (or in IL terms, a native int). This handle works in the same way as Windows handles - it doesn't give the address of an object, but it can identify the object to the GCHandle class. The idea is that whenever unmanaged code wants to call a method of a managed object, it passes the handle to a static GCHandle method - effectively saying, "You're a managed type so you're allowed to keep hold of the reference corresponding to this handle. Can I have it for a moment please?" The unmanaged code gets the object reference just long enough to call the method it wants. As long as it only uses this object reference as a local variable inside a method, everything is fine. The way this works is illustrated by a sample called GCHandleDemo. This sample defines a class called ManTest, which is identical to the ManGCTest class from the previous sample:

 __gc class ManTest { public:    ManTest(int x) { this->x = x; }    int DoubleIt() { return 2 * x; } private:    int x; }; 

In order to demonstrate an unmanaged class wrapping a managed class, we'll define two unmanaged classes, each of which serves as an unmanaged wrapper around ManTest and needs to contain some reference to the embedded ManTest instance. One of these classes, called TemplateWrapper, does this using the gcroot<> template, and the other, RawWrapper, does the same thing but without using gcroot<>. Hence TemplateWrapper demonstrates the syntax you would normally use in this situation, while RawWrapper shows us what's actually going on under the hood.

Here's TemplateWrapper:

 class TemplateWrapper { private:    gcroot<ManTest*> pTest; public:    TemplateWrapper(int x)    {       pTest = new ManTest(x);    }    int DoubleIt()    {       return pTest->DoubleIt();    } }; 

And here's RawWrapper:

 class RawWrapper { private:    void *handle; public:    RawWrapper(int x)    {       ManTest *pTest = new ManTest(x);       GCHandle gcHandle = GCHandle::Alloc(pTest);       System::IntPtr intPtr = GCHandle::op_Explicit(gcHandle);       handle = intPtr.ToPointer();    }    int DoubleIt()    {       GCHandle gcHandle = GCHandle::op_Explicit(handle);       Object *pObject = gcHandle,Target;       ManTest *pTest = __try_cast<ManTest*>(pObject);       return pTest->DoubleIt();    }    ~RawWrapper()    {       GCHandle gcHandle = GCHandle::op_Explicit(handle);       gcHandle.Free();    } }; 

Instead of storing a pointer to a managed class (which would be illegal), RawWrapper stores a void* pointer - this will contain the handle that GCHandle gives us to identify the ManTest object.

In the RawWrapper constructor we need to instantiate a managed ManTest object and obtain the handle required to identify the object. Instantiating the object is easy - we just use the new operator. The problem of course is that we are only allowed to store the pointer to this object as a local variable. So we pass this reference to the static GCHandle::Alloc() method, which creates a GCHandle instance that wraps the object. Since GCHandle is a value type, we can store it directly in our constructor - we don't need to access it via a pointer. We now invoke an operator defined on GCHandle, which returns an IntPtr(native int), which is the actual handle. We will use this handle as a void*, so we convert it to that using the IntPtr.ToPointer() method. Note that, besides returning a GCHandle instance, GCHandle.Alloc() will store the reference to the object in some static internal data structure, the details of which are undocumented (most likely it would be a dictionary-based collection). The fact that the reference is stored here will make sure the garbage collector thinks that the object is in use, and so doesn't garbage-collect it.

Now let's examine that DoubleIt() method. Here we need to use the handle we've stored to retrieve a reference to the object; it's basically the reverse process to that for getting the handle. We call the static GCHandle.op_Explicit() method to instantiate a temporary GCHandle struct that corresponds to this handle/object reference - this method will of course retrieve the reference from GCHandle's own internal data structure. We then use an instance property, GCHandle.Target to retrieve the actual object reference. We get the reference as an Object*, so we have to cast it to ManTest* before we can use it.

Finally, we need to make sure that when our wrapper class is destroyed, so is the managed Test object. The GCHandle.Free() method deals with that - it removes it from its internal data structure so that the object can be garbage-collected.



Advanced  .NET Programming
Advanced .NET Programming
ISBN: 1861006292
EAN: 2147483647
Year: 2002
Pages: 124

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net