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.

Comments (11)

  1. Maksa says:

    Boris,

    Did you consider writing an interop book?

    I’d say that there’s a market out there for that, made out of tons of people that have legacy C++ that they’d like to interleave it with something more cushioned. You already sold at least one copy.

  2. Ben says:

    Where’s the performance comparison you promised?

  3. borisj says:

    touche Ben. I apologize for dropping the ball, it happens too often on this blog. I’ll work on it this week.

  4. Olga says:

    Boris,

    I try to build this example and I can’t overcome the final step, when the executable is being linked. I get the following error:

    C:work.NETTestdebugNativeHello.dll : fatal error LNK1302: only support linking safe .netmodules; unable to link ijw/native .netmodule

    What am I missing?

    Thanks,

    Olga

  5. Alon says:

    Boris,

    This is a very good example. I’ve been using these techniques (bidirectional wrapping) in my project ,tha past two years, and it works very nice . I even used it to wrap Managed GUI Controls(Written in c#) to be used in MFC applications and other c++ code.

    One of the most important lesson i learn daily, is be carefull with dead poinerts. When you write a managed system that is based on a wrapping layer of native code , you must be extra carefull that no native object was deleted before it’s twin wrapper has been removed, and that you don’t GC a wrapper of a native object that is in use somewhere. These things can be a massive brain killer in debugging…..

  6. Normand Bédard says:

    Hi Olga,

    I have exactly the same problem:

    atal error LNK1302: only support linking safe .netmodules; unable to link ijw/native .netmodule

    I am trying to create a console application that use the HelloNative dll file.

  7. John Byerly says:

    I am also having the same problem with linking.  I have tried numerous approaches to this problem and have yet to get this going. There is next to no information on the net about calling managed code from unmanaged code (despite the titles of some of the pages to the contrary). I have yet to find a complete, front to back, solution.

    This blog post was great, but did not include an unmanaged call to the HelloWorld object!

  8. John Byerly says:

    Okay, I will admit to being a total idiot, in hopes that it will help someone else.

    I was linking my unmanaged app to the managed .dll file!

    Linking to the .lib file is what I meant. Why the linker did what I told it to do, rather than what I meant is anyone’s guess  πŸ™‚

  9. Catalina says:

    Hi John,

    I am having the same linking problem but the managed .dll does not create a .lib to link to. How did you solve that issue? Or how did you specify the linker to link to the .lib file?

    thanks,

    Catalina

  10. oyefremov says:

    What about default copy constructor and copy operator?

    NativeHello(const NativeHello&) and NativeHello& operator=(const NativeHello&) will be generated from .h file differently for managed and native envirounments. Copy constructor and operator= should be defined explicitly in this case.

  11. Did anybody solve the dll linker issue ? What was the fix?