Welcome back! It has been a very long time since the last article in this three-part series. The second article focused on the ability to mix managed and unmanaged code in the same module, which is an ability that is unique to Managed C++; no other CLR language possesses this capability. In this installment, I will take this one step further. I will show you how to take existing legacy unmanaged C++ code, and make it usable from any CLR language in the managed world. This is accomplished via managed wrappers, which act as a managed proxy for the unmanaged C++, thus allowing that existing code to be used from C#, VB.NET, or any other .NET language. I don't have to tell you how valuable this ability is to businesses that have lots of existing C++ code that they wish to use from C# or VB.NET.
A quick note: this article, and this series, assume that the reader is familiar with the basics of the .NET Framework, including the CLR, and has worked with "managed" languages such as C# and VB.NET. It is also strongly assumed that the reader is also an experienced C++ programmer. It should be noted that this is neither an introductory C++, or .NET article and I cannot provide support for anything beyond this article's contents, including all basic C++ questions.
|
Related Reading
.NET Framework Essentials |
Let's review something from the second article: unmanaged code, at least on the Microsoft platform, is everything you have been programming for years, before .NET. Unmanaged, or "native" code, includes VB6, COM, Win32, native C++, and so forth. It is code that predated .NET, and therefore has absolutely no knowledge of .NET and cannot directly make use of any managed facilities. Our interest is in taking unmanaged C++ code and making it available for use from any CLR language. The last article showed how to mix unmanaged and managed C++ in the same C++ file, but that was just a step in the ultimate direction I am going in here: making that legacy C++ code accessible directly from C# and VB.NET.
Although the benefits of the CLR are absolutely compelling, there are literally billions of lines of native C++ code that work perfectly well and will stay in operation for years to come. Indeed, even the Win32 API itself and the shell functions are all unmanaged code, and will be for years to come (until the release of Longhorn). Much of this code works perfectly well and represents millions of dollars of investment for many companies. As most projects move to .NET, particularly in the areas of web services, ASP.NET web applications, and Windows forms, there is a large desire for businesses to be able to reuse their existing non-UI C++ code from new .NET applications. Typically, in my experience, companies starting with .NET want to utilize the above three technologies to leverage the immense RAD benefits of creating managed user interfaces in a fraction of the time right away, but still "call" or utilize their existing legacy C++ code that may contain significant value. In addition, for many shops facing shrinking IT staffs and budgets, as well as large amounts of native code, the issue of new technology adoption is large, and prohibits whole-scale rewriting.
In the last article, I talked, in depth, about something known as It Just
Works (IJW)! and noted that through the use of the /clr switch and IJW, that it
allows you to take your native C++ code and (mostly) "make it" managed. The
output of code compiled with the /clr switch is MSIL.
Using that switch and the IJW technology, we can start to look at the simplest wrapper example.
Suppose we have the following unmanaged C++ class:
class Foo
{
public:
// constructor
Foo(void) {}
// destructor
~Foo(void) {}
// some method
void DoSomeFoo(){}
};
Knowing what we do about MC++ and IJW from the last two articles, what can we
do to this code to make it accessible from managed clients such as C# and VB.NET?
Well, we already know that we can mix unmanaged and managed C++ through IJW and
the /clr compiler switch. With that knowledge, and the knowledge of design
patterns, we can create some sort of proxy object or wrapper that is managed,
yet through IJW, communicates with the unmanaged code on our behalf. Our managed
wrapper would do this delegation. We would contain a pointer to the unmanaged
class, in our case Foo, and wrap it in a ManagedFoo.
Microsoft has outlined a series of steps to produce a managed wrapper in their Migration Guide, which I will distill in this article to the following:
Applying those steps to our simple example produces the following managed wrapper:
__gc class MFoo
{
private:
Foo * _foo;
public:
// constructor
MFoo() { _foo = new Foo();}
// destructor
~MFoo() { delete _foo; }
// method
void ManagedDoSomeFoo() { _foo->DoSomeFoo(); }
};
This is all that needs to be done to create a simple managed wrapper, but obviously there are quite a few details that come into the picture for code that is not as trivial as our example. The full details are provided in the Managed Extensions for C++ Migration Guide that Microsoft provides with Visual Studio .NET. As I cannot hope to cover all such cases and details in this short article, I will present a condensed and simplified explanation.
One thing I should note is that you don't have to wrap every single member function of a given unmanaged class. Hopefully, all of my readers are familiar with refactoring techniques, which Martin Fowler describes in his most excellent book and are some of the main tenets of Extreme Programming. Well, creating managed wrappers affords you the opportunity to refactor your code in a way that makes sense to your managed clients. There are many things that one used to have to do with native C++ that no longer need to be done in the world of the CLR. In the process of designing managed wrappers, I encourage you to refactor and only expose methods that you are going to need and use from managed clients. If you don't need something, then don't wrap it. Also, C++ classes typically contain private and helper methods. Since private methods are not accessible to other unmanaged classes, they should not be accessible by managed clients, either. Helper functions fall into a similar category and typically should not be exposed.
In order to provide a more substantial example to illustrate the process of writing managed wrappers, I have written a simple C++ class that implements a simple linked list. The list is comprised of items:
class Item
{
public:
friend class UnmanagedLinkedList;
// constructors and destructors
Item(int value, Item *ItemToLinkTo = 0);
virtual ~Item(void) { delete m_next; }
int Value() const { return m_iValue; }
void Next(Item* link) { m_next = link; }
Item* Next() { return m_next; }
private:
int m_iValue;
Item* m_next;
};
inline
Item::Item( int value, Item *item ) : m_iValue( value )
{
if ( !item )
m_next = 0;
else
{
m_next = item->m_next;
item->m_next = this;
}
}
The class UnmanagedLinkedList looks like the following:
#pragma once
#pragma unmanaged
#include "Item.h"
#define NULL 0
class UnmanagedLinkedList
{
public:
// Constructors
UnmanagedLinkedList()
: m_first(NULL), m_last(NULL), m_current(0), m_size(0) {}
// Copy constructor
UnmanagedLinkedList( const UnmanagedLinkedList &rhs ) :
m_first( 0 ), m_last( 0 ),
m_current( 0 )
{
insert_all( rhs );
}
UnmanagedLinkedList&
operator=( const UnmanagedLinkedList &rhs )
{
remove_all(); insert_all( rhs );
return *this;
}
virtual ~UnmanagedLinkedList(void);
// Accessors
Item* Front() const { return m_first; }
// int Size() { return m_size; }
// Member insert methods
void insert( Item *ptr, int value );
void insert_all( const UnmanagedLinkedList &rhs );
void insert_end( int value );
void InsertFront(int value);
// Member remove methods
int remove( int value );
void remove_front();
void remove_all();
int isEmpty();
void DisplayList();
private:
Item* m_first;
Item* m_last;
Item* m_current;
Item* GetNewItem(int value);
int m_size;
};
|
In this section of the article, I will show how to build the managed class that
will wrap our linked list, so that we can call the code from C#. First,
create a new Managed C++ class library project in VS.NET 2003 called
ManagedList, copy the files Item.h, Item.cpp, UnmanagedLinkedList.h,
and UnmanagedLinkedList.cpp into the project folder, and then add them to the
project.
The next thing to do is define a managed class called ManagedList, and declare
a data member whose type is a pointer to the class being wrapped; that is, an
UnmanagedLinkedList*. The code, so far, from ManagedList.h looks like this:
#pragma once
#include "UnmanagedLinkedList.h"
#pragma managed
#using <mscorlib.dll>
using namespace System;
namespace ManagedList
{
public __gc class ManagedList
{
public:
private:
UnmanagedLinkedList __nogc* m_pUnmanagedList;
};
}
One piece of advice I would like to give you in regards to constructors is that you don't have to wrap every constructor of your unmanaged class. Again, writing managed wrappers affords you the ability to refactor and simplify your code. In addition, you should consider if the constructor in question will make sense in the managed environment. In other words, if your constructor is initializing types that you will no longer be using in an managed environment, then the constructor is not needed. These kind of decisions must be made on a case-by-case basis.
The default constructor is trivial and simply instantiates the class being wrapped:
MList::MList()
{
m_pUnmanagedList = new UnmanagedLinkedList();
}
The copy constructor, on the other hand, brings up some interesting issues. In
native C++, we often find it necessary to have a user-defined copy constructor
and a copy assignment issue, to perform true "deep-copy" semantics and not just
member-wise copying. The way to do this in the .NET Framework, and thus in Managed
C++, is to implement the IClonable interface in our MList class:
public __gc class MList : public ICloneable
{
public:
MList();
virtual Object* Clone()
{
// Create new instance of the managed list
MList* mList = new MList();
// calls unmanaged copy constructor
*(mList->m_pUnmanagedList) = *m_pUnmanagedList;
// deep copy any other members from this-> to mList->
return mList;
}
private:
UnmanagedLinkedList __nogc* m_pUnmanagedList;
};
Wrapping destructors is trivial: have the destructor in the managed wrapper call the destructor in the unmanaged class:
~MList() { m_pUnmanagedList->~UnmanagedLinkedList(); }
I hope right now you are asking yourself, "But Sam, .NET is all about love. It is supposed to take care of memory for me with that garbage collection thingie." While that is certainly true for managed code, the CLR and the garbage collector have no idea about unmanaged resources. Our class holds such a reference to the unmanaged class, and therefore we must call the destructor in the unmanaged class explicitly, so that the underlying unmanaged object is destroyed.
Managed C++ does not allow overloaded operator=(), just as it does not allow
copy constructors. The workaround is to define a method named Assign() and call
it explicitly:
virtual MList* Assign(MList* otherOne)
{
if (otherOne != this)
{
*m_pUnmanagedList = *(otherOne->m_pUnmanagedList);
// Deep copy other members
}
return this;
}
Believe it or not, we're done with the hard part! All that is left is to wrap
the rest of the member functions of the class, if it makes sense to wrap
them.. The good news is that almost all of the time, member functions
may simply be wrapped by delegating the implementation of the function to the
unmanaged class (except for Accessor functions, which I will discuss
separately). I also like to rename the managed versions to fit with the Camel
and Pascal casing standards of .NET. For a refresher, here are the remaining
member functions declared in the header file:
// Member insert methods
void insert( Item *ptr, int value );
void insert_all( const UnmanagedLinkedList &rhs );
void insert_end( int value );
void InsertFront(int value);
// Member remove methods
int remove( int value );
void remove_front();
void remove_all();
int isEmpty();
void DisplayList();
The completed implementation looks like the following:
void MList::Insert( Item *ptr, int value )
{
m_pUnmanagedList->insert(ptr, value);
}
void MList::InsertAll(const UnmanagedLinkedList &rhs)
{
m_pUnmanagedList->insert_all(rhs);
}
void MList::InsertEnd(int value)
{
m_pUnmanagedList->insert_end(value);
}
void MList::InsertFront(int value)
{
m_pUnmanagedList->InsertFront(value);
}
int MList::Remove( int value )
{
return(m_pUnmanagedList->remove(value));
}
void MList::RemoveFront()
{
m_pUnmanagedList->remove_front();
}
void MList::RemoveAll()
{
m_pUnmanagedList->remove_all();
}
void MList::DisplayList()
{
m_pUnmanagedList->DisplayList();
}
The only things left are the accessor functions. There's nothing much to discuss here, other than to note that MC++ has full support for properties, which formalizes the notion of C++ member accessor functions through them. We only have two to consider. Recall these from the unmanaged code:
// Accessors
Item* Front() const { return m_first; }
int Size() { return m_size; }
All we have to do with these is make them full properties in the MC++ header file:
// properties
__property Item* get_Front();
__property int get_Size();
All that remains now is to test our completed wrapper! Open up VS.NET 2003
and create a new Visual C# Console Project named TestManagedList. There is no
need to change the default name of Class1, but you do need to add a reference to
the DLL project we just built. I assume you know how to bring up the Add
Reference dialog, so simply choose the Projects tab, browse to the
ManagedList\debug directory, and select ManagedList.dll.
Upon adding the reference, we can add the line of syntactical sugar that allows us to reference names in the namespace with "shortcut" syntax:
using ManagedList;
We are now ready to rock and roll, and test our managed wrapper. The first
thing to do is "new up" an instance of the wrapper (ManagedList):
MList theList = new MList();
The next thing to do is to call the methods to insert values at the front and back of the list, and then display it:
// Insert at the front and back and display the results
for (int i=0; i < 10; ++i)
{
theList.InsertFront(i);
theList.InsertEnd(i);
}
theList.DisplayList();
The next thing to test out is the property:
int listSize = theList.Size;
Console.WriteLine("The size of the managed list is {0} items", listSize.ToString());
Now it is time to test the operation of the Clone() method, to act as C++'s copy constructor:
MList newList = (MList)theList.Clone();
listSize = newList.Size;
Console.WriteLine("Size of the clone is {0} items",
listSize.ToString());
Console.WriteLine("...and the members are...");
newList.DisplayList();
// test assignment operator
newList.InsertFront(42);
newList.InsertFront(420);
listSize = newList.Size;
Console.WriteLine("Size of the clone is now {0} items",
listSize.ToString());
Console.WriteLine("and the members are:");
newList.DisplayList();
// now assign to the original to test the assignment operator
theList.Assign(newList);
listSize = theList.Size;
Console.WriteLine("Size of the original is now {0} items",
listSize.ToString());
Console.WriteLine("and the members are:");
newList.DisplayList();
We have concluded our whirlwind three-part look at Managed C++ and its place in the canon of CLR languages. In this article, we used an example piece of unmanaged C++ code, and wrote a managed wrapper for it, step by step. As you saw, this process is not rocket science, but requires close attention to some details to get right. Hopefully, with this material, you may get started writing wrappers for your legacy C++ code so you may use it from C# and VB.NET.
Sam Gentile is a well-known .NET consultant and is currently working with a large firm, using Visual C++ .NET 2003 to develop both unmanaged and managed C++ applications.
Return to ONDotnet.com
Copyright © 2009 O'Reilly Media, Inc.