ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


Effective Interop with Managed C++

by John Bush
06/23/2003

One of the new features of Visual Studio .NET 2003 is Windows Forms designer support for managed C++. Now you can write your Windows Forms applications in any of the "big three" .NET languages and get full support from the IDE.

You know what? I'm a long-time C++ programmer but, in all honesty, I don't know why Microsoft bothered. No one in his or her right mind would want to use managed C++ for new .NET application development. It's an odd hybrid language that still supports the familiar C++ syntax, but extends it with a bunch of proprietary keywords. Writing a .NET application in managed C++ requires extensive use of these new keywords, often resulting in a jumbled mess that C++ old-timers would barely recognize.

Despite all of that, don't write off managed C++ yet. If you can look past the superficial syntax issues, it offers unmatched power to developers who need legacy C++ applications to interoperate with .NET. The hybrid nature of managed C++ becomes a strength in these situations, because you're able mix your legacy C++ code with managed C++ in nearly any combination.

The flexibility of being able to mix managed and unmanaged code also introduces a host of new issues that aren't present in any other .NET language. In this article, I'll review some of these issues and present practical advice on how to deal with them.

Target Audience

The target audience for this article is developers who are interested in using managed C++ for interop. Ideally, you're a C++ developer with a working knowledge of .NET. I'm also going to assume that you're familiar with the basics of managed C++. If you have no previous experience with managed C++, an excellent prerequisite to this article is Sam Gentile's "Intro to Managed C++" series.

Mastering Visual Studio .NET

Related Reading

Mastering Visual Studio .NET
By Ian Griffiths, Jon Flanders, Chris Sells

Don't Break the Illusion

When using managed C++ to wrap unmanaged classes, the goal should be to make the managed wrapper class behave identically in all respects to a real managed class. Consumers of the wrapper class should have no idea that the class' underlying implementation is unmanaged code. As an example, consider how you might create a managed wrapper class for the following unmanaged C++ class' public interface:

class Customer
{
public:
  Customer();
  ~Customer();

  const std::wstring& GetName() const;
  void SetName(const std::wstring& name);

  double CalculateAccountValue() const;
};

Here's my first attempt at making it managed:

__gc class ManagedCustomer1
{
public:
  ManagedCustomer1() { m_pCustomer = new Customer(); }
  ~ManagedCustomer1() { delete m_pCustomer; }

  String* GetName()
  {
    return m_pCustomer->GetName().c_str();
  }

  void SetName(String* name)
  {
    m_pCustomer->SetName( ToStdString(name) );
  }

  double CalculateAccountValue(void)
  {
    return m_pCustomer->CalculateAccountValue();
  }

private:
  Customer* m_pCustomer;
};

This is a typical managed wrapper class. It uses an embedded Customer pointer and manages its lifetime via ManagedCustomer1's constructor and destructor (actually, that's the finalizer in managed C++). Beyond that, ManagedCustomer1 simply provides managed versions of each function on Customer. (By the way, the function ToStdString() is a helper function I used to convert a managed string to an unmanaged STL string. Its implementation is not relevant here, but is based on information available in Tomas Restrepo's excellent MC++ FAQ.)

While there's nothing technically wrong with the ManagedCustomer1 wrapper class, I made a mistake by not updating the interface. It's often a bad idea to clone a C++ class interface in your managed wrapper class, because in doing so, you completely miss out on .NET features like properties, indexers, and interfaces. These features are expected by the consumers of your object and will be missed if they are not present.

Another potential problem with ManagedClass1 is the lack of try/catch blocks. Unmanaged code can throw unmanaged exceptions, and if you let these propagate into managed code, not only will you break the illusion of your class being managed, the resulting managed SEHException probably won't be as informative as the original unmanaged one. A well-designed managed wrapper class should always catch known unmanaged exceptions and convert them to managed ones.

With these problems in mind, a better wrapper class implementation might be:

__gc class ManagedCustomer2
{
public:
  ManagedCustomer2() { m_pCustomer = new Customer(); }
  ~ManagedCustomer2() { delete m_pCustomer; }

  __property String* get_Name()
  {
    return m_pCustomer->GetName().c_str();
  }

  __property void set_Name(String* name)
  {
    m_pCustomer->SetName( ToStdString(name) );
  }

  double CalculateAccountValue(void)
  {
    double val = 0.0;

    try
    {
      val = m_pCustomer->CalculateAccountValue();
    }
    catch ( UnmanagedException& ue )
    {
      throw new ApplicationException( ue.Message().c_str() );
    }

    return val;
  }

private:
  Customer* m_pCustomer;
};

Ultimately, the goal is to present the illusion that the consumer is interacting with "real" managed objects. By doing this, not only will your unmanaged objects appear to be model .NET citizens, but it also makes it easier if you decide to later swap out your object's unmanaged implementation with a managed one.

Avoid Unnecessary Managed/Unmanaged Code Transitions

Minimizing the number of managed/unmanaged code transitions is crucial for good performance in any .NET application. Even C# and Visual Basic programmers aren't immune from this problem, as overuse of PInvoke or COM interop can turn an otherwise lightning-fast application into one that runs slower than molasses in winter.

While PInvoke and COM interop make it obvious where managed/unmanaged transitions occur in your code, it is not always straightforward to identify these transitions when using managed C++. In contrast to the explicit behavior of PInvoke and COM interop, the managed C++ compiler attempts to compile your code as managed, but quietly falls back to using unmanaged code as necessary. This behavior is key to the "It Just Works" (IJW) design philosophy of managed C++, and makes it possible to port legacy code to managed C++ quickly and easily. However, the downside is that it also makes it easy to lose track of all of the managed/unmanaged code transitions in your application.

Let's examine what happens if you compile a C++ function as managed C++ (i.e., using the /clr compiler option):

static std::wstring SayHello(const std::wstring& name)
{
  wchar_t buf[40];
  swprintf(buf, L"Hello %s!", name.c_str() );
  return buf;
}

(The purpose of this function is to explore what happens when it's compiled using managed C++. It certainly isn't going to win any programming excellence awards.)

Inspecting the compiled output of this code using ILDASM reveals that the SayHello() function compiles to managed code, as expected. However, internally, the function calls three other methods, none of which are managed:

Because each of these calls is to functionality that exists in an external STL or CRT library, managed C++ cannot generate managed code for these three functions. As a result, they remain unmanaged, causing an expensive managed/unmanaged transition (also called an "IJW thunk") to occur as each is executed.

To complicate matters further:

As you can see, it's not always easy to know in advance how many IJW thunks will be generated when using managed C++. Because of this, compiling legacy applications using managed C++ often does "just work," but can also take a large toll in performance.

So how do you reduce the number of these thunks? There are several techniques, but the easiest is, whenever possible, to leave your legacy code as native C++. At first this might seem counterintuitive, but when performing interop work, there's no reason to recompile legacy applications wholesale using managed C++. (And, as you've seen, this usually does more harm than good.) Instead, do the opposite -- create a thin layer of managed code that wraps access to the unmanaged portion. Not only does this greatly reduce the number of IJW thunks, but it leaves the bulk of your code unmanaged (don't forget that unmanaged code is still faster than managed).

Other ways to reduce the number of IJW thunks are:

Keep in mind that these tips are only for improving your application's performance with managed C++. If you're happy with your managed C++ application's performance, then none of this may be necessary. However, if your application could use a boost, reducing the number of managed/unmanaged code transitions can make a dramatic improvement in performance.

Conclusion

There is a lot more to managed C++ interop than can be covered in a single article. However, I hope you've learned about some of the unique issues that affect managed C++ interop work and have picked up a few practical techniques to help you deal with them.

John Bush


Return to ONDotnet.com

Copyright © 2009 O'Reilly Media, Inc.