Click here to Skip to main content
15,867,939 members
Articles / Programming Languages / C#
Article

Reusing Legacy DLLs in C#

Rate me:
Please Sign up or sign in to vote.
4.48/5 (28 votes)
18 Nov 20026 min read 193K   2.2K   74   22
This article gives you a way to reuse existing code, without rewriting it to .NET

Introduction

This article should give you a way to reuse existing code, which is supposed to be bug free ;), without rewriting it to .NET Framework.

Background

When the .NET and managed code appeared, it was clear that is required the interoperability between the legacy code written in some language (like C++ or Delphi, you name it) and the brand new .NET languages. Of course, .NET Team didn't leave out this posibility, and introduced P/Invoke or Platform Invoke. The MSDN documentation is quite good on using Win32 API by using P/Invoke, however it doesn't explain very clearly how to use a custom DLL in your code.

Also, I must say that P/Invoke isn't the only way to reuse the existing DLL code. You can use managed C++ to import your DLL, but this is not the scope of this article.

System Requirements

To compile the solution you need Visual Studio .NET and Visual Studio 6.0 to create the DLL. The sample application is written in C# and imports a custom DLL written in Visual C 6.0 to do some complicated stuff that was written before .NET appeared :). Also to run the binaries you will need .NET Framework to be installed on the target machine.

The DLL stuff

I've written a sample DLL that exports 2 functions MyAppend and KillBuffer.

char* MyAppend(char* in, char* arg1, char *arg2, char*arg3, BOOL last);
void  KillBuffer(char* in);

MyAppend gets 5 parameters: first is a char* buffer that holds the concatenated string so far and should be the value returned by a previous MyAppend call or NULL, next 3 are the string to be added and the last one is a boolean that says that this call is the last one or there are more to come. The internal behaviour is to write at in-4 an int that holds the size of the allocated buffer, so it isn't simply a string. We will see how to convince .NET not to modify this data.

KillBuffer is simple. It simply frees the buffer used by MyAppend.

All functions are exported as C (not decorated) and are using cdecl calling convention.

The C# part

In C# I used a sample console application that calls three times MyAppend, displays the concatenated thing and disposes the buffer used.

First of all we need tell the compiler that we will be dealing with legacy application and pointers (only if required). So, in project properties we allow unsafe code (see screenshot). In my example actually unsafe was not required, because I choose to marshal all parameters that involve pointers (strings as LPStr, and I am using IntPtr class). But, if you don't marshal explicitly all parameters that involve pointers, unsafe keyword it's required. Actually it is a simple way to find if unsafe is required. Compile the project, and you will see complaints if unsafe code is required.

Project Properties

After that, we can start declaring our functions. Usually, it's better to declare them in a separate class, but this is not a requirement. You can add the functions to what class do you want since the functions will be static.

Another thing to mention is that you need to reference the InteropServices namespace like this:

C#
using System.Runtime.InteropServices;

Let's start importing the functions:

C#
class MyDLL {
  [DllImport("legacy",
    EntryPoint = "MyAppend",
    ExactSpelling = true,    // Bypass A or W suffix search
    CharSet = CharSet.Ansi,    // We want ANSI String
    CallingConvention = CallingConvention.Cdecl)]
                // Called as Cdecl
  public static extern /*unsafe*/ IntPtr Append(
      IntPtr ptr,
      [MarshalAs(UnmanagedType.LPStr)]
      string arg1,
      [MarshalAs(UnmanagedType.LPStr)]
      string arg2,
      [MarshalAs(UnmanagedType.LPStr)]
      string arg3, bool isLast);

  [DllImport("legacy",
    EntryPoint = "KillBuffer",
    ExactSpelling = true,
    CharSet = CharSet.Ansi,
    CallingConvention = CallingConvention.Cdecl)]
  public static extern /*unsafe*/ void StringDispose(IntPtr ptr);
}

It looks interesting isn't it?. First of all, not all the fields of the DllImport attribute are required. I choosed to write them because it's better to bypass defaults which can give you a strange behaviour. ExactSpelling tells the compiler to skip the A or W suffix search and search a function named exactly as we tell. As you may know, the API functions usually have 2 variants: ANSI and Unicode. Because of that, if you search the header definitions you will a lot of ifdefs surrounding function definitions. For example, let's take GetModuleFileName function. If you search the DLLs for a function named like this you will not find it. Instead you will find GetModuleFileNameA and GetModuleFileNameW variances. By using ExactSpelling turned off, you tell the compiler to search this variances, allowing you to import Win32 API easily. In our case we know exactly what we search for.

EntryPoint allows you to rename the function to whatever you want, without recompiling the DLL. This option is useful if you import several functions from different DLLs in the same class and you have collisions in function names.

CharSet tells the compiler how to expand LPTSTR. Charset.Ansi means that TCHAR is 1 byte variance, Charset.Unicode means 2 byte. By setting this field I could skip the MarshalAs attribute.

CallingConvention tells the compiler which is the calling convention for our DLL.

As you may noted, the first parameter for MyAppend it's declared as char* in the DLL, but as IntPtr in C#. Why is that? If we declared as string, the .NET framework would marshal the string for us because the only type of strings that exist in the .NET world are Unicode. By marshaling, we would be losing the size of the allocated buffer that is stored at ptr-4. Declaring the parameters as IntPtr allows us to preserve the content (since the framework sees only a pointer, and we accomplished the task.

The MarshalAs attribute allows us to specify the exact way to pass the parameters. Here we choose to set explicitly that strings are single byte.

The main function looks like:

C#
IntPtr target = IntPtr.Zero;
target = MyDLL.Append(target,"xx","yy","zzz",false);
target = MyDLL.Append(target,"xx","yy","zzz",false);
target = MyDLL.Append(target,"xx","yy","zzz",true);
Console.WriteLine("Returned: {0}",Marshal.PtrToStringAnsi(target));
MyDLL.StringDispose(target);

As you see, first we initialize the target pointer to NULL. We call three times the Append function, and we display the resulting string. To display we used another function Marshal.PtrToStringAnsi. Because the first parameter is a pointer in our declaration, we must convert it back to a string for display. PtrToStringAnsi function allows us to accomplish that. If the pointer was a Unicode string, we would use PtrToStringUni for example. Marshal class offers a lot of handy functions for conversion from/to legacy world.

Troubleshooting

If you fail to call your existing code, or you have strange results, you should check:

  • String type (ANSI or UNICODE). If you set the MarshalAs attribute you shouldn't have any problems
  • Correct mapping of types from unmanaged to managed code. Note that long in C++ is 4 bytes, and Long in C# is 8 bytes.
  • Calling convention. Mismatching this can get you to strange results, because if your function is declared as cdecl and you use it as stdcall (the default choice) you will see that everything works, but instead the function parameters will not be removed from stack, leading to a possible stack overflow.

Conclusion

.NET Framework is a great way to develop new applications. However, for some application it isn't feasible, or we don't have enough time to rewrite old code to .NET. So either we stick to the old application or we try to migrate it per module. By using P/Invoke you can accomplish this task pretty easy.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Romania Romania
Student in last year at Faculty of Automatic Control and Computers of Polytechnical University Bucharest.
I like programming a lot, working with many languages like C++, VB, VB.Net, C#.
Now I'm working at United Management Technologies Romania in C++.

Comments and Discussions

 
GeneralMy vote of 5 Pin
dnyan8613-Sep-12 1:24
dnyan8613-Sep-12 1:24 
GeneralMy vote of 1 Pin
dudeua15-Feb-10 13:55
dudeua15-Feb-10 13:55 
GeneralThanks Pin
Bobobob2114-Jan-09 6:46
Bobobob2114-Jan-09 6:46 
Big Grin | :-D Thanks I wanted to know how a .dll works and this realy helped. Big Grin | :-D
AnswerMarshalling: Using native DLLs in .NET Pin
meukjeboer25-Sep-08 2:30
meukjeboer25-Sep-08 2:30 
QuestionDoes this article still apply for Visual Studio 2005 ? Pin
JaimeVSDC17-Aug-06 5:42
JaimeVSDC17-Aug-06 5:42 
GeneralPInvoke restriction: cannot return variants. Pin
thomasholme8-May-06 2:07
thomasholme8-May-06 2:07 
QuestionRe: PInvoke restriction: cannot return variants. - Remember? Found a solution? Pin
Gert H28-Apr-10 22:13
Gert H28-Apr-10 22:13 
Generalthanks, this helped a lot Pin
scottelloco9-Mar-06 9:51
scottelloco9-Mar-06 9:51 
QuestionC# Dll calling C++ Dll Question Pin
jmason3097-Mar-06 5:18
jmason3097-Mar-06 5:18 
AnswerRe: C# Dll calling C++ Dll Question Pin
Jogel19-Aug-06 8:51
Jogel19-Aug-06 8:51 
QuestionHow to import MFC classes into C# application Pin
gaurav098116-Sep-04 3:40
gaurav098116-Sep-04 3:40 
AnswerRe: How to import MFC classes into C# application Pin
cchrism16-Sep-04 23:52
cchrism16-Sep-04 23:52 
GeneralProblem to edit and test DLL in Web App. Pin
peraonline20-Apr-04 3:31
peraonline20-Apr-04 3:31 
GeneralRe: Problem to edit and test DLL in Web App. Pin
cchrism20-Apr-04 4:13
cchrism20-Apr-04 4:13 
GeneralRe: Problem to edit and test DLL in Web App. Pin
peraonline20-Apr-04 20:44
peraonline20-Apr-04 20:44 
GeneralThis topic deserves a better approach Pin
Stephane Rodriguez.5-Nov-02 5:13
Stephane Rodriguez.5-Nov-02 5:13 
GeneralRe: This topic deserves a better approach Pin
David Stone5-Nov-02 5:16
sitebuilderDavid Stone5-Nov-02 5:16 
GeneralRe: This topic deserves a better approach Pin
Alexandru Savescu5-Nov-02 21:54
Alexandru Savescu5-Nov-02 21:54 
GeneralRe: This topic deserves a better approach Pin
Stephane Rodriguez.5-Nov-02 22:05
Stephane Rodriguez.5-Nov-02 22:05 
GeneralRe: This topic deserves a better approach Pin
SteveKuznicki5-Jan-06 7:35
SteveKuznicki5-Jan-06 7:35 
QuestionWhy unsafe? Pin
Richard Deeming5-Nov-02 4:51
mveRichard Deeming5-Nov-02 4:51 
AnswerRe: Why unsafe? Pin
cchrism5-Nov-02 5:04
cchrism5-Nov-02 5:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.