In my last post, I began my little foray into basic managed/native interop scenarios. The goal is to discuss (in a first step at least) the different ways one can access native code from the managed world. Arguably, the simplest method is the one I used in part 1, which is to use C++/CLI and its built-in understanding of native code. There are other ways to do this, especially since C# (and VB) users also need to cross the barrier now and again.
In this entry, we're going to look into using P/Invoke via DLLImport. That may sound a little esoteric but it will be clear in no time. P/Invoke is short form for Platform Invoke, which is what the managed world calls a transition into native irrespective of the language one is coding in. In fact, in part 1, the compiler took care of generating the underlying P/Invoke whenever a transition needed to happen. DLLImport is a .NET attribute that allows you to define entry points into native libraries. It is the C# equivalent of feeding the C++ linker an input library. And now, on with the show.
The first thing we'll do is take a look at a typical usage of DllImport and apply that to our ongoing example.
static extern Int32 MessageBox(UInt32 hWnd, String message, String Caption, UInt32 uType);
Seems pretty simple. This statement defines a static method in the current scope, which maps to a function exported in user32.dll. How we would go about applying this to the HelloWorld class from part 1? Previously we had instantiated the type and called its member function. Unfortunately, the technique we discuss in this part has a fundamental limitation. We cannot import types through DllImport, only functions. Thus, while DllImport is well-suited to C-style APIs like Win32, it becomes much more difficult with object-oriented C++ code. However, this is software and there's always a way. Let's dive in and find out what we need to do to make this work (and hopefully realize this is not a good way to go).
First, we need an instance. Native objects live in the native heap and cannot be instantiated by the CLR, which only works with the managed heap. So we need to expose the native object's "new" and "delete" functionality as flat, exported APIs. We can do this by adding two functions to our original piece of C++ such that it looks like this.
class __declspec(dllexport) HelloWorld
void SayThis(wchar_t *phrase);
extern "C" __declspec(dllexport) HelloWorld* HelloWorld_New();
extern "C" __declspec(dllexport) void HelloWorld_Delete(HelloWorld* hw);
A number of things have changed here. We've added two functions outside of the HelloWorld class that wrap its constructor and destructor. We expose them as extern "C" in order to prevent the compiler from decorating the name of the functions. "Decoration?" you ask. Indeed, in C++ the compiler will mangle the names of the HelloWorld methods such that the CLR cannot identify the proper entry point for a function. You may recognize this issue when you see the EntryPointNotFoundException. Luckily we can help. There is a very useful tool called dumpbin, which provides tons of information about a binary (executable, library etc…). If we want to find what functions are exported via our DLL, we need only do the following:
dumpbin /exports interop101.dll
Looking through the output of this program, we find our target.
?SayThis@HelloWorld@@QAEXPA_W@Z (public: void __thiscall HelloWorld::SayThis(wchar_t *))
We can thus write the following C# code.
[DllImport("interop101.dll", EntryPoint = "HelloWorld_New")]
public static extern IntPtr NewHelloWorld();
[DllImport("interop101.dll", EntryPoint = "HelloWorld_Delete")]
public static extern void DeleteHelloWorld(IntPtr hw);
EntryPoint = "?SayThis@HelloWorld@@QAEXPA_W@Z",
CharSet = CharSet.Unicode,
CallingConvention = CallingConvention.ThisCall)]
public static extern void SayThis(IntPtr thisptr, String phrase);
static void Main(string args)
IntPtr hw = NewHelloWorld();
SayThis(hw, "I'm a C# application using DllImport!");
I suppose this deserves a bit more explanations. The EntryPoint argument to DllImport should be fairly intuitive as we're just guiding the CLR, which cannot auto-magically glean the answer. As for SayThis, I have added two essential arguments to its associated DllImport attribute. The first is specifying the right character set. Since our Message Box expects a Unicode string, the CLR must convert strings appropriately. The second is the calling convention. Since SayThis is an instance method, it contains an implicit argument that represents the "this" pointer of the receiver. In our case, this is a native pointer that we must explicitly pass as an argument.
Voila. Seems easy? Ugly? Weird? Tedious? The least one can say is that this mechanism is quite practical when dealing with flat C-style APIs. This fact is even more true thanks to pinvoke.net, which maintains a repository of mappings so you don't have to do this work in common cases.
In my not so humble opinion though; when it comes performing interop within your code base, this is not a scalable mechanism. In Part 3, we will examine how C++ once again helps make this scenario elegant.