Calling .NET from Unmanaged C++

My coworker Julie Nelson and I work on a legacy app written in C++/MFC. We wanted to take advantage of some managed devices such as .NET controls and web services, but the app is too big to rewrite in C#, so we needed a way to call these devices from the existing code. This article describes what we came up with: a fairly general method for calling managed code from unmanaged. The technique is demonstrated by a little dialog app that can communicate with the ADO.NET data source of your choice.

The idea is simple, and the work involved is mostly editing. For any .NET class or web service you want to use, you develop a set of wrapper functions exposing the methods and properties you need. The wrappers are in three parts: public interface, private implementation, and translation layer between the two. These parts are distributed in a particular way between .cpp and .h files, so that the latter can be included in unmanaged code while the former can call managed classes. There is a systematic naming convention, so you can look at a call and know which system function it wraps.

The client app for demonstrating the technique takes an ADO.NET connect string, connects to a data source, and retrieves schema data or query results into a grid. It uses a few System.Data.Oledb classes—Command, Connection, DataAdapter, and DataReader—wrapped and callable from the standard MFC dialog app.

Layers

  • Public interfaces (.h file). These specify the routines your app is allowed to call. The interfaces are in terms of friendly types (like CString), and the .h file can be included in your normal MFC app. Class names all start with “NL_” (for NetLib), followed by the name of the system class being wrapped. Each NL class has a member pointer to its implementation.
  • Private implementations (.cpp file). Each NL class has a corresponding “NLI_” (NetLib Implementation) class in the .cpp file. These classes convert arguments as necessary and call the managed system routines. Each NLI class has a garbage-collected pointer to a system class object.
  • Public implementations (.cpp file). The NL interfaces in the .h file just pass their arguments to the equivalent NLI functions.

Example

One of the classes you want to use is System::Data::OleDb::OleDBConnection. You need capabilities to establish a connection, obtain a command object, and access some schema information.

The public interface in NETLib.h exposes these functions in the form of an NL wrapper class:

class NL_OleDbConnection
{
public:
   NL_OleDbConnection();
   NL_OleDbConnection(NLI_OleDbConnection *p);
   NL_OleDbConnection(const CString& conn);
   virtual ~NL_OleDbConnection();

   CString             DataSource();
   CString             ConnectionString();
   void                Open();
   void                Close();
   NL_OleDbCommand*    CreateCommand();
   NL_DataTable*       GetSchema();

protected:
   friend class NLI_OleDbCommand;
   NLI_OleDbConnection *m_pi;
};

Each NL class has a default constructor, plus one that takes a pointer to the implementation, plus possible others for convenience (in this case, one for constructing from a string). This example has two read-only properties returning strings, Open/Close methods, plus two properties that return other objects as NL pointers.

Forward declarations are necessary for the compiler. There are many, so we collected them in a separate header file (class_defs.h), which has two lines per class:

class NL_OleDbConnection;
class NLI_OleDbConnection;

The private implementation in NETLib.cpp looks like this:

class NLI_OleDbConnection
{
public:
   NLI_OleDbConnection() : m_OleDbConnection(gcnew OleDbConnection) { }
   NLI_OleDbConnection(OleDbConnection^ g) : m_OleDbConnection(g) { }
   NLI_OleDbConnection(const CString& conn)
   : m_OleDbConnection(gcnew OleDbConnection(gcnew String(conn))) { }

   CString             DataSource()
      { return m_OleDbConnection->DataSource; }
   CString             ConnectionString()
      { return m_OleDbConnection->ConnectionString; }
   void                Open()
      { m_OleDbConnection->Open(); }
   void                Close()
      { m_OleDbConnection->Close(); }
   NL_OleDbCommand*    CreateCommand();
   NL_DataTable*       GetSchema();

   gcroot<OleDbConnection^> m_OleDbConnection;
};

The actual system object is the member m_OleDbConnection. Most NLI functions are simply inline calls to members or properties of that object. More complicated functions are implemented elsewhere in the .cpp file.

Connecting the two parts is a series of translation calls in NETLib.cpp:

NL_OleDbConnection::NL_OleDbConnection()
   : m_pi(new NLI_OleDbConnection) { }
NL_OleDbConnection::NL_OleDbConnection(NLI_OleDbConnection *p)
   : m_pi(p) { }
NL_OleDbConnection::NL_OleDbConnection(const CString& s)
   : m_pi(new NLI_OleDbConnection(s)) { }

NL_OleDbConnection::~NL_OleDbConnection()
   { delete m_pi; }
CString NL_OleDbConnection::DataSource()
   { return m_pi->DataSource(); }
CString NL_OleDbConnection::ConnectionString()
   { return m_pi->ConnectionString(); }
void NL_OleDbConnection::Open()
   { m_pi->Open(); }
void NL_OleDbConnection::Close()
   { m_pi->Close(); }
NL_OleDbCommand* NL_OleDbConnection::CreateCommand()
   { return m_pi->CreateCommand(); }
NL_DataTable* NL_OleDbConnection::GetSchema()
   { return m_pi->GetSchema(); }

where each NL routine calls the corresponding routine of the implementation object, m_pi.

Project

The included project is for VS2005, and requires the .NET Framework 2.0. Assuming it builds and works, what you get is a little data source browser, which is fairly handy but not intended to serve any real purpose. It demonstrates a few wrappers for a few classes, and should have enough code to show how you can go on to develop your own.

— Jim Dill

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read