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
- Anonymous
March 16, 2007
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. - Anonymous
May 29, 2007
Where's the performance comparison you promised? - Anonymous
July 10, 2007
touche Ben. I apologize for dropping the ball, it happens too often on this blog. I'll work on it this week. - Anonymous
July 11, 2007
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 .netmoduleWhat am I missing?Thanks,Olga - Anonymous
July 18, 2007
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..... - Anonymous
March 16, 2009
Hi Olga,I have exactly the same problem:atal error LNK1302: only support linking safe .netmodules; unable to link ijw/native .netmoduleI am trying to create a console application that use the HelloNative dll file. - Anonymous
April 10, 2009
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! - Anonymous
April 10, 2009
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 :-) - Anonymous
May 09, 2009
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 - Anonymous
August 26, 2009
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. - Anonymous
August 07, 2011
Did anybody solve the dll linker issue ? What was the fix?