C#/C interop with DllImport

Introduction

So you have to 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 are : first you’re right when you think DllImport is straightforward for simple interfaces, second this is the no-overengineering, no-overhead, no-nonsense, KISS tutorial for it.

So keep reading and in 10 minutes you’ll 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.dlllibrary.
    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.dlldynamic 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.dlldynamic library.

The C# application

With such an incredible C library we needed a decent C# application to demonstrate its power :

using System; // Console
using System.Runtime.InteropServices; // DllImport

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 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 diner 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) (i.e. 32-bit platform), and will then be allowed to call native x86 functions.
(If you’re not interested by the technical intricacies you can safely go to the end of this article)

But why do we need to specify the target platform?
Because, as you may have noticed in the CL compiler self-description, we’ve used the 32-bit version of CL, so we generated a 32-bit native library that can only be called from 32-bit native code.
And, tough the .Net assembly code generated by CSC, the MSIL, will be the same for all the target platforms, it will be loaded and turned into different native codes, during the JIT compilation, by different CLR.
So we ask the CSC compiler to mark the resulting .Net executable assembly as tied to 32-bit native code meaning it must be handled by a 32-bit CLR who knows how to interact with 32-bit native code.
If you forget this flag you’ll end up with this kind of error when running the generated EXE on a 64-bit system: “Unhandled Exception: System.BadImageFormatException: An attempt was made to load a program with an incorrect format. (Exception from HRESULT: 0x8007000B)” because, by default, CSC marks the assembly as targeting the anycpu platform, meaning it can be used by any CLR: on a 64-bit system it will then be handled by the 64-bit CLR, not the 32-bit one.

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 have 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.
Moreover, for those intrepid enough to read the technical details, I hope you’ve learned something useful about the inner workings of the .Net framework.

DllImport is perfect for simple procedural APIs but if you have a richer object-oriented API (typically written in C++) then you’ll need more powerful tools:

  • C++/CLI wrapper: low-level approach without any external tools based on the ability of the C++/CLI language to talk both to .Net and native code,
  • SWIG: generic interop framework with support for a large range of languages.

If you have any question or remark about this article feel free to let a comment, I really appreciate any feedback.

6 thoughts on “C#/C interop with DllImport

  1. Pingback: C#/C Interop | Sezgin Ege

  2. Pingback: C# / C Interop | Sezgin Ege

  3. What if inside the next function we want to save the result in a file and the file cannot be opened for writing?
    No exception handling mechanism defined in our .dll and we need to catch the exception in our C# code. How to do it?
    There are a bunch of StackOverflow answers like using:
    catch(Exception){…}
    catch{…}
    etc
    but none worked for me.

    Basically the question is: how to reliably catch unmanaged exceptions in managed code?

    • Hi Adi,
      AFAIK you can’t handle native exceptions on the managed side.
      So if the C code does not implement some error handling mechanism like SEH you won’t have any chance to be notified from your .Net/C# code and it will crash.
      Keep me informed if you find a viable alternative.

Leave a Reply

Your email address will not be published. Required fields are marked *

Prove me you\'re human :) *