Freigeben über


The Bridge - a pattern to leverage Managed C# code from an Unmanaged C++ code base

Building bridges from unmanaged C++ to managed C# using the CLI features

[note: attached code contains the full example solution implementing this pattern]

If you write or maintain C++ applications, you may want to leverage your (or your co-worker's) C# components - these could be visual components or logical components. You can leverage them directly, without COM enabling them or using COM wrappers! You can actually integrate them straight into your C++ application and leverage the managed assemblies as you would a project in your Visual Studio C++ solution. You can debug and step straight across the Unmanaged C++ / Managed C++ / Managed C# (or other CLR languages)

You can realize a variety of benefits:

  1. You don't have to attach to multiple debugee's and jump through hoops to inspect and comprehend the state of your application and its dependencies at a given moment in real time.
  2. You can debug straight through from the unmanaged WinMain down into your C# component, across each of the layers, without aggravation.
  3. Your deployments will be simplified (no need to register COM components)
  4. Your friends and coworkers will think you're amazingly brilliant

Its actually pretty straight foward to do this, and the results are pretty clean. I'll present some Patterns you can follow to help you achieve this, and walk through a sample app to demonstrate an implementation of the pattern:

The sample consists of:

  • A native C++ windows console app project with a WinMain()
  • An .h header file and a .cpp implementation file that implement a C++ to C# bridging pattern that bridges the gap from unmanaged C++ code to managed C# code
  • A simple C# component containing methods and properties that we want to invoke from our native C++ console application

The Complete Solution

Generate this solution yourself in Visual Studio 2010 - invoke the File / New / Project... menu choice. In the New Project dialog, navigate to Other Languages node and click on "Visual C++ " to see the list of project templates.  From this templates list, click on "Win32 Console Application". Give it a cool name like CliTest or something and put it somewhere useful in your regular PoC project location.  I named mine "CliExample"

After Visual Studio gyrates for a bit you'll end up with a new solution containing the C++ project you just added.  You'll see the initial project displayed in the Solution Explorer view - if that's not appearing for you, go to the View menu and click on "Solution Explorer".

We want to add the C# managed component now, so right-click on the 'Solution' at the top of the Solution Explorer view and choose "Add -> New Project... ". You'll see the "New Project" window once again, and this time just make sure "Visual C#" is selected in the 'Installed Templates' list and then click on 'Class Library' in the list of project templates. Once again give it a cool name like 'CSharpy' or whatever.

For me, I named my projects 'Cli Example' and 'ManagedComponent'... Here's what it all looks like if you get it right.

The CliExample project is a simple C++ Windows Console Application project. The only files I've added to the template solution generated by Visual Studio 2010 are the ManagedBridge.h and ManagedBridge.cpp files.

  1. The ManagedBridge.h file includes a forward reference to the managed Bridge class declared in the ManagedBridge.cpp file. This is necessary because Bridge is the boundary C++ managed class that bridges the gap between Unmanaged and Managed C++, and provides the gateway to the Managed C# (or CLR) components we wish to leverage.

  2. ManagedBridge.cpp has a special compile setting: Common Language RunTime Support is set to /clr.  The rest of the project is a normal 'cplusplus' project

  3. The CliExample project itself is configured for Mixed debugging – this allows you to step across the boundary and set breakpoints on both sides

  4. The ClIExample project also contains a reference to the managed component – this is done via the Add Reference button at the bottom of the following dialog


The ManagedComponent project contains our (super simple) C# component. The relevant parts of this pattern are best discovered via interactive debugging and stepping through the code from the outside C++ console app all the way into the ManagedComponent class. There's subleties encountered in wiring the various classes and layers together to construct the bridge that are hard to descibe but easy to see in the code.  Here's a snapshot of the final teasingly tiny managed method that's invoked via C++ pointers and the bridge across the boundaries...


CliExample.cpp contains the main entry point for the console application, and its about as simple as it can get. But lets look at it to see what's in there.

 // CliExample.cpp : Defines the entry point for the console application.
 #include "stdafx.h"
#include "ManagedBridge.h"
 
using namespace ManagedBridge;
 
Bridge* ptr = NULL;
 
int _tmain(int argc, _TCHAR* argv[])
{
   ptr = new Bridge();
 
   ptr->PrintMessage("Call 1 to managed component through CLI boundary. Value should display as 0");
   ptr->SetValue(15);
   ptr->PrintMessage("Call 2 to managed component through CLI boundary. Value should display as 15.");
 
    delete ptr;
  return 0;
} 

This code is pretty straight forward: we #include the ManagedBridge.h file and declare a namespace for convenience. Then we allocate the mysterious Bridge class, make some normal looking C++ calls and then, like every good citizen, deallocate the Bridge object before terminating.  Hmmm, no rocket science here. 

But what happens behind the scenes is a bit more interesting! Within the Bridge class we allocate the MngdAdapter class (gcnew MngdAdapter) on the CLRs managed memory pool. In C++ terms this is a pointer we don't really have a reference to. Its not ours! This is a garbage collected memory footprint that we've allocated and asked for a handle to. Since it's an auto_gcroot we don't have to treat it specially. Its scope and lifetime are managed by the CLR's managed memory pool.

The instantiation of the MngdAdapter results in the allocation of the ManagedComponent instance (which is our C# component!). The adapter lives only to enable proxied calls from the Bridge to the ManagedComponent's API: The PrintMessage, SetCount, and GetCount calls.. 

So what do we see when we build and run this application?

 Call 1 to managed component through CLI boundary. Value should display as 0: 
Value: 0
 Call 2 to managed component through CLI boundary. Value should display as 15: 
Value 15
 

Let's dig a bit deeper

Earlier we referenced the ManagedBridge.h file included in CliExample.cpp.  Let's take a look inside it now:

 #pragma once
 
namespace ManagedBridge {
   
    // anonymous pre-declaration of the adapter class that we'll instantiate
 // in the c++ code implementation of our class. It has to be anonymously pre-declared
    // as it's a managed class 
 class MngdAdapter;
 
  class Bridge
 {
   private:
      MngdAdapter* adapter;
 
 public:
       Bridge();       
        ~Bridge();
 
     void Bridge::SetValue(int value);
     int Bridge::GetValue();
 
     void PrintMessage(const char* sMsg);
   };
};

A couple of things of note here: The MngdAdapter reference is declared anonymously with a forward reference (class MngdAdapter;). This ManagedBridge.h file is included in the native C++ ManagedBridge.cpp implementation. As a result of the compiler settings for the CliExample.cpp file, it is compiled natively. This is so important that you should spend time making sure you get what I'm saying - look at the code and examine the modified compiler settings.  Comment on this post if you think I can be clearer! 

Remember, our intention is to build a bridge from the unmanaged to the managed code, so the Bridge class is fully defined giving us an API we can use in the unmanaged native code, with the assumption that it will bridge the gap for us.

As you'll soon see we define the MngdAdapter class in the associated implementation file (fully declared obnoxiously in the ManagedBridge.cpp).  Thus we just use the forward reference and ask the compiler to trust us that it will be defined before linking commences. Both of these classes end up proxying calls into the managed components, though you could look at either like a facade in a richer example as there's no necessity that the bridge and or coupler proxy calls to a single managed component.

 // ManagedBridge.cpp implementation file with definition of 'MngdAdapter' class mysteriously referenced
// in the ManagedBridge.h file with a forward reference
 #include "stdafx.h"
 
#include "ManagedBridge.h"
 
 
#include <msclr/auto_gcroot.h>
using msclr::auto_gcroot;
 
#using <System.dll>
using namespace System;
using namespace ManagedComponent;
 
namespace ManagedBridge {
 
 ///
  // adapter needed to hide clr stuff from our main .h file so client app 
    // code can compile cleanly; Provides adapter to the CSharp component
    // that we wish to interact with
 ///
  public class MngdAdapter
  {
   private:
      auto_gcroot<MngdComponent^> adapter;
 
    public:
       MngdAdapter() : adapter(gcnew MngdComponent()) {}
 
        int GetValue()
       {
           return adapter->Value;
        }
 
      void SetValue(int value)
      {
           adapter->Value = value;
        }
 
      void PrintMessage(const char* sMsg)
        {
           adapter->PrintMessage(gcnew String(sMsg));
        }
   };
  
    ///
  // Bridges Unmanaged to managed code - uses a adapter to thunk the gap
   ///
  Bridge::Bridge() 
  {
       adapter = new MngdAdapter();
   }
       
    Bridge::~Bridge() 
 {
       delete adapter;
  }
 
  int Bridge::GetValue()
   {
       return adapter->GetValue();
   }
 
  void Bridge::SetValue(int value)
  {
       adapter->SetValue(value);
    }
 
  void Bridge::PrintMessage(const char* sMsg)
    {
       adapter->PrintMessage(sMsg);
 }
 }

As you can see, the calls flow like this:

Console App (Unamaged : CliExample.cpp) -> Bridge (Unmanaged -> Managed : C++ ManagedBridge.h) -> MngdAdapter (Managed C++ : ManagedBridge.cpp) -> ManagedComponent (Managed C# : ManagedComponent.cs)

https://blogs.msdn.com/cfs-file.ashx/__key/communityserver-blogs-components-weblogfiles/00-00-01-39-62/8836.CliExample.zip