Introduction
This article shows how you can make your existing C++ code available to the .NET environment.
Background
Recently, I had to make my existing C++ code available for use in the .NET environment. My first thought was to convert all existing C++ code to C#. Undoubtedly, the rewrite would have been a major task and I just did not have that kind of time. An alternative was to use pInvoke mechanism, that would be ok except it would have been mostly C style code without much object-oriented programming. Finally, I decided to use C++/CLI as a bridge between my native C++ code and C# (or other .NET languages) clients.
Using the Code
The demo code consists of four simple projects:
- A C++ Windows DLL library (the existing code)
- A C++ console application
- A C++/CLI class library that wraps the C++ Windows DLL
- A C# console application
The code looks pretty straightforward but did present some obstacles for me such as dealing with different data types across languages; having to supply a callback function from .NET to C++ etc. The code is Unicode compliant.
Part One - cgiSolverBlaze
The part is a pure C++ Windows DLL. It includes one input data structure, a solution interface and implementation. In reality, this would be your existing working C++ code base, potentially very complicated and certainly most valuable. The following is the simple data structure for input. Notice the use of Unicode compliant data types (TCHAR) and functions (_tcscpy instead of strcpy).
const int LABEL_SIZE = 128;
struct CGISOLVERBLAZE_API cgiMaterial{
cgiMaterial(int id = -1){
iId = id;
_tcscpy(szLabel, _T("Default"));
fE = 29e6; }
void setId(int nIdentidy){
iId = nIdentidy;
}
void setProperties(const TCHAR* szMaterialLabel, double fYoungsModulus) {
_tcscpy(szLabel, szMaterialLabel);
fE = fYoungsModulus;
}
int iId;
TCHAR szLabel[LABEL_SIZE];
double fE;
};
Next is the solution interface. It is important to keep a clean interface for users of your library. Notice the callback function which will be passed from C++ or C# client programs.
typedef void (* fnLISTMSG)(LPCTSTR szMsg);
struct CGISOLVERBLAZE_API cgiIStructure {
virtual void setListMessageFunction(fnLISTMSG fnListMsg)=0;
virtual void setMaterials(const std::vector <cgiMaterial > & vMat)=0;
virtual void getMaterials(std::vector <cgiMaterial > & vMat)const=0;
virtual bool runAnalysis()=0;
};
CGISOLVERBLAZE_API cgiIStructure* CreateStructure();
The solution implementation sets the callback function, a vector of input data. The function runAnalysis() could be a lengthy computation routine.
class CGISOLVERBLAZE_API CcgiStruct : public cgiIStructure
{
public:
CcgiStruct();
~CcgiStruct();
public:
virtual void setListMessageFunction(fnLISTMSG fnListMsg);
virtual void setMaterials(const std::vector < cgiMaterial > & vMat);
virtual void getMaterials(std::vector < cgiMaterial > & vMat)const;
virtual bool runAnalysis();
protected:
fnLISTMSG m_fnListMsg;
std::vector < cgiMaterial > m_vMat;
};
cgiIStructure* CreateStructure(){
return new CcgiStruct;
}
CcgiStruct::CcgiStruct() {
m_fnListMsg = 0;
}
CcgiStruct::~CcgiStruct() {
m_fnListMsg = 0;
}
void CcgiStruct::setListMessageFunction(fnLISTMSG fnListMsg){
m_fnListMsg = fnListMsg;
}
void CcgiStruct::setMaterials(const std::vector < cgiMaterial > & vMat){
m_vMat = vMat;
}
void CcgiStruct::getMaterials(std::vector < cgiMaterial > & vMat)const{
vMat = m_vMat;
}
bool CcgiStruct::runAnalysis(){
m_fnListMsg(_T("I am about to start computing"));
for(int i = 0; i < 3; i++) {
m_fnListMsg(_T("..."));
Sleep(1000);
}
m_fnListMsg(_T("I am done with computing.\nHope you enjoyed it!\n\n"));
return true;
}
Part Two - cgiSolverBlazeTest
This part is a C++ console application that uses the C++ DLL above. It is listed here for the sake of completeness. The callback function is implemented in the console application and passed to the C++ DLL. Depending on the use of character set, we use wcout or cout to output string to console window.
#ifdef _UNICODE
#define COUT wcout
#else
#define COUT cout
#endif
static void ListMsg(LPCTSTR sz) {
COUT << sz << endl;
}
int main() {
COUT << _T("Running from native C++ client") << endl;
cgiIStructure* pStructure = CreateStructure();
pStructure->setListMessageFunction(ListMsg);
vector < cgiMaterial> vMat;
cgiMaterial mat;
mat.setId(1); mat.setProperties(_T("Default"), 29000); vMat.push_back(mat);
pStructure->setMaterials(vMat);
bool bRun = pStructure->runAnalysis();
return 0;
}
Part Three - cgiSolverBlazeCli
The part is a C++/CLI class library. It serves as a bridge between the native C++ DLL and .NET client. It includes a input data class, a solution class interface and its implementation. The input data class corresponds to the input structure in the C++ DLL.
public ref class cgiMaterialCli{
public:
cgiMaterialCli(){;}
void setId(int _iId){
iId = _iId;
}
void setProperties(String^ _szLabel, double _fE){
szLabel = _szLabel;
fE = _fE;
}
int iId;
String^ szLabel;
double fE;
};
The solution class interface includes a pointer to a raw C++ solution object. Pay special attention to the declaration of the delegate which is used to pass the callback from the .NET client to C++ DLL. [UnmanagedFunctionPointer(CallingConvention::Cdecl, CharSet = CharSet::Unicode)] etc. are needed for the proper calling convention and proper casting of .NET delegate to function pointer through the InteropServices (depending on the character set used in C++ DLL). It took me several hours to figure this out. A generic List in .NET corresponds to the standard C++ vector. Notice the reference operator to the List of the reference type of cgiMaterialCli in the getMaterials() function. If you are not familiar with the notation of C++/CLI, I recommend the book "Pro Visual C++/CLI" by Stephen R.G. Fraser.
using namespace System;
using namespace System::Text;
using namespace System::Diagnostics;
using namespace System::Collections::Generic;
using namespace System::Runtime::InteropServices;
#include "cgiDefinesCli.h"
struct cgiIStructure; public ref class cgiSolverBlazeClass{
public:
cgiSolverBlazeClass();
~cgiSolverBlazeClass();
#ifdef _UNICODE
[UnmanagedFunctionPointer(CallingConvention::Cdecl, CharSet = CharSet::Unicode)]
#else
[UnmanagedFunctionPointer(CallingConvention::Cdecl, CharSet = CharSet::Ansi)]
#endif
delegate void ListMessageDelegate(String^);
void setListMessageFunction(ListMessageDelegate^ fnListMsg);
bool createStructure();
void setMaterials(List < cgiMaterialCli^ > ^ listMat);
void getMaterials(List < cgiMaterialCli^ > ^% listMat);
bool runAnalysis();
protected:
!cgiSolverBlazeClass();
private:
cgiIStructure* m_pStructure;
GCHandle m_delegateHandle;
ListMessageDelegate^ m_nativeCallback;
};
The solution class implementation is as follows. You should include tchar.h and atlstr.h before the using namespace System. Otherwise, you would get bunch of unintelligible compile errors. CString is conveniently used to convert String^ to TCHAR. Conversion from TCHAR* to String^ is straightforward as String takes TCHAR* in its constructor. Notice how GetFunctionPointerForDelegate() is used to cast the .NET delegate to C++ function pointer.
#include "stdafx.h"
#include "../cgiSolverBlaze/_cgiIStructure.h"
#include < atlstr.h >
#include "cgiSolverBlazeCli.h"
using namespace cgiSolverBlazeCli;
typedef void (* fnLISTMSG)(LPCTSTR sz);
CString cgiConvertString(const String^ s){
CString sOut(s);
return sOut;
}
cgiSolverBlazeClass::cgiSolverBlazeClass(){
m_pStructure = 0;
}
cgiSolverBlazeClass::~cgiSolverBlazeClass(){}
void cgiSolverBlazeClass::setListMessageFunction(ListMessageDelegate^ fnListMsg){
m_nativeCallback = fnListMsg;
m_delegateHandle = GCHandle::Alloc(m_nativeCallback);
IntPtr ptr = Marshal::GetFunctionPointerForDelegate(m_nativeCallback);
m_pStructure->setListMessageFunction( static_cast < fnLISTMSG > (ptr.ToPointer()) );
}
cgiSolverBlazeClass::!cgiSolverBlazeClass(){
OutputDebugString(_T("cgiSolverBlazeClass finalized!"));
}
bool cgiSolverBlazeClass::createStructure(){
m_pStructure = ::CreateStructure();
return true;
}
void cgiSolverBlazeClass::setMaterials(List < cgiMaterialCli^ > ^ listMat){
std::vector < cgiMaterial> vMat;
for(int i = 0 ; i < listMat->Count; i++){
cgiMaterialCli^ item = listMat[i];
cgiMaterial mat;
mat.iId = item->iId;
mat.fE = item->fE;
_tcscpy(mat.szLabel, cgiConvertString(item->szLabel));
vMat.push_back(mat);
}
m_pStructure->setMaterials(vMat);
}
void cgiSolverBlazeClass::getMaterials(List < cgiMaterialCli^ > ^% listMat){
std::vector < cgiMaterial> vMat;
m_pStructure->getMaterials(vMat);
listMat->Clear();
for(int i = 0; i < vMat.size(); i++){
const cgiMaterial& mat = vMat[i];
cgiMaterialCli^ item = gcnew cgiMaterialCli();
item->iId = mat.iId;
item->fE = mat.fE;
item->szLabel = gcnew String(mat.szLabel);
listMat->Add(item);
}
}
bool cgiSolverBlazeClass::runAnalysis(){
return m_pStructure->runAnalysis();
}
Part Four - cgiSolverBlazeTestCSharp
The part is a C# console application. It only interfaces with the C++/CLI class library.
class Program {
static void Callback(string s) {
Console.WriteLine("{0}", s);
}
static void Main(string[] args) {
Console.WriteLine("Running from C# client through C++/CLI");
cgiSolverBlazeClass solver = new cgiSolverBlazeClass();
solver.createStructure();
cgiSolverBlazeClass.ListMessageDelegate ListMsg =
new cgiSolverBlazeClass.ListMessageDelegate(Callback);
solver.setListMessageFunction(ListMsg);
List < cgiMaterialCli > listMat = new List < cgiMaterialCli > ();
cgiMaterialCli mat = new cgiMaterialCli();
mat.setId(1);
mat.setProperties("Default222", 29000);
listMat.Add(mat);
solver.setMaterials(listMat);
bool bRun = solver.runAnalysis();
}
}
Points of Interest
There are occasions where you need to make C++ code available to .NET users. If you have control to the existing C++ code, you should seriously consider using C++/CLI as a wrapper instead of using pInvoke or a complete rewrite. After all, your existing code has been working and possibly working faster than the .NET counterpart. I hope this article helps some of you in some way. I would like to end this article by a quote from Rich Cook:
"Programming today is a race between software engineers striving to build bigger and better idiot-proof programs, and the Universe trying to produce bigger and better idiots. So far, the Universe is winning."
!Happy Coding!
History
- 23rd September, 2010: Initial post
Junlin Xu is the founder of Computations & Graphics, Inc. (www.cg-inc.com) and author of OpenGraph Library and Real3D-Analysis. He has seventeen years of software development experience. His expertise includes C++, C#, C++/CLI, ObjectiveC, MFC, OpenGL, DirectX, WIN32 API, COM/COM++, WinForms, MS SQL, MySql, ASP.NET, WCF, WPF, Mirth Connect. He is also an expert in Finite Element Method procedure. He is currently a software engineer at a company in Colorado. He can be reached at junlin.xu@gmail.com.