Arranging Custom Marshaling With P/Invoke

Thursday Oct 30th 2003 by Kate Gregory

Share:

Learn how to arrange custom marshaling for function parameter when using P/Invoke to access a legacy function from Visual C++.

Using P/Invoke to access a legacy function in a DLL gives you string conversions for free, as I showed you in my last column. You can convert other types yourself, but it can be more convenient to get the framework to make those conversions for you. You have to write the conversion code, but you don't call it explicitly. Instead, you arrange custom marshaling for the function parameter.

(I'm using the same method, Log(), in this column that I did in my last column. It takes a char* and a SYSTEMTIME*.)

You arrange custom marshaling by writing a class that inherits from System::Runtime::InteropServices::ICustomMarshaler and overriding some methods in that class. Then, you add a DllImport attribute to the function directing the marshaler to use your class (by calling those overridden methods) to perform the necessary conversions. Although you can call your class anything you like, it helps maintainability if you have a naming convention. For example, most of the Microsoft samples name the custom marshaling class with the name of the unmanaged class to which it converts, an underscore, and the words CustomMarshaler. Following that convention, the class to convert from a DateTime to a SYSTEMTIME would be called SYSTEMTIME_CustomMarshaler.

Writing the Custom Marshaler

The ICustomMarshaler interface has eight public methods that must be overridden. This class can be used to marshal from managed to native data (as the example in this column uses) or from native to managed, or both. The methods are as follows:

Constructor—If you have no member variables in your implementation, ignore it

Destructor—If you have no member variables in your implementation, ignore it

static ICustomMarshaler* GetInstance(String* cookie)—Implements the singleton pattern for the marshaler

void CleanUpNativeData(IntPtr pNativeData)—Cleans up after MarshalManagedToNative() when the call is complete

Object* MarshalNativeToManaged(IntPtr pNativeData)—Converts values returned from a DLL

void CleanUpManagedData(Object* ManagedObj)—Cleans up after MarshalNativeToManaged when the call is complete

int GetNativeDataSize()—Helps allocate memory for values returned from a DLL

You must implement all these methods (except the constructor and destructor) to implement the interface. In the sample presented in this column, the marshaling is one way: The code will convert a DateTime to a SYSTEMTIME. If you want to use this class to convert a SYSTEMTIME to a DateTime, you would have to implement all the functions in this interface with meaningful bodies. In the interest of space, I only implemented the methods needed for converting a DateTime to a SYSTEMTIME and fooled the compiler with stub bodies for the rest of the methods. As well, I implemented only the conversion of a single object. Some custom marshalers can handle arrays of objects as well as single objects.

(Three methods, presented in boldface, are stubbed inline just to make the compiler happy.) Notice that this class implements the singleton pattern, with a private constructor and a public static GetInstance() method that provides access to the only and only instance of the class. The implementation of GetInstance() looks like this:

That's just like any other singleton class and has nothing to do with custom marshaling. You'll see it in use shortly.

MarshalManagedToNative()takes an Object pointer and allocates some memory for the native equivalent, and then copies data from the Object to the native structure or class. This signature cannot be changed, so the DateTime structure that is to be passed to Log() will have to be boxed, converting it to an Object*, so that this function can unbox it and perform the conversion.

This function creates a SYSTEMTIME structure in a special area of memory that can be shared by the managed and unmanaged code. Memory is allocated there using AllocCoTaskMem(). The first two lines allocate enough memory for a SYSTEMTIME plus an extra int, which will go before the SYSTEMTIME structure to tell the marshaler how many objects are being passed. Then, a second IntPtr object is created that "points" to the start of the structure, after that first integer. (This code uses two kinds of pointers: the traditional C-style pointer, such as int* or Object*, and a managed type called IntPtr, which functions just like a pointer, although the C++ compiler doesn't recognize it as one.)

The integer at the start of the allocated block is set to 1, because only one SYSTEMTIME instance is being created.

Now, the casting and unboxing happen. First, ptrST is put through a static cast to a SYSTEMTIME pointer; later, code will use that pointer to set the elements of the structure. Then, the Object* that was passed to MarshalManagedToNative() is put through a static cast to a boxed DateTime. This is safe because you know that a boxed DateTime will be passed to Log(). To unbox the DateTime, just declare another DateTime structure and use the dereferencing operator, *.

The second-last line of code takes care of copying all the elements of the DateTime structure into the SYSTEMTIME structure by using the helper function, MakeSystemTimeFromDateTime(); I showed earlier. Finally, the function returns the IntPtr that "points" to the start of the structure itself. The marshaler will look before this address for the count of the number of objects.

The third function to be overridden is CleanUpNativeData(). It must call FreeCoTaskMem() to reverse the allocation that was performed in MarshalManagedToNative(). Here's the code:

This function is handed a pointer to the start of the structure and it needs to "back up" to the beginning of the memory that was allocated, so it subtracts the size of an integer from the pointer it was handed. It then creates an IntPtr pointer, and passes it to FreeCoTaskMem().

Using the Custom Marshaler

Having written a custom marshaling class, you must ask the marshaler to use it. You do this by adding an attribute on the parameter to the Log() method, like this:

The type of time has changed from SYSTEMTIME* to __box DateTime*, which will make it much simpler to call from managed code. The MarshalAs and MarshalTypeRef attributes tell the marshaler to use SYSTEMTIME_CustomMarshaler to marshal this parameter.

Now, the code that calls the functions in the DLL can be written using data types that are more common in a managed C++ application. Here is a revised main:

Now, if I do say so myself, this is really cool. You've taken some old C++ code that was in a DLL, and your new managed code can talk to it as though it actually took managed types (String*, DateTime) for parameters! The calling code needs not even know what a SYSTEMTIME structure is. The framework converts the System::String string to a char* string, and your own code (called by the framework) converts the boxed DateTime to a SYSTEMTIME*. The converted parameters are handed over to the function in the DLL seamlessly.

So, who needs P/Invoke? Anyone who wants to interop to an old C++ function that doesn't take blittable types, and who doesn't want to write and then explicitly call conversion code each time the old function is called. Custom marshaling lets you achieve simplicity for those who want to call your code. That's what P/Invoke offers you.

About the Author

Kate Gregory is a founding partner of Gregory Consulting Limited (www.gregcons.com). In January 2002, she was appointed MSDN Regional Director for Toronto, Canada. Her experience with C++ stretches back to before Visual C++ existed. She is a well-known speaker and lecturer at colleges and Microsoft events on subjects such as .NET, Visual Studio, XML, UML, C++, Java, and the Internet. Kate and her colleagues at Gregory Consulting specialize in combining software develoment with Web site development to create active sites. They build quality custom and off-the-shelf software components for Web pages and other applications. Kate is the author of numerous books for Que, including Special Edition Using Visual C++ .NET.