Reusing Legacy DLLs in C#






4.48/5 (26 votes)
Nov 5, 2002
6 min read

195174

2240
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.
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:
using System.Runtime.InteropServices;
Let's start importing the functions:
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:
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, andLong
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 asstdcall
(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.