How to Use an Unmanaged DLL from Managed C

How to Use an Unmanaged DLL from Managed C++

The import library that's created for a DLL simplifies using that DLL from unmanaged C++, because you don't have to use LoadLibrary() and GetProcAddress() to make the calls. It can also simplify using the same DLL from managed C++. Not all DLLs can be accessed through the import library from managed C++, but many can.

Using the Import Library

Here's how to create a managed C++ application that uses the legacy DLL through the import library:

  1. Create a managed console application called UseDLLManaged .

  2. Copy the include file for the DLL, legacy.h, to the project folder.

  3. Copy legacy.dll and legacy.lib from the Debug folder under the Legacy project to UseDLLManaged .

  4. Add legacy.lib to the linker dependencies as described earlier in this chapter.

At this point you are ready to add code to the application that uses the DLL. Here is the managed equivalent of the unmanaged test harness from earlier in this chapter:

 
 // This is the main project file for VC++ application project // generated using an Application Wizard. #include "stdafx.h"  #include "windows.h"  #include "legacy.h"  #using <mscorlib.dll> using namespace System; int _tmain() {    System::Console::Write(S"1 + 2 is ");    System::Console::WriteLine(__box( Add(1,2)));    SYSTEMTIME st;    GetLocalTime(&st);    Log("Testing from Managed Code", &st);    System::Console::WriteLine(S" log succeeded");    return 0; } 

This code runs and works beautifully. Managed C++ is quite happy to deal with old header files like windows.h, to create structures on the stack and pass their address to the DLL, and so on. The string created in managed code can be passed to the old unmanaged code without incident. This is It Just Works (IJW) in action again, and most developers are pleasantly surprised every time they come up against it.

However, this code is using a classic C-style string, a pointer-to-char. Your new managed code is far more likely to be using a String pointer that points to memory on the managed heap. For example, the contents of a WinForm text box or other UI components are represented as a managed string pointer, not char* . Here is a version of the console application that tries to work with a String* :

 
 System::Console::Write(S" 1 + 2 is "); System::Console::WriteLine(__box( Add(1,2))); SYSTEMTIME st; GetLocalTime(&st);  String* s = new String("Testing with a heap string");  Log(s, &st); System::Console::WriteLine(S" log succeeded"); return 0; 

This code will not compile. The Log() function doesn't take a String* , it takes a char* . You can make a char* from a String* like this:

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

This works, but it's a little awkward . What would be much simpler is if you could declare a version of Log() that takes a String* , have the framework convert the String* to a char* , and then call the Log() code from your DLL. You can do just that with PInvoke , discussed in the next section.

Using PInvoke to Control Marshaling

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.

The Add() function, which takes and returns doubles, doesn't need any special marshaling. If all the functions in legacy.dll worked well with default marshaling, the IJW techniques of the previous chapter would be all you needed. As you've seen, Log() doesn't exactly need special marshaling, it can be used as-is, but the programmer must convert the managed type (such as String* ) to the unmanaged type (such as char* ) before each call. The DLL will be much more useful if you can arrange these conversions to be done as part of the marshaling step.

Create another managed console application called UseDLLPInvoke . Copy legacy.dll from the Debug folder under Legacy to the UseDLLPInvoke project folder. Do not copy legacy.h or legacy.lib.

Rather than #include legacy.h, this code defines the functions and adds attributes to those definitions that govern the marshaling. Before the main() function in UseDLLPInvoke.cpp, add these lines:

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

There are several differences between these definitions of Log() and Add() and the definitions in legacy.h, as follows :

  • The LEGACY_API macro has been removed.

  • Each function has a DllImport attribute naming the DLL (without the .dll extension) as the first parameter.

  • The DllImport attribute on Log() further specifies that managed strings passed to this function are to be marshaled to ANSI strings before being passed to the function in the DLL.

The DllImport attribute is in the InteropServices namespace, so add this line before those function definitions:

 
 using namespace System::Runtime::InteropServices; 

This code won't compile yet, because SYSTEMTIME has not been defined. Rather than including all of windows.h, it's a better idea to just copy in the definition of SYSTEMTIME from winbase.h. That definition, however, uses the typedef WORD . It must be replaced with short in the managed definition. The completed definition looks like this:

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

At this point the project should build without errors, although the main() does nothing yet. The next issue to overcome is getting a SYSTEMTIME that holds the current time. Calling GetLocalTime() is too platform-specific; just as a String* is more likely to be useful (as the text property of a text box perhaps) than a char* , so an instance of a DateTime structure is more likely to be useful (perhaps as the selected date from a date time picker) than a SYSTEMTIME .

FINDING DEFINITIONS OF LEGACY TYPES

To find the definition of SYSTEMTIME so that you can copy it, open one of the projects that includes windows.h (Legacy or UseLegacyDLL), right-click SYSTEMTIME in the code, and then choose Go To Definition. You can then copy the definition into the Clipboard and paste it into UseDLLPInvoke.cpp.

To find the definition of WORD , right-click it in the definition of SYSTEMTIME in winbase.h and choose Go To Definition. You'll find it's just a typedef for unsigned short (defined in windef.h). Because unsigned types are not Common Language Specification compliant, use a short to stand in for a WORD .


In the previous section you saw how to convert a String* to a char* . You can also write a function to convert a DateTime to a SYSTEMTIME . It would look like this:

 
 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; } 

Type this function into UseDLLPInvoke.cpp before the main() function. Then edit the main() function so that it reads as follows:

 
 int _tmain() {     System::Console::Write(S" 1 + 2 is ");     System::Console::WriteLine(__box( Add(1,2)));     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; } 

Build and run the application. It should display that 1 + 2 is 3 and that the log succeeded. When you open log.txt you should see the current date and time, followed by the message from your code.

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. The next section shows you how.

Writing a Custom Marshaler

The marshaler knows how to convert a String* to a char* (or to several other popular string types in various languages). All you need to do is ask, by adding to the DllImport attribute on the method, like this:

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

When you want to have a conversion performed for you, you write a class that inherits from System::Runtime::InteropServices::ICustomMarshaler and override some methods in that class. Then you add an 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 .

The Custom Marshaling Class

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 chapter 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) Used to save marshaler constructing multiple instances

  • IntPtr MarshalManagedToNative(Object* pDateTime) For values passed to a DLL

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

  • Object* MarshalNativeToManaged(IntPtr pNativeData) For values returned from a DLL

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

  • int GetNativeDataSize() For values returned from a DLL

You must implement all these methods (except the constructor and destructor) in order to implement the interface. In the sample presented in this chapter, 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, this chapter only implements the methods needed for converting a DateTime to a SYSTEMTIME and fools the compiler with stub bodies for the rest of the methods. As well, it implements only the conversion of a single object. Some custom marshalers can handle arrays of objects as well as single objects.

Type this class into UseDLLPInvoke.cpp, before the declarations of Log() and Add() but after the declaration of SYSTEMTIME :

 
 __gc public class SYSTEMTIME_CustomMarshaler : public ICustomMarshaler { public:  static ICustomMarshaler* GetInstance(String* cookie);   IntPtr MarshalManagedToNative(Object* pDateTime);   void CleanUpNativeData(IntPtr pNativeData);  Object* MarshalNativeToManaged(IntPtr pNativeData){return 0;};    void CleanUpManagedData(Object* ManagedObj){};    int GetNativeDataSize(){return 0;}; private:    //singleton pattern    static SYSTEMTIME_CustomMarshaler* marshaler = 0; }; 

The three methods in boldface must be implemented. Code and discussion for each follows.

GetInstance()

GetInstance() is the simplest of the three methods to implement: You could return a new instance every time it was called, but why waste time and memory doing that when this marshaler has no member variables or anything else to differentiate one instance from another? This code uses the Singleton pattern to provide one single instance that anyone can use by calling the GetInstance() method. It relies on a static pointer (shared between all instances, and created even when no instances are created). The first time anyone calls GetInstance() , the pointer will still be NULL . An instance is created and the pointer is saved; from now on all calls will get the existing pointer. Here's the code for GetInstance :

 
 ICustomMarshaler* SYSTEMTIME_CustomMarshaler::GetInstance(String* cookie) {     if (!marshaler)         marshaler =  new SYSTEMTIME_CustomMarshaler();     return marshaler; } 

Enter this code immediately after the class definition.

MarshalManagedToNative()

The MarshalManagedToNative() method 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.

Here is the code for MarshalManagedToNative :

 
 IntPtr SYSTEMTIME_CustomMarshaler::MarshalManagedToNative(Object* pDateTime) {    int size = sizeof(SYSTEMTIME)+sizeof(int);    IntPtr ptrBlock = (Marshal::AllocCoTaskMem(size)).ToPointer();    int offset = ptrBlock.ToInt32()+sizeof(int);    IntPtr ptrST(offset);    int __nogc * pi = (int __nogc *)ptrBlock.ToPointer();    *pi = 1;    SYSTEMTIME* pst = static_cast<SYSTEMTIME*>(ptrST.ToPointer());    __box DateTime* bdt = static_cast<__box DateTime*>(pDateTime);    DateTime dt = *bdt;    *pst = MakeSystemTimeFromDateTime(dt);    return ptrST; } 

This code uses two kinds of pointersthe 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. 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. The integer is set to 1 because only one SYSTEMTIME is being created.

Now the casting and unboxing happens. 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 this function 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() , first presented in the previous section. 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.

Enter the code for MarshalManagedToNative() after the code for GetInstance() .

CleanUpNativeData()

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:

 
 void SYSTEMTIME_CustomMarshaler::CleanUpNativeData(IntPtr pNativeData) {    SYSTEMTIME * pST = static_cast<SYSTEMTIME*>(pNativeData.ToPointer());    int offset = pNativeData.ToInt32()-sizeof(int);    IntPtr pBlock(offset);    Marshal::FreeCoTaskMem(pBlock); } 

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() .

Enter the code for CleanUpNativeData() after the code for MarshalManagedToNative() , and build the project to be sure you haven't made any typing errors.

Changing the DllImport Attribute and the Calling Code

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. Change the definition of Log() to read like this:

 
 [DllImport("legacy", CharSet=CharSet::Ansi)] bool Log(String* message,          [In,MarshalAs(UnmanagedType::CustomMarshaler,          MarshalTypeRef=__typeof(SYSTEMTIME_CustomMarshaler))]          __box DateTime* time); 

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. Edit the main() function to read as follows:

 
 int _tmain() {     System::Console::Write(S" 1 + 2 is ");     System::Console::WriteLine(__box( Add(1,2)));     String* s = new String("Testing with a heap string");     Log(s, __box(System::DateTime::Now));     System::Console::WriteLine(S" log succeeded");     return 0; } 

Build and run the code, and examine the log.txt file in the root of the C: drive. You should see the current date and time, along with the new message from the main() function. You have successfully written a custom marshaler for PInvoke so that managed code can pass new managed objects to legacy DLL code, rather than having to work directly with legacy structures and classes.



Microsoft Visual C++. NET 2003 Kick Start
Microsoft Visual C++ .NET 2003 Kick Start
ISBN: 0672326000
EAN: 2147483647
Year: 2002
Pages: 141
Authors: Kate Gregory

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