How to Build a DLL in MinGW and use it in C# with P/Invoke

This is a pretty niche topic for our first blog post, but I’ve been using MinGW compile some applications originally built for Linux on Windows.  Our company uses .NET to build GUI applications for Microsoft Windows and one frequent use case is callling a C dynamic link library (DLL) from C# or VB.NET code.

Today I’m going to write explain with some examples on how you can:

  1. Write a shared library in C
  2. Compile that library to a DLL using MinGW
  3. Augment the library so it is easy to P/Invoke from .NET
  4. Write a .NET class that uses the DLL

So, lets get started.  The first thing we need to do is write a C shared library that we want to use.  Lets make this simple to start with, three functions: Sum, Concatenate, CallTwice.

/* combiner.h */
#include <stdio.h>
#define EXPORT __declspec(dllexport)
extern “C” {
EXPORT int __stdcall Sum(int arg1, int arg2);
EXPORT int __stdcall Concatenate(char *s1, char*s2, char*dest);
EXPORT int __stdcall CallTwice(int (_stdcall *func)(int index));
}

/* combiner.cc */
#include “combiner.h”
EXPORT int Sum(int arg1, int arg2) {
return (arg1 + arg2);
}
EXPORT int Concatenate(char *s1, char *s2, char *dest) {
return sprintf(dest, “%s%s”, s1, s2);
}
EXPORT int CallTwice(int (_stdcall *func)(int index)) {
return func(0) + func(1);
}

Alright, so what is all this extra stuff doing?

  • extern “C” marks these as C functions so that the compiler doesn’t mangle the symbol name.  Compilers mangle symbol names for C++ functions to support function overloading (which C does not support).
  • EXPORT this is a macro that we apply to functions to indicate they are should be part of the DLL export.  This macro evaluates to __declspec(dllexport).
  • __stdcall marks the order in which parameters are placed on the stack and who is responsible for cleaning up the function call. While __cdecl is the default for C programs, most Windows API calls are __stdcall and thus is the default for P/Invoke.  One can use __cdecl, we just need to specify the calling convention in the C# code.

Now it is time to compile the DLL, lets do it in two commands

gcc -c combiner.cc
gcc -shared -o combiner.dll combiner.o -Wl,–add-stdcall-alias

The first command is pretty simple, it builds an object file from the source code in combiner.cc.  The second line is a bit more interesting as it creates a DLL from the compiled object file and there are two important options present:

  • -shared tells the compiler this is a shared library and not an executable;  if you forget this option, you may get the error “undefined reference to `WinMain`”
  • -Wl,–add-stdcall-alias is an argument to gcc that passes the linker option –add-stdcall-alias.  This option creates an alias for each function without the size of the arguments appended (as is convetion for stdcall).

Once you have created a DLL, check that it has the correct exports by using dumpbin, a utility supplied with Visual Studio.  If you do not have dumpbin, you can use a program like PE Explorer.  We are interested in the exports section for the DLL, so at the VS 2008 console run the command:

dumpbin /exports combiner.dll

Now, we need to consume the DLL in C#, so lets write a class in CombinerDLL.cs that will use the functions exported by the DLL:

using System.Runtime.InteropServices;
public delegate int CombinerCallback(int index);
public class CombinerDLL {
public const string DllName=”combiner.dll”;

[DllImport(DllName)]
public static extern int Sum(int arg1, int arg2);
[DllImport(DllName)]
public static extern int Concatenate(string s1, string s2, string s3);
[DllImport(DllName)]
public static extern int CallTwice(CombinerCallback function);
}

Next, we need to write a class that will test the behaviour is correct:

using System.Diagnostics.

The DLL we created must be in the same directory as the compiled C# application for the DllImport to work correctly.  To accomplish this I usually just add the DLL to the Visual Studio Project, and set “Copy to Output” as “Copy always”.  This way the latest DLL is copied to the output folder whether we build in Debug or Release mode.

Some problems that will likely occur:

  • DllNotFoundException – this occurs when the DLL specified in your import statement is not in current directory (generally the directory where the application is executing from)
  • EntryPointNotFoundException – this can occur if the name of the function you are trying to call has been mangled, you forgot use the linker option –add-stdcall-alias, or the function signature in your C# application does not match the C function signature in the DLL.  Ensure the function name is correct by using dumpbin or a similar utility

There is far more to the puzzles of P/Invoke than I have covered here, but this introduction should get you going.

References

http://msdn.microsoft.com/en-us/library/984x0h58%28v=VS.71%29.aspx
http://publib.boulder.ibm.com/infocenter/macxhelp/v6v81/index.jsp?topic=/com.ibm.vacpp6m.doc/language/ref/clrc11cplr312.htm
http://sig9.com/node/35
http://www.pinvoke.net/
http://support.microsoft.com/kb/177429

Advertisements
This entry was posted in .NET, C++, P/Invoke and tagged , , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s