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