Marshaling and Conversion with P/Invoke

During my series of head-spinning interop columns, I showed you how to use P/Invoke to access a DLL from Managed C++, and then showed how you don't always need P/Invoke. Often It Just Works interop will take care of everything for you. You need P/Invoke if you would like to control the marshaling or conversions associated with a call; you don't need it if the types passed to or returned from the function are types that will be converted for you, such as double or int variables.

If your unmanaged function takes a string, a char*, you can call it from managed C++ by creating a char*, but you can also pass it a String* and let the framework do the conversion for you if you use P/Invoke. It Just Works interop won't convert strings for you. What's more, you can write code of your own to convert a managed type to an unmanaged type and arrange for this conversion code to be wrapped around calls into the DLL. In this column, I'll show you how to control marshaling yourself, and how P/Invoke can be useful to a C++ programmer.

When you make a call from managed to unmanaged code, all the parameters you want to send to the unmanaged code are gathered up, rearranged into the right order, possibly copied or converted, possibly pinned to ensure they don't move during the call, and so on. This is called marshaling, and it's supposed to make you think about the beginning of a parade when someone prods and pushes to get all the floats lined up in order before they head out. When you call a legacy library by adding the .lib file to the linker dependencies, you get the default marshaling. If the function only takes and returns so-called blittable types, this is fine. Blittable types have an identical memory representation in managed and unmanaged code. C++ fundamental types such as int are blittable types. Strings are not.

Here is a function that takes a char* and a SYSTEMTIME*. SYSTEMTIME is an old-style way of representing a date and time, very popular among C SDK programmers.

LEGACY_API bool Log(char* message, SYSTEMTIME* time)
{
   FILE* logfile = fopen("C:\\log.txt","a");
   fprintf(logfile, "%d/%d/%d - %d:%d %s \r\n",
      time->wYear, time->wMonth, time->wDay,
      time->wHour, time->wMinute, message);
   fclose(logfile);
   return true;
}

If you want to code along with this column, make a DLL that exposes this method, using the techniques I've shown you earlier for writing a DLL in unmanaged C++. You'll need to include windows.h.

Passing a String to a Function in a DLL

To call this function from Managed C++, you have two choices: IJW and P/Invoke. If you want to access it with IJW, you would add the import library (.lib) file to the linker dependencies, you would #include a header file that defines the function, and you would make sure that you passed a char* and a SYSTEMTIME* to the function call, like this:

#include "windows.h"
#include "legacy.h"
#using <mscorlib.dll>

using namespace System;

int _tmain()
{
   SYSTEMTIME st;
   GetLocalTime(&st);
   Log("Testing from Managed Code", &st);
   System::Console::WriteLine(S"log succeeded");
   return 0;
}

(GetLocalTime is an SDK function that fills a SYSTEMTIME structure with the current local time.) This code works, and it's clearly managed code, but it's not using managed types. What if the string you wanted to pass to Log() was already in a System::String variable? To use IJW Interop, you must convert the String* to a char*. Here is one way to do that:

String* s = new String("Testing with a heap string");
char* pChar = (char*)Marshal::StringToHGlobalAnsi(s).ToPointer(); 
Log(pChar, &st);

That's hardly pleasant, and it's not an option for types other than strings anyway.

If, instead, you use P/Invoke, you can get your strings converted automatically, so that you pass a String* to the function in your code, but a char* actually reaches the functions inside the DLL. Instead of using the header file, legacy.h, to declare the Log() function, you declare it yourself with attributes that control the marshaling. As a side effect, you no longer link to the import library because PInvoke will deal with finding and loading the DLL. The resulting code looks like this:

extern "C" {
[DllImport("legacy", CharSet=CharSet::Ansi)]
bool Log(String* message, SYSTEMTIME* time);
}

The first parameter on the DllImport attribute, legacy, identifies the DLL in which the function is declared. The second parameter controls the way that strings are marshaled. You can convert the Unicode string in a System::String to an ANSI string, as in this example, or a wchar* if that's what the code in the DLL is expecting.

The DllImport attribute is in the InteropServices namespace, so add a using line before the function definition:

using namespace System::Runtime::InteropServices;

Now you can pass a String* to the Log() function:

String* s = new String("Testing with a heap string");
Log(s, &st);
System::Console::WriteLine(S"log succeeded");

The DllImport attribute will ensure that s is converted from a String* type of string to a char* type of string at runtime, whenever the Log() function is called.

Passing a Non-String, Non-Blittable Type

You've seen in previous columns that when you pass a blittable type, such as a double, you don't need to do anything—don't even need to use PInvoke—to handle marshaling and conversion issues. When you pass a string, you add a parameter to the DllImport attribute on your declaration of the function to arrange for appropriate string marshaling behind the scenes. But what about other types, such as the SYSTEMTIME structure that is passed to Log()?

Just as you could convert the String* to a char* yourself and pass it along, you can write a function to convert a System::DateTime structure to a SYSTEMTIME structure. Here's one that's really easy to read:

SYSTEMTIME MakeSystemTimeFromDateTime(DateTime dt)
{
    SYSTEMTIME st;
    st.wYear         = dt.get_Year();
    st.wMonth        = dt.get_Month();
    st.wDayOfWeek    = dt.get_DayOfWeek();
    st.wDay          = dt.get_Day();
    st.wHour         = dt.get_Hour();
    st.wMinute       = dt.get_Minute();
    st.wSecond       = dt.get_Second();
    st.wMilliseconds = dt.get_Millisecond();

    return st;
}

(There are cleverer versions of this in existence. Because both structures actually represent the same thing, a date and time, and because that can be represented by a single number of ticks since a reference date and time, it's possible to convert from one to the other in a single line of code. It's not as readable though, so I'll present it the long-and-simple way in this column.)

SYSTEMTIME is defined in winbase.h, but in a managed application you probably don't want to #include that header file. There's nothing stopping you from simply pasting the definition into your own code. It relies on typedefs such as WORD, so you have to poke around a bit and make some substitutions to get a streamlined definition:

typedef struct _SYSTEMTIME {
    short wYear;
    short wMonth;
    short wDayOfWeek;
    short wDay;
    short wHour;
    short wMinute;
    short wSecond;
    short wMilliseconds;
} SYSTEMTIME;

Now you can write a managed main that doesn't use GetLocalTime() and that works with a DateTime. After all, your code might be passing a date that came from a DateTimePicker or other control that returns a DateTime structure, so you want to avoid working directly with a SYSTEMTIME structure. Here's the revamped main:

extern "C" {
[DllImport("legacy", CharSet=CharSet::Ansi)]
bool Log(String* message, SYSTEMTIME* time);
}
int _tmain()
{
    SYSTEMTIME st =
               MakeSystemTimeFromDateTime(System::DateTime::Now);
    String* s = new String("Testing with a heap string");
    Log(s, &st);
    System::Console::WriteLine(S"log succeeded");
    return 0;
}

Now the string is being converted automatically, because of the parameter passed to the DllImport attribute, and the DateTime structure is being converted by hand to a SYSTEMTIME structure just before the call. This works, though it's slightly inconvenient.

If a function in a DLL is going to be called repeatedly, it's obviously more convenient to add an attribute to the function definition asking the framework to convert a String* to a char* than to expect the programmer to do that conversion before every function call. By the same logic, wouldn't it be great if you could just pass a DateTime to Log() and have the marshaler convert it to a SYSTEMTIME? Well, you can. It's not built in the way string conversions are (everybody does string conversions), but it's not terribly hard to do. I'll show you next column how it's done.

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.




Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Protecting business operations means shifting the priorities around availability from disaster recovery to business continuity. Enterprises are shifting their focus from recovery from a disaster to preventing the disaster in the first place. With this change in mindset, disaster recovery is no longer the first line of defense; the organizations with a smarter business continuity practice are less impacted when disasters strike. This SmartSelect will provide insight to help guide your enterprise toward better …

  • Corporate e-Learning technology has a long and diverse pedigree. As far back as the 1980s, companies were adopting computer-based training to supplement traditional classroom activities. More recently, rich web-based applications have added streaming audio and video, real-time collaboration and other new tools to the e-Learning mix. At the same time, the growing availability of informal learning tools--a category that includes everything from web searches to social media posts--are having a major impact on …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds