Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Leveraging your Existing C++ Code for Use in the .NET Environment

4.25/5 (8 votes)
23 Sep 2010CPOL4 min read 27.8K   258  
This article shows how you can make your existing C++ code available to .NET environment

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:

  1. A C++ Windows DLL library (the existing code)
  2. A C++ console application
  3. A C++/CLI class library that wraps the C++ Windows DLL
  4. 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).

C++
const int LABEL_SIZE = 128;
struct CGISOLVERBLAZE_API cgiMaterial{
    cgiMaterial(int id = -1){
        iId = id;
        _tcscpy(szLabel, _T("Default"));
        fE = 29e6;	// psi for steel
    }
    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.

C++
// function callbacks to notify the clients
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();    // create an instance of solution

The solution implementation sets the callback function, a vector of input data. The function runAnalysis() could be a lengthy computation routine.

C++
class CGISOLVERBLAZE_API CcgiStruct : public cgiIStructure
{
public:
    CcgiStruct();
    ~CcgiStruct();
public:
    // implement interfaces from cgiIStructure
    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"));
    // do lengthy calculation here
    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.

C++
#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();
    // message functions, can be set null in which case no messages 
    // will be printed during solution
    pStructure->setListMessageFunction(ListMsg);
    
    // define materials
    vector < cgiMaterial> vMat;
    cgiMaterial mat;
    mat.setId(1);    // material id, to be referred later
    mat.setProperties(_T("Default"), 29000);  // material label, young's modulus
    vMat.push_back(mat);
    pStructure->setMaterials(vMat);
    
    // run analysis
    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.

C++
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.

MC++
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;   // forward declaration
public ref class cgiSolverBlazeClass{
    public:
        cgiSolverBlazeClass();
        ~cgiSolverBlazeClass();

#ifdef _UNICODE
        // jxu: important to keep the right calling convention and character set
        [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.

C++
#include "stdafx.h"
// jxu:  this include file must be included prior to using namespace System;
#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 = gcnew ListMessageDelegate(this, &cgiSolverBlazeClass::Callback);
    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.

C++
class Program {
        // delegate used for the call back.
        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();

            // the following should be called after createStructure()
            cgiSolverBlazeClass.ListMessageDelegate ListMsg = 
		new cgiSolverBlazeClass.ListMessageDelegate(Callback);
            solver.setListMessageFunction(ListMsg);

            // define materials
            List < cgiMaterialCli > listMat = new List < cgiMaterialCli > ();
            cgiMaterialCli mat = new cgiMaterialCli();
            mat.setId(1);
            mat.setProperties("Default222", 29000);
            listMat.Add(mat);
            solver.setMaterials(listMat);

            // run static analysis
            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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)