Interop 101 – Part 5

As overdue as this post is, let's just jump in. In my first 4 installments, I focused on the different ways you could access native functionality from managed code. In this post, I will flip the actors around and investigate how to expose managed functionality to native clients.

The first thing to note is that COM Interop can always solve this problem. Using very little work, you can take a managed assembly and use built-in framework tools to generate a COM shim for native C++ (or VB) clients. Of course that means your calling code has to go through COM and if this is solely for the purpose of interop and you don't need to use COM as your component technology then the performance cost is absolutely not worth it. Thus, once again, C++/CLI will save us :-)

In our little story, I have a HelloWorld C# type that looks like this:

 

public class HelloWorld

{

private static int Counter =
0;

public void Speak()

{

MessageBox.Show("Hello World #" + Counter++);

}

}

 

Now we have a C++ client that wants to access this functionality (apparently, this user doesn't know that MessageBox exists as a purely native API). The simplest way to access it is to compile the file where the calling code is using /clr and then instantiating this object and calling the method. Super easy. Hold on, the client just called and said the code must remain 100% native. Why? Who knows :-) In this case, we'll have to create an inverted wrapper that provides a purely native interface. Here we go…

The execution is simple: build a new DLL that is compiled /clr but that exposes a native class as opposed to a reference class (remember, C++/CLI preserves native semantics and automagically exports things correctly).

// NativeHello.h

 

#ifdef _MANAGED

#using <HelloWorld.dll>

#include <vcclr.h>

#else

#include <stddef.h>

#endif

 

class NativeHello

{

private:

#ifdef _MANAGED

gcroot<HelloWorld^>
hw;

#else

intptr_t hw;

#endif

public:

__declspec(dllexport) NativeHello();

__declspec(dllexport) ~NativeHello();

__declspec(dllexport) void
Speak();

};

 

With the following implementation.

NativeHello::NativeHello()

{

// initialize managed hello world

hw =
gcnew HelloWorld();

}

 

NativeHello::~NativeHello()

{

// nothing to do :)

}

 

void NativeHello::Speak()

{

hw->Speak();

}

 

Let's go through each part of this in turn. At the top, I enclosed two statements within a check to see if we are compiling as managed (i.e. with /clr). The #using statement is essentially the equivalent of #import for COM. For C# programmers out there, this statement is equivalent to adding a reference to an assembly. The #include statement introduces the gcroot abstraction, which I'll talk about in a second. Now why did I enclose these statements in #ifdef _MANAGED? Our goal is to create a DLL that can be accessed by a purely native client and unfortunately in the native world, libraries do not (exactly) describes themselves and we need to use a header file as the descriptor. When a native client includes our native wrapper header file, the code enclosed within the _MANAGED block will be ignored. This is necessary since these statements only make sense for managed compilands. Luckily, the native client only needs to know about the types/functions we're exporting and hiding these statements has no ill effect. The #else clause adds an #include for intptr_t mentioned below.

Our wrapper type is then declared with a private member called hw, which is of type gcroot< HelloWorld^> . In the converse example from my previous posts, we simply embedded a native pointer as a private member. The fact is, you can't have a handle embedded in a native type so the gcroot template creates an (seamless) indirection by using the BCL's GCHandle value type, which enables the native code to hold a managed object and prevents the CLR's garbage collection of the object. However, this template only makes sense when we're compiling managed. Thus, in the case of a native includer, the gcroot member is stored as a simple intptr_t, which has the same size as gcroot on any platform.

Of course, we need to start exporting some real functionality! The constructor, destructor and the Speak method are all exported in the traditional native manner using __declspec(dllexport). None of these prototypes expose the managed implementation as is our goal. In other words, here is the view of the wrapper class from a native client.

 

class NativeHello

{

private:

intptr_t
hw;

public:

NativeHello();

~NativeHello();

void
Speak();

};

 

Voila. We now have a wrapper for purely native clients that are unable to use /clr (VC6 clients I presume!). Of course, you should make sure these clients use the same compiler, unless you want to open the way for the most obscure bugs on the planet, nay, in the galaxy(mixing CRTs leads to the dark side).

If you find yourself needing this type of wrapper often, you should go take a peek at Paul DiLascia's generic version of this sample that generates wrappers for any managed type.