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,
CharSet = CharSet.Ansi,
CallingConvention = CallingConvention.Cdecl)]
public static extern 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 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, 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.
| You must Sign In to use this message board. |
|
|
 |
 | Thanks  Bobobob21 | 7:46 14 Jan '09 |
|
|
 |
|
 |
Please read the article: "Marshalling: Using native DLLs in .NET" http://blog.rednael.com/2008/08/29/MarshallingUsingNativeDLLsInNET.aspx
It's an in-depth article about how to use a native DLL (or C++ DLL) in your managed .Net code. The article shows which types are interoperable, how to import a DLL, how to pass strings, how to pass structures and how to de-reference pointers.
And C# source code examples are included.
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
 |
Does this article still apply for Visual Studio 2005 ? Or does the Net Framework 2.0 have any new ways to reuse Legacy DLLs ? Thanks for the help. Jaime Arcila
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
 |
Hi.
I was trying to use an old Dll someone has written many years ago. We can't find the source but we think it is written in VB 6 or older.
Its functions takes Variants as input and return parameters e.g. this (In VB.NET variang => Object)
Private Declare Function xl_daysbetween Lib "zerotics.dll" (ByRef date1 As Object, _ Optional ByRef date2 As Object = Nothing, Optional ByVal cldr_conv As Object = Nothing, _ Optional ByVal fraction As Object = Nothing) As Object
When I try to call it I get Exception: PInvoke restriction: cannot return variants. If I change the return parameter type to something else than Object I get: PInvokeStackImBalance was detected
Does anyone know if it is at all possible to use an old DLL which returns variants from .NET?
/Thomas
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
 |
Thanks for the article Marius. After searching around the web and finding a bunch of different ways to pass a char* from a C++ app to a C# app I decided to use your code. Very well explained and worked perfectly. I'll be using this methods to pass XML back from a eVC++ dll that reads Windows CE a CEDB to a C# exe which will serialize the XML and send it to a web service. The C++ dll was necessary since CEDB API is not availabe in the .NET Compact Framework.
-Scott
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
This article was very helpful to me in calling legacy dll functions from a C# application, but now I'm trying to make one simple call from a C# assembly, and keep getting a 'System.DllNotFoundException'
I did not have this problem calling this same function directly from a C# App. The Dll being called resides in the project directory.
If you can help me out please respond. I've listed a few lines of code below.
Thanks, Jim
using System; using System.Runtime.InteropServices;
namespace PIMotion { public class Mercury { [DllImport("MercLib", EntryPoint = "MCRS_getDLLversion", ExactSpelling = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] public static extern int MCRS_getDLLversion(); public Mercury() { int DllRev; DllRev = MCRS_getDLLversion();
}
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
 |
Hi, I had the same problem, I found out that the external DLL requires to be added as in German "Verweis" (probably "Link" in English?). But this is only possible if the DLL is translated with the "CLIR"-Option enabled.. Regards. Jo
|
| Sign In·View Thread·PermaLink | 1.50/5 |
|
|
|
 |
|
 |
Hi,
This article works excellent when i am trying to import any function defined in the MFC DLL. But i am not able to import class from MFC dll into my C# code. I want to instantiate MFC class in my C# application and refer to all methods of that class. Can anybody help me out, and tell how the class declaration should take plase in C# code as we do for function declarations?
|
| Sign In·View Thread·PermaLink | 1.50/5 |
|
|
|
 |
|
 |
As far as I know you cannot import directly MFC classes in C#. The only way I know is to write a proxy class in managed C++, that wraps the required classes into fully .NET classes (managed). This is quite cumbersome since you cannot simply derive a class and use __gc keyword (you cannot break the line between managed and unmanaged worlds with derivation). I implemented such a proxy class, and it took some time, with only one class and around 20-30 methods.
Regards, Marius CONSTANTIN
|
| Sign In·View Thread·PermaLink | 2.43/5 |
|
|
|
 |
|
 |
Question: if I modify the DLL function and rebuild the dll, the application that load the dll (ex: a Web Application, with DLLIMPORT) don't apply the modify; why? I must reboot windows to apply the modify in the web page...every time Stop and re-launch IIS is not succefully: are there a cache memory where windows save the compiled DLL at run time?
Thank you for help, bye!
|
| Sign In·View Thread·PermaLink | 4.00/5 |
|
|
|
 |
|
 |
Check to make sure that the DLL is not referenced by Visual Studio. If so, you close Visual Studio, recompile and start again Visual Studio. A tool to help you is Process Explorer from SysInternals.
Regards, Marius CONSTANTIN
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
The problem is the "aspnet_wp.exe" process that probably mantain the DLL compiled in memory. If I stop the process (with task manager) and re-launch the web application it's all right. Thanks, Matteo Perazzo
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
:( From the programming forums it's obvious to see that what people need is a way to help them to declare the function prototypes. In the ideal world, someone would introduce a RCW debugger, and that would be $astonishing$. But I guess we could begin with a simple table of the most useful and often used WIN32 functions : SendMessage, FindWindow, ... I can envision such an article would become a reference for a lot of people wasting their time with this boring and undebuggable manual mapping thing.
So, IMHO, you should change your article title to a less generic one, so at least people understand : - what you are talking about : I had to read the entire article before I figured out what you were trying to teach. - that it's not as broad a covered topic as the title currently suggests, and you simply handle a tiny case which should only be given its tiny corner.
Back to real work : D-25.
|
| Sign In·View Thread·PermaLink | 4.60/5 |
|
|
|
 |
|
 |
.S.Rod. wrote: So, IMHO, you should change your article title to a less generic one, so at least people understand : - what you are talking about : I had to read the entire article before I figured out what you were trying to teach. - that it's not as broad a covered topic as the title currently suggests, and you simply handle a tiny case which should only be given its tiny corner.
I totally agree with you here. This article's name is a bit too broad. And I do agree that it would be nice to have an RCW debugger. It'd make me a lot more inclined to do Win32 API stuff than I would do under the current conditions.
You will now find yourself in a wonderous, magical place, filled with talking gnomes, mythical squirrels, and, almost as an afterthought, your bookmarks -Shog9 teaching Mel Feik how to bookmark
I don't know whether it's just the light but I swear the database server gives me dirty looks everytime I wander past. -Chris Maunder
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
You're alright to a given extent. However, the usefulness of articles is what counts most.
Back to real work : D-24.
|
| Sign In·View Thread·PermaLink | 1.00/5 |
|
|
|
 |
|
 |
This article was a good introduction to this big problem with managed/unmanaged code. What it needs as a follow up is an analysis of the P/Invoke calls and how the different marshalling affects the calls.
S.Kuznicki
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
 |
You only need to use unsafe code if you are working directly with pointer types in your code. For example, if you had declared:
public static extern unsafe char* Append( char* ptr, char* arg1, char* arg2, char* arg3, bool isLast); then the unsafe would be required. If you are simply marshalling managed types to unmanaged code as pointers, or working with the IntPtr class, your code is not considered unsafe. For example, this is acceptable as "safe" code:[DllImport("kernel32.dll", CharSet = CharSet.Auto)] private static extern int GetShortPathName( [MarshalAs(UnmanagedType.LPTStr)] string path, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder shortPath, int shortPathLength);
|
| Sign In·View Thread·PermaLink | 2.50/5 |
|
|
|
 |
|
 |
You are right. First I tried to use the code without marshalling all parameters (unsafe), and I left the unsafe keyword. I didn't tried to remove unsafe to see what happens. So, in my example unsafe is not required.
Regards, Marius CONSTANTIN
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
|