Microsoft & .NETVisual C#Head-Spinning Continued: P/Invoke

Head-Spinning Continued: P/Invoke

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

From Kate Gregory’s Codeguru column, “Using Visual C++ .NET“.

I’m continuing a series of columns on ways to access legacy C++ code from new managed C++ code. First I introduced you to the legacy class, which does arithmetic, and laid out the choices you have as a C++ programmer for interop. The second column wrapped the legacy class up as a COM component and accessed it from both unmanaged and managed code. But since COM Interop carries the largest performance penalty of all the choices, it should be used only when it’s needed. There are other ways to run that old code from new code.

I introduced P/Invoke in this column almost a year ago, showing how to call some handy DLLs that you’re sure to have on your machine, because they come with Windows (here). In this column I’ll show how to wrap your legacy code into a DLL as well as how to call it from both managed and unmanaged code.

Creating a DLL

When you create a DLL in unmanaged C++, you can write a collection of global functions, or you can write member functions of one or more classes. If you’re calling it from unmanaged C++, writing a class and its member functions is an excellent choice. You need to keep in mind, though, that C++ functions are decorated (mangled) with information about the parameters they take and the class they are in. This is invisible when you use the DLL from another unmanaged application, but a bit of a problem when calling from managed code. It’s simpler if your DLL contains only global functions, which you can mark with extern “C” to prevent decoration. You don’t have to throw away your object-oriented approach: write one DLL for each class, and have the member functions of the class as global functions in that DLL.

If you adopt this approach for a large and complex system, remember that you don’t have to convert your old code to a DLL: you can write a new DLL that offers certain services, and that calls your old code to implement those services. This simpler facade can use classes from your existing library and simply wrap the functionality into the DLL. In this sample, I’ll implement Add right in the DLL, just because it’s so simple.

I created a new Win32 project and set the application type to DLL. Then I coded the Add() function:

extern "C" __declspec(dllexport) 
double Add(double num1, double num2)
{
   return num1 + num2;
}

The extern “C” prevents name decoration, which is normally applied even to global functions. The __declspec(dllexport) indicates to the compiler that this function is to be exported from the DLL. The rest of the function is straightforward: in a more complex system this would be a wrapper function that calls methods of other classes or functions in another DLL.

Using the DLL — The Old Way

I created an ordinary unmanaged console application to use the Add() function. The single file of code looks like this:

#include "stdafx.h"
#include 
using namespace std;

extern "C" __declspec(dllimport) double Add(double num1,
                                            double num2);

int _tmain(int argc, _TCHAR* argv[])
{
  cout << "1 + 2 is " << Add(1,2) << endl;
  return 0;
}

To avoid any “DLL Hell” issues, I prefer to make local copies of any DLLs I use, so that changes to the DLL don’t affect my code unless I choose to copy the changed DLL. I copied legacy.dll into the project folder for the console application. When you build a DLL project in Visual C++, you get a companion .LIB file called the import library; I copied that file to the same place. Then it’s just a matter of linking the import library into the project: in Solution Explorer right-click the project and choose Properties, expand the Linker section, select Input, and add legacy.lib to the Additional Dependencies properties.

That’s all it takes to use the DLL. It’s not very different from linking with a static library, as far as your coding effort is concerned. The real issue is that when the DLL changes, your code uses the new code. That’s a double-edged sword, of course. Keeping a private copy can spare you that worry, while still enabling your old unmanaged code and your new managed code to use the same library, as you’re about to see.

Using the DLL — The New Way

How does managed code use a DLL? It’s all over the documentation: Platform Invoke, also known as P/Invoke. Most examples show you how to access a Windows DLL, in case the Base Class Libraries don’t offer some particular functionality you need. It’s no different to access your own DLLs though.

In a console application, to use the Add function, you need to declare it with a DllImport attribute, like this:

using namespace System::Runtime::InteropServices;
extern "C" {
[DllImport("legacy")]
double Add(double num1, double num2);
}

The parameter to this attribute, legacy, names the dll. Notice that you omit the extension: in some other .NET languages you must include the extension, so don’t let sample code from another language confuse you here. You don’t need to add any references to use the DllImport attribute, but it is in a namespace of its own, making a using statement convenient.

Copy the DLL into the project folder, and then you can just call the function:

System::Console::Write(S"1 + 2 is ");
System::Console::WriteLine(__box( Add(1,2)));

As in my COM Interop column, you need to box the double that’s returned from the legacy function in order to pass it to WriteLine(), which doesn’t know how to handle double variables, or any other unmanaged type. No matter how you settle your interop issues, you’re going to need to convert between managed and unmanaged data as part of your solution. If you’re lucky, boxing alone will take care of what you need.

The real power of PInvoke lies outside this example. It provides access to the DLL, but it can layer extra capabilities on top of that. For example, if the DLL function takes a char* string, you can declare the .NET version of the function as taking a System::String*, and the framework will handle the conversion for you automatically. You can add a variety of attributes to control things like string marshaling, structure layout, and more. You can even write your own marshaling code to convert between a managed and unmanaged data type. I’ll be returning to more advanced PInvoke topics in a later column.

Is This The Way For You?

Does it make sense to wrap your existing code into a DLL, change your existing unmanaged code to use the DLL, and then have your new managed code use the DLL with PInvoke? Well, it’s a higher-performance solution than the COM Interop approach I presented in an earlier column. However, if the default marshaling works for you, it’s a bit of a waste to use PInvoke. Next time I’ll show you how to access that same DLL without the DllImport attribute — from C++ only.

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.


# # #

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories