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.
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.
|
Related Reading Mastering Visual Studio .NET |
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.
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:
c_str().swprintf().std::wstring's constructor for the function's return value.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:
Not every call to an external library results in an IJW thunk. Functions that are implemented inline in a header file may still be compiled to managed code.
Not all functions can even be compiled to managed code. Managed C++ always generates unmanaged code when it encounters certain constructs.
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:
Learn to take manual control over what code is generated as managed vs. unmanaged. The C++ compiler allows individual files in projects to be compiled as managed C++ using the /clr switch, and within those files, you can further limit managed code generation by using the #pragma managed and #pragma unmanaged statements.
Follow Microsoft's recommendation of using chunky, not chatty, calls.
Use ILDASM to examine your managed output and find out where thunks are taking place. Armed with this knowledge, you're often able to identify other opportunities for reducing IJW thunks.
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.
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.
Return to ONDotnet.com
Copyright © 2009 O'Reilly Media, Inc.