Consuming Unmanaged C++ Class Libraries from .NET Clients

Introduction

Did you ever face the challenge of calling a regular (unmanaged) C++ class library from managed code? Well, I did, and obviously some of the folks on the discussion board did as well. So, why is it such a challenge?

While the .NET Interop Services offer very good support to integrate C-style DLLs and COM objects directly into your C# or VB.NET code, the same is not true for unmanaged C++ class libraries. If you want to make calls into an unmanaged C++ class library, you definitely have to write a wrapper class, and you have to write it in managed C++. No way out.

In this tutorial, I will introduce a set of three sample projects to demonstrate the most basic concepts of writing a managed wrapper around an unmanaged C++ class library. The diagram below depicts the general architecture:

The sample projects include one method, one property, and one event. If you want to learn the bits and pieces of Managed C++, the section "Managed Extensions for C++ Programming" from the MSDN is highly recommended.

An Unmanaged C++ Library

A simple unmanaged C++ DLL is provided to show how C++ classes can be exported. These are the important points:

To define the symbols that will be exported from a DLL, the __declspec(dllexport) and __declspec(dllimport) statements can be used in the class header file. However, because the header file will be used by the DLL project as well as by prospective client projects, we have to make sure that our symbols are exported from the DLL, but imported by clients. The following code does the trick:

#ifdef UNMANAGED_EXPORTS
   #define UNMANAGED_API __declspec(dllexport)
#else
   #define UNMANAGED_API __declspec(dllimport)
#endif

class UNMANAGED_API CUnmanaged
{
};

If UNMANAGED_EXPORTS is defined as a preprocessor directive in the unmanaged DLL project, this DLL will export all symbols of the class CUnmanaged. A client, however, would not define this prepocessor directive; hence, it would import those symbols.

In C and C++, callback functions are the equivalent of what we call events in other environments. There are different ways of implementing a callback mechanism in an unmanaged C++ class. For this sample, I have chosen to implement a callback class. This means that we define an abstract base class, which has to be overridden by any client that wants to actually provide callback methods. In the sample, this abstract base class happens to be nested:

class UNMANAGED_API CUnmanaged
{
public:
   class UNMANAGED_API CFeedback
   {
   public:
      virtual void OnValueChanged( int nValue ) = 0;
   };

   CUnmanaged( CFeedback* pFeedback );
   void SetValue( int nValue );

private:
   CFeedback* m_pFeedback;
};

The class CUnmanaged maintains a pointer to an instance of a class derived from CUnmanaged::CFeedback. Such a derived class has to be implemented by any client that wants to receive the OnValueChanged callback event.

For our sample, the callback provided by a client will be called from inside SetValue():

void CUnmanaged::SetValue( int nValue )
{
   ...
   m_pFeedback->OnValueChanged( nValue );
}

Once again: The OnValueChanged() handler that is called here must be implemented by a client application. CUnmanaged::CFeedback does not provide an implementation of that handler.

The Managed C++ Wrapper Library

This is the centerpiece of this tutorial: the actual wrapper library. What do we have to do to write a .NET-style wrapper? Regarding the interface to the .NET client applications, we have to provide .NET methods, properties, and delegates/events. Regarding the unmanaged library, we have to provide an implementation of its callback mechanism. So, the main task is the translation between the delegate/event concept on the one hand and the callback concept on the other hand. Compared to this, methods and properties are boilerplate, so I will not focus on them.

The managed wrapper library will contain both managed and unmanaged code. Hence, it needs to be a Mixed Mode DLL. What's that? Mixed Mode is the opposite of Pure Intermediate Language. It allows a DLL to have an explicit entry point as well as static variables, which are required by the C Runtime library. To convert your class library project to Mixed Mode, you have to make some changes in the Linker section of your project's property pages:

  • Add /noentry to the Additional Options field of the Command Line page.
  • Add msvcrt.lib to the Additional Dependencies field of the Input page.
  • Remove nochkclr.obj from the Additional Dependencies field of the Input page.
  • Add __DllMainCRTStartup@12 to the Force Symbol Reference field of the Input page.

Having our wrapper enabled to implement managed as well as unmanaged code, we do exactly this. We will actually provide a managed class to serve as an interface to .NET clients, and an unmanaged class to implement the callback mechanism required by the unmanaged DLL. First, we derive a callback class from the unmanaged DLL's CUnmanaged::CFeedback:

#include "..\Unmanaged\Unmanaged.h"
using namespace ManagedLib;

class Feedback : public CUnmanaged::CFeedback
{
public:
   Feedback( Managed* p );
   void OnValueChanged( int nValue );

private:
   gcroot< Managed* > m_pManaged;
};

Because we are deriving this class from a class defined in our unmanaged DLL, we have to include its header file. Because we are using our managed wrapper class (see definition below), we have to use its namespace, "ManagedLib".

But, the main trick here is using the gcroot template. This actually implements a smart pointer, i.e. a pointer to a variable that frees itself after going out of scope. We need this template here because we want to access a managed object (a pointer to an instance of "Managed") from an unmanaged class, which normally would be impossible. Why would that be impossible? Because managed objects are subject to the garbage collector, their address is not fixed; it will be moved around by the garbage collector. Hence, unmanaged code is unable to maintain a pointer to managed objects. However, .NET provides a handle to managed objects, called GCHandle. To simplify avoiding memory leaks, this is wrapped into a smart pointer, and this smart pointer is called gcroot.

Next comes the definition of our managed class. The following code extract highlights its most important features:

#include "..\Unmanaged\Unmanaged.h"
class Feedback;

namespace ManagedLib
{
   public __delegate void ValueChangedHandler( int i );

   public __gc class Managed
   {
   public:
      Managed();
      ~Managed();
      ...
      __property int get_Value();
      __event void add_ValueChanged
         ( ValueChangedHandler* pValueChanged );
      __event void remove_ValueChanged
         ( ValueChangedHandler* pValueChanged );

   private public:
      __event void raise_ValueChanged( int i );

   private:
      CUnmanaged __nogc* m_pUnmanaged;
      Feedback __nogc* m_pFeedback;
      ValueChangedHandler* m_pValueChanged;
   };
}

A forward declaration introduces the unmanaged class Feedback. Its header file will be included by the .cpp file. Because we implement a managed class, we define a namespace. The delegate as well as the event methods are standard Managed C++ syntax, as is the definition of the property. The method raise_ValueChanged can be made internal by the private public specifier because it will be called only from within the same project. The managed class will create instances of the unmanaged classes CUnmanaged and Feedback, so we define pointers to them. Because these instances are unmanaged, they are not affected by garbage collection, so we use the __nogc specifier for them.

Implementation of the Managed class is straightforward. First, we create instances of both unmanaged classes:

Managed::Managed()
{
   m_pFeedback  = new Feedback( this );
   m_pUnmanaged = new CUnmanaged( m_pFeedback );
}

Note that the instance of Feedback receives a this pointer because it will raise Managed's ValueChanged event. The instance of CUnmanaged, on the other hand, receives a pointer to the Feedback instance because it will call its callback method.

Because both pointers point to unmanaged objects, they have to be deleted manually. The Managed class' destructor takes care of that:

Managed::~Managed()
{
   delete m_pFeedback;
   delete m_pUnmanaged;
}

The mechanism to register delegates as well as the event method have to implemented yet. In terms of design patterns, .NET is using the Observer pattern here, so we implement an add_ as well as a remove_ method.

void Managed::add_ValueChanged( ValueChangedHandler* pValueChanged )
{
   m_pValueChanged = 
      static_cast< ValueChangedHandler* >
      (Delegate::Combine( m_pValueChanged, pValueChanged ));
}

void Managed::remove_ValueChanged( ValueChangedHandler* pValueChanged )
{
   m_pValueChanged = 
      static_cast< ValueChangedHandler* >
      (Delegate::Remove( m_pValueChanged, pValueChanged ));
}

void Managed::raise_ValueChanged( int i )
{
   if( m_pValueChanged != 0 )
      m_pValueChanged->Invoke( i );
}

To complete the managed wrapper, one last piece of code needs to be done: the implementation of the callback method that is responsible for actually raising the .NET event. This happens in the Feedback class that has been derived from the CUnmanaged::CFeedback abstract base class.

void Feedback::OnValueChanged( int nValue )
{
   m_pManaged->raise_ValueChanged( nValue );
}

This callback method will be called by CUnmanaged, and it will raise the event implemented by the Managed class.

A Simple C# Consumer

The simplest part of this tutorial is the consumer. It is that simple because it is completely C#. This is the code:

static void Main(string[] args)
{
   Managed oManaged = new Managed();
   oManaged.ValueChanged
      += new ValueChangedHandler( OnValueChanged );
   oManaged.SetValue( 42 );
}

static void OnValueChanged( int i )
{
   ...
}

An event handler (OnValueChanged) is defined and added to our instance of the Managed class. That's it.

Building and Running the Projects

The source code is provided as a Visual Studio 2003 solution. Load the solution ConsumingUnmanagedCode.sln into VS 2003. Execute "Build Solution," but be aware that you have to execute this twice. This is because after successfully building Unmanaged.dll and Managed.dll during the first run, you may have to add the correct reference to the Managed.dll to the Consumer project. Sorry for this inconvenience...

After having successfully built all three projects, press F5 to run the app and see its output in the debug window.



About the Author

Andreas Wieberneit

I started my IT career during the late 80s, using programming environments such as C, Pascal and dBase. With the advent of the 90s, I discovered C++ and OOP. Since then, I devoted most of my work to build modular systems, composed of DLLs, COM objects, and service applications; to participate in the development of multi-tier business systems; and to communicate with Oracle, SQL-Server and postgreSQL databases, all kinds of hardware devices, as well as smart cards. With the beginning of the new millenium I switched to .NET, where the need to integrate existing components forced me to deal with the secrets of interoperability. During the past years, I shared my working hours between managing software projects and doing hands-on implementation of software systems. Currently, I live and work in Toronto, Canada. You can reach me at awieberneit@yahoo.ca.

Downloads