Greg Dolley’s Weblog

A Blog about Graphics Programming, Game Programming, Tips and Tricks

Archive for February 3rd, 2008

Part 2: How to Make Native Calls from Managed Code in C++/CLI

Posted by gregd1024 on February 3, 2008

This is an extension from my previous post on C++/CLI marshalling. In this segment I’m going to talk about sending an object from managed code to native code. While doing this kind of marshalling is required less and less these days as we depend more on external .NET components instead of COM DLL’s or native C API’s, there may still be times where you’ll need to do it.

If you would like to play with the original source code discussed in this post, you can download it below:

Native Library

In order to keep the examples as simple as possible we’re going to create our own native library (.lib) instead of using some third party API (rest assured however, whether you’re calling the Win32 API or a third-party DLL, the process remains the same). Our library will contain one function and one native class. The class looks like this:

class NativeObject
{
   public:
      NativeObject();

   public:
      char m_byte;
      char *m_string;
      int m_int;
      float m_float_array[3];
};

We will be creating the .NET equivalent of this class in different project. You will see how to marshal the .NET equivalent object into the following native library function:

void SomeNativeFunction(NativeObject *obj)
{
   printf(“\r\nInside native library function. Object dump:\r\n\r\n”);
   printf(“obj->m_byte = %i\r\n”, obj->m_byte);
   printf(“obj->m_string = %s\r\n”, obj->m_string);
   printf(“obj->m_int = %i\r\n”, obj->m_int);
   printf(“obj->m_float_array[] = {%f, %f, %f}\r\n”,
                                  obj->m_float_array[0],
                                  obj->m_float_array[1],
                                  obj->m_float_array[2]);
}

I put both the class definition and the prototype of SomeNativeFunction() into a header file called, “NativeSampleAPI.h.” This header will used in the managed project so it can have access to the library’s type(s) and function(s).

Managed Object

Now we will create a C++/CLI project using the following wizard template: “CLR Console Application.” The first step is to create a managed class who’s layout exactly matches the native class’s layout byte-for-byte. For simple types such as int’s and char’s, choosing the correct managed type to match the corresponding native type is easy – in C++/CLI the native type “char” matches managed type “Byte,” the type “int” matches “Int32,” etc. However, for non-intrinsic types, choosing the equivalent can be confusing – how do you deal with an array which has an arbitrary size, or a string that is ANSI instead of unicode?

For these cases we use the “MarshalAs” member attribute to give the compiler more details on how we want the final unmanaged type to end up. See below for the complete conversion of our library’s native type to .NET:

[StructLayout(LayoutKind::Sequential)]
public ref class NativeObjectEquivalent
{
   public:
      NativeObjectEquivalent()
      {
         m_byte = 0x02;
         m_string = “Managed object”;
         m_int = 200;
         m_float_array = gcnew array<float>(3);
         m_float_array[0] = 1.0f;
         m_float_array[1] = 2.1f;
         m_float_array[2] = 3.2f;
      }

   public:
      System::Byte m_byte;

      [MarshalAs(UnmanagedType::LPStr)]
      String ^m_string;
      Int32 m_int;

      [MarshalAs(UnmanagedType::ByValArray,
                 SizeConst=3,
                 ArraySubType=UnmanagedType::R4)]
      array<float> ^m_float_array;
};

The “MarshalAs” attribute applies to the member immediately following it. It accepts either a short or an “UnmanagedType” enum as the first parameter telling the compiler exactly which native type to use. If you need to specify more information about the type, such as fixed string length or array size, you would then fill in a series of named parameters following the first. For example, it’s not enough to tell the compiler that “m_float_array” is an array – it also needs to know the array size and the native type of each element. That’s where “SizeConst” and “ArraySubType” come into play. “SizeConst=3” tells the compiler that “m_float_array” is an array of fixed size and the number of elements it contains is three. “ArraySubType=UnmanagedType::R4” tells the compiler that it’s a “float” array (a “double” array would have been specified with “R8” instead of “R4”).

Now that the two types match in size, let’s see how to pass the managed object to the native function. Take a look at the following code in our managed project:

int main(array<System::String ^> ^args)
{
   Console::WriteLine(“Constructing managed object…”);
   NativeObjectEquivalent ^obj = gcnew NativeObjectEquivalent();

 

   Console::WriteLine(“Writing managed object to unmanaged memory space…”);
   IntPtr ptr(Marshal::AllocHGlobal(Marshal::SizeOf(obj->GetType())));
   Marshal::StructureToPtr(obj, ptr, false);

   Console::WriteLine(“Passing managed object to native function…”);
   SomeNativeFunction((NativeObject *)ptr.ToPointer());

   Console::WriteLine();
   Console::WriteLine(“Press any key to continue…”);
   Console::ReadKey();

   return 0;
}

Let’s go through things line by line (not counting print messages). First we create an instance of NativeObjectEquivalent, our managed version of the native class, via gcnew. Then AllocHGlobal() is called in order to allocate a chunk of memory from the unmanaged heap. It’s important that the memory is not on the managed heap so we’re ensured the pointer will never move (managed pointers can have their address changed during heap compaction). At this point we call StructureToPtr(). This function marshals a managed object into an unmanaged block of memory pointed to by the IntPtr. Finally, we call our library function converting the IntPtr into a native void pointer via the ToPointer() call. We must cast the void pointer into a NativeObject pointer to satisfy the compiler.

And… we’re done! Let’s look at the output:

MarshalToNativeAPI_SampleOutput

Notice how m_string is set to “Managed object” – proving that the object inside our native function did indeed originate from the managed project. 😉

Conclusion

This post, of course, only scratches the surface on the many different types of marshalling that can take place. Hopefully it has given you a good starting point for figuring out more advanced marshalling by yourself. If you have any questions or comments feel free to send me an email via my Contact page.

-Greg Dolley

*Get new posts automatically! Subscribe via RSS here. Want email updates instead? Click here.

Posted in C++/CLI, Tips and Tricks | 5 Comments »