Introduction
So you have or want to develop a native component (typically a DLL written in C or C++) with a simple API and you need to use it from another component developed in C#. You know that you can use the DllImport
mechanism and you’ve seen the 156387 tutorials that show how to make it with kernel32.dll (and by the way this is sometimes only what you need). You’ve also seen some tutorials that to illustrate what you think is a simple matter are using huge codes and made you scream “pleeaaase get to the point!”, or the ones that ask you to setup a Visual Studio project, but you want to avoid useless plumbing and understand what happens under the hood. You’ve tried by yourself and feel you’re almost there but you still have errors and start to feel frustrated.
The good news is: first you’re right when you think DllImport
is straightforward for simple interfaces, second this is the no-over-engineering, no-overhead, no-nonsense, KISS tutorial for it.
So keep reading and in 10 minutes, you’ll know DllImport
(almost) like a pro.
(Source code is available here.)
The Awesome C Library
Here is the C code for our top-notch numerical library, lib.c:
__declspec(dllexport)
int next(int n)
{
return n + 1;
}
The first line I’ve highlighted is the only thing to notice: it’s a directive that asks the compiler to mark the function as public
so that it can be accessed from external components. Without this directive, the functions are by default internal
to the library and are hidden from the outside.
Generating the library is straightforward and I give you three solutions:
- If you have Visual Studio on your computer: open a Visual Studio Command Prompt, go to the folder containing your source code and invoke the CL compiler:
>cl /LD lib.c
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
Copyright (C) Microsoft Corporation. All rights reserved.
lib.c
Microsoft (R) Incremental Linker Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
/out:lib.dll
/dll
/implib:lib.lib
lib.obj
Creating library lib.lib and object lib.exp
This will generate a bunch of files, you can keep only the “lib.dll” library. You can check that the “next” function has been correctly exported with dumpbin
:
>dumpbin /exports lib.dll
Microsoft (R) COFF/PE Dumper Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file lib.dll
File Type: DLL
Section contains the following exports for lib.dll
00000000 characteristics
512A5028 time date stamp Sun Feb 24 18:38:48 2013
0.00 version
1 ordinal base
1 number of functions
1 number of names
ordinal hint RVA name
1 0 00001000 next
Summary
2000 .data
2000 .rdata
1000 .reloc
5000 .text
- If you do not use Visual Studio, you can use MinGW, a great cross-compiler: open the MinGW shell, go to the folder and invoke the GCC compiler:
$ gcc -shared -o lib.dll lib.c
This will only generate the “lib.dll” dynamic library. You can check that the “next
” function is exported with the NM utility:
$ nm --extern-only --demangle lib.dll | grep next
61fc1254 T next
- You can also use the Windows SDK that contains the CL compiler: instructions and output are similar to the Visual Studio situation.
Whatever the tools you’ve used, you now have a “lib.dll” dynamic library.
The C# Application
With such an incredible C library, we needed a decent C# application to demonstrate its power:
using System; using System.Runtime.InteropServices;
class App
{
[DllImport("lib.dll", CallingConvention = CallingConvention.Cdecl)]
extern static int next(int n);
static void Main()
{
Console.WriteLine(next(0));
}
}
When run, it will reveal to you what is the number following 0
. But to maintain the suspense, we first analyze the code, at least the two highlighted lines: they tell the C# compiler that somewhere in the “lib.dll” file, there is a function named “next
” which takes an integer as unique parameter and returns an integer. (If you’re in a hurry, you can skip the next paragraph and directly go to the compilation part.)
Moreover, this code says that a specific procedure should be used to call the native function: the so-called CDecl calling-convention. If you don’t specify this parameter, all will appear to work fine and you won’t notice any error, except if you activate the MDAs: you will then be presented with an error like “A call to PInvoke function has unbalanced the stack. This is likely because the managed PInvoke signature does not match the unmanaged target signature“. But why? First, you should know that the default calling convention for C functions is CDecl
. But by default, DllImport
uses StdCall
, hence the need to explicitly specify CDecl
to override the default behavior. Second, without diving too deep into the technical details, CDecl
states that the calling side has to do some cleanup after the call, whereas StdCall
states that this is the responsibility of the called side. So without more clues, the C# code would have called the function expecting it to cleanup but would have been surprised if it was not the case, hence the pInvokeStackImbalance MDA. (This kind of misconception about who has to do the cleanup after dinner has caused more than one divorce!)
Let’s now compile our C# application: locate the CSC C# compiler you want to use on your system (e.g. if you have .NET 4.0, you can find one in C:\Windows\Microsoft.NET\Framework\v4.0.30319) and invoke it this way:
>csc /platform:x86 App.cs
Microsoft (R) Visual C# Compiler version 4.0.30319.17929
for Microsoft (R) .NET Framework 4.5
Copyright (C) Microsoft Corporation. All rights reserved.
Simple, except the compilation flag /platform:x86
: it specifies that this application targets a specific native platform (x86), and will then be allowed to call native x86 functions. If you forget this flag, you’ll end up with this kind of error when running the generated EXE: “Unhandled Exception: System.BadImageFormatException: An attempt was made to load a program with an incorrect format. (Exception from HRESULT: 0x8007000B)“.
Hey! we’re done, we only have to run our Test.exe managed application to know the integer following 0:
>App.exe
1
Wow, I’d never guessed!
Conclusion
You’ve seen how to do simple DllImport
and you should now be able to use .NET/native interop via DllImport
in more complex, real-life, scenario.
If you have any questions or remarks about this article, feel free to let a comment.