Introduction
What is the best way to integrate a legacy system written in native C++ with a .NET application? The first thing that comes to our mind is the COM. Yeah we can certainly expose methods from native C++ through COM. But dealing with COM is not an easy task. There is always one problem, Memory leaks. It’s a pain to find and fix these leaks. Through COM you can just expose a set of methods, so where is OOA? Is there a way to group these methods as a class? I was just exploring in this direction.
So, if C++ has to interact with .NET languages using COM, then what is the purpose of Managed C++ or C++ /CLI? There is a way to build a managed library which internally wraps some native C++ code. So for the outer world, it looks like a managed library, which internally wraps up the unmanaged code.
The Problem Statement
How to create a VC++ library which can be consumed by all .NET languages and also by all legacy languages like Native C++, Visual Basic 6, Visual FoxPro etc.
The Solution
There are two known solutions to the above problem:
- Expose the required methods through COM/COM+. This is the most widely used method for years.
- Write a managed C++/CLI wrapper for the native C++ library. Yes, a managed library is easily accessible to all .NET languages, but legacy languages? To expose it to legacy languages, expose the methods through COM. Even though exposing the methods through COM is a roundabout way of doing it, I assume that the library is going to be used more in .NET languages, and it helps in maintainability.
We'll discuss the second method in this article.
The Benefits
- Dealing with COM in C++ is really tedious.
- Memory leaks can be minimized, as we take full advantage of the CLR.
- COM is usually slower and hard to maintain.
- Methods can be grouped as a class, so we can even wrap native C code with object oriented concepts.
The First Step
So where do I start? I have a sample native C++ application. I want to wrap it in a managed class. The major hurdle we have is dealing with parameters. The operation goes as follows.
- Get the parameters using Managed Data types
[System::String^, System::UInt32, etc]
- Marshall the parameters to Unmanaged data types.
- Call the unmanaged methods using these unmanaged parameters.
- Marshall the return values to Managed data type.
- Return Managed parameters.
I know it can get tedious, but if you can take this pain you have a lot to gain.
Using the Code
Let us see a simple sample application in C++/CLI, which wraps a few native C++ methods. The class definition looks as below:
public ref class ManagedWrapperClass
{
private:
void MarshalString ( System::String ^ s, std::string& os );
public:
int ManagedAdd(int num1, int num2);
System::String^ ManagedConcat(System::String^ string1,System::String^ string2);
void ManagedCompareConcat(System::String^ string1, System::String^ string2,
[System::Runtime::InteropServices::Out]System::String^ %concatString,
[System::Runtime::InteropServices::Out] System::Int32 %result);
};
public class unmanagedClass
{
public:
unmanagedClass();
int UnManagedMethodAddNum(int num1, int num2);
std::string UnManagedMethodConcatString
(std::string string1,std::string string2);
void UnManagedMethodCompareConcat(std::string string1,
std::string string2, std::string* concatString, int *result);
};
Here I have defined a class in C++/CLI, which exposes three methods. The first method ManagedAdd()
is relatively simple which just adds two integers and returns the sum. If you look at the second method (ManagedConcat()
), the parameters are System::String^
which is a .NET compatible data type. It requires some marshalling. The third method ManagedCompareConcat()
wraps a method which has some OUT
parameters, which is basically a pointer variable in C++.
The method definition is as follows:
int unmanagedClass::UnManagedMethodAddNum(int num1, int num2)
{
return num1+ num2;
}
std::string unmanagedClass::UnManagedMethodConcatString
(std::string string1,std::string string2)
{
return string1.append(string2);
}
void unmanagedClass::UnManagedMethodCompareConcat(std::string string1,
std::string string2, std::string * concatString, int *result)
{
std::string string3;
string3.append(string1);
string3.append(string2);
concatString->assign(string3);
int cmp = strcmp(string1.c_str(),string2.c_str());
*result = cmp;
}
The wrapper methods are implemented as below:
int ManagedWrapperClass::ManagedAdd(int num1,int num2)
{
unmanagedClass myClass ;
return myClass.UnManagedMethodAddNum(num1,num2);
}
System::String^ ManagedWrapperClass::ManagedConcat
(System::String ^string1, System::String ^string2)
{
std::string unmanagedString1, unmanagedString2;
MarshalString(string1,unmanagedString1);
MarshalString(string2,unmanagedString2);
unmanagedClass myClass;
std::string resultString =
myClass.UnManagedMethodConcatString(unmanagedString1,unmanagedString2);
System::String^ returnString = gcnew System::String(resultString.c_str());
return returnString;
}
void ManagedWrapperClass::ManagedCompareConcat(System::String^
string1, System::String^ string2,
[System::Runtime::InteropServices::Out]System::String^ %concatString,
[System::Runtime::InteropServices::Out] System::Int32 %result)
{
std::string unmanagedString1, unmanagedString2;
MarshalString(string1,unmanagedString1);
MarshalString(string2,unmanagedString2);
unmanagedClass myClass;
std::string resultString;
int resultInt;
myClass.UnManagedMethodCompareConcat
(unmanagedString1,unmanagedString2,&resultString,&resultInt);
concatString = gcnew System::String(resultString.c_str());
result = resultInt;
}
void ManagedWrapperClass::MarshalString ( System::String ^ s, std::string& os )
{
const char* chars =
(const char*)(Marshal::StringToHGlobalAnsi(s)).ToPointer();
os = chars;
Marshal::FreeHGlobal( System::IntPtr((void*)chars));
}
The major hurdle when we do a wrapping will be parameter marshalling. But it’s not hard to find these methods to convert from native data types to .NET compatible data types.
The Second Step
With the above implementation, I'm able to generate a DLL which can be consumed across all .NET languages. How do I use the same library in other legacy languages like native C++, Visual Basic 6 or Visual FoxPro? Now the problem is simple. How to consume a managed library in unmanaged code. For this, the best method is expose the methods through COM. You can ask me, why I want a Managed wrapper, to access native C++ application in native C++ application. Very true, we have easier ways of consuming an unmanaged library in an unmanaged C++ application. But, that forces us to have two different libraries or DLLs. One is a managed one; the other is an unmanaged one for doing the same task. Is there a way to have a single library which works across? Yeah there is a way. The best and easiest way to call a managed method in unmanaged code is COM. So here we'll see how to add the COM stuff into this library. Exposing the methods to COM is quite simple. Only addition is an interface, which is required for exposing the methods. Now the class definition looks like follows:
[ComVisible(true)]
[Guid("1DB44778-857F-47d1-9D4A-D90F109EFF35")]
public interface class IManagedWrapperClass
{
int ManagedAdd(int num1, int num2);
System::String^ ManagedConcat(System::String^ string1,System::String^ string2);
void ManagedCompareConcat(System::String^ string1,
System::String^ string2,
[System::Runtime::InteropServices::Out]System::String^ %concatString,
[System::Runtime::InteropServices::Out] System::Int32 %result);
};
[ComVisible(true)]
[ClassInterface(ClassInterfaceType::None)]
public ref class ManagedWrapperClass : public IManagedWrapperClass
{
private:
void MarshalString ( System::String ^ s, std::string& os );
public:
virtual int ManagedAdd(int num1, int num2);
virtual System::String^ ManagedConcat
(System::String^ string1,System::String^ string2);
virtual void ManagedCompareConcat(System::String^ string1,
System::String^ string2,
[System::Runtime::InteropServices::Out]System::String^ %concatString,
[System::Runtime::InteropServices::Out] System::Int32 %result);
};
public class unmanagedClass
{
public:
unmanagedClass();
int UnManagedMethodAddNum(int num1, int num2);
std::string UnManagedMethodConcatString(std::string string1,std::string string2);
void UnManagedMethodCompareConcat(std::string string1,
std::string string2, std::string* concatString, int *result);
};
Now we have a library, which can be consumed by all .NET languages as well as all legacy languages which support COM.
Consuming this Library
Consuming this library in .NET languages is quite simple. Add this DLL as a reference to the project and you are ready to go. But consuming in legacy application will take a bit of work. To consume it in native C++, we have to create the TLB file using the command:
RegAsm /tlb:<filename.tlb> "filename.dll"
Or using the TlbExp
command.
You can use this TLB file in your C++ application using the #import
directive.
Final Word
There are lots of libraries, with complex low level operations like image processing, networking, etc. still in native C++. Hopefully this method helps us to wrap those libraries as .NET data types to get a seamless access.