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

Native under Managed

5.00/5 (10 votes)
26 Jan 2012CPOL5 min read 72.9K   936  
A guide about how to mix native and managed code in one solution

Introduction

DotNET is more and more widly used in desktop software development especially UI, meanwhile native C/C++ is still the main aspect for many performance sensitive functionalities. We have many reasonable motivations to use both native and managed code to make software powerful and easy to maintain. I hope this article would be a guide for those who haven't experienced the convenience of such mixed programming.

Overview

I assume you are already familiar with how to code with C# and (native) C++, and maybe know some CLI C++. If you are not familiar with CLI C++, you could get many ideas from search engines; the "C++/CLI Language Specification" should be a good handbook too.

There are two approaches to combine native & managed programs together. One is managed under native and another is native under managed. In this article we are focusing on the second one which uses managed code for UI as top layer is more common.

a.png

P/Invoke is much more limited in many cases even though it can be used to call native interfaces in managed environment.

Using the Code

Tips

All source code associated with this article are built using NativeC++/CLI C++/C# with VS2008(SP1); but it's still practicable to port the source code to VS2005 and later versions.

If you'd like to debug native code in a mixed enviroment, you'd to set the "Enable unmanaged code debugging" option checked on the entry managed project, for detail information please see MSDN.

Native

The demo project is only a sample showing how to combine native C++ to C# via CLI C++. For example we got a pure native C++ class like below:

MC++
class MyNativeClass {
public:
    class Listener {
    public:
        Listener();
        virtual ~Listener();
        virtual void Callback(const ObjParamDict &p);
    };
public:
    MyNativeClass();
    virtual ~MyNativeClass();
    void SetFuncPtr(FuncPointer fp);
    void SetListener(Listener* lsn);
    void Fun0(const ParamList &p);
    void Fun1(const ObjParamDict &p);
    s32 GetIntegerProp(void) const;
    void SetIntegerProp(s32 val);
private:
    FuncPointer mFuncPtr;
    Listener* mListener;
    s32 mInteger;
};

You may find frequently used programming concepts in this class like functions to be called, function pointer/listener callbacks, fields with getter/setter etc. I encapsuled a simple generic type data holder class named Obj in this project; you could replace it with some other type-list/variant/any libraries as you choose.

In Fun0 we extract three f32 from the ParamList and calculate a result using those parameters, then call the function pointer callback with calculated result as a parameter. See below:

MC++
void MyNativeClass::Fun0(const ParamList &p) {
    f32 f0 = p.at(0);
    f32 f1 = p.at(1);
    f32 f2 = p.at(2);
    f32 sum = f0 + f1 + f2;
    ParamList _p;
    _p.push_back(sum);
    (*mFuncPtr)(_p);
}

In Fun1 we walk over the ObjParamDict and serialize iterated key-values to a single StdStr, then call the listener callback with serialized result. See below:

MC++
void MyNativeClass::Fun1(const ObjParamDict &p) {
    StdStr ret;
    for(ObjParamDict::const_iterator it = p.begin(); it != p.end(); ++it) {
        ret += "[";
        ret += it->first.getStr();
        ret += ": ";
        ret += it->second.getStr();
        ret += "]; ";
    }
    ObjParamDict _p;
    _p["RET"] = ret.c_str();
    mListener->Callback(_p);
}

CLI Wrapper

CLI C++ is a superset of standard C++, so we can write both native (in original pure native C/C++ syntax) and managed (in CLI C++ syntax) code in a CLI wrapper project which plays the part of glue between native and managed sides. It's considered to deal with some data type/structure adapting and invoke transmiting at this layer.

1. Data type adapting

It's very easy to convert fundamental data types from native to managed and vice versa. We can assign a value directly to a native/managed variable like int ni = 1986; Int32 mi = ni; or Single ms = 3.14f; float ns = ms;. For native/managed data types correspondence, see below:

b.png

We have to use an explicit managed object if we want to call managed functions like System.Object.ToString(), such as int ni = 123; String mfi = (Int32(ni)).ToString();.

Unlike fundamental data types, we have to do some essential converting for complex types. For example strings are converted like below:

MC++
String^ WrapperUtil::ConvertString(ConstStr n) {
    return gcnew String(n);
}
StdStr WrapperUtil::ConvertString(String^ m) {
    IntPtr p = Marshal::StringToHGlobalAnsi(m);
    ConstStr linkStr = static_cast<Str>(p.ToPointer());
    StdStr result(linkStr);
    Marshal::FreeHGlobal(p);
    return result;
}

And further more this entire article is almost talking about how to convert more complex user defined types.

2. Native field getter/setter to managed property

To adapt native field getter/setter to managed property, just encapsule them into a CLI C++ property definition like below:

MC++
property Int32 IntegerProp {
    Int32 get();
    Void set(Int32 v);
} 

Then call a native getter in get() and a native setter in set().

3. Native callback to managed delegate/event

It is a little complicated making event definition in CLI C++. See below:

MC++
public:
    delegate Void SomeEventHandler(List<Object^>^ p);
protected:
    SomeEventHandler^ OnSomeEvent;
public:
    event SomeEventHandler^ SomeEvent {
    public:
        void add(SomeEventHandler^ _d) {
            OnSomeEvent += _d;
        }
        void remove(SomeEventHandler^ _d) {
            OnSomeEvent -= _d;
        }
    private:
        void raise(List<Object^>^ p) {
            if(OnSomeEvent)
                OnSomeEvent->Invoke(p);
        }
    }

We have to write some original native C++ code to bridge native function pointer or listener to managed event in a CLI C++ project, such as defining some global/static function handler or derived listener handler instance; then pass those pointers to native, and call SomeEvent(List<Object^>^) when native callback occurs to raise it to upper pure managed environment. This would be a rattling on work.

4. std::exception to System::Exception

I always surround decurrent native invoking with try-catch statements in CLI C++ then re-throw a managed version exception antrorsely to pure managed environment. See below:

MC++
try {
    DoSomething();
} catch(const std::exception &ex) {
    String^ msg = WrapperUtil::ConvertString(ex.what());
    throw gcnew Exception(msg);
}

Managed

You can follow pure managed programming experiences to integrate a wrapped CLI C++ project after referencing it. For this example, we can do it like below in C#:

C#
public partial class FormMain : Form
{
    private MyCliClass underManaged = null;
    public FormMain()
    {
        InitializeComponent();
        underManaged = new MyCliClass();
        underManaged.FuncPtrEvent += new MyCliClass.MyFuncPtrEventHandler(underManaged_FuncPtrEvent);
        underManaged.ListenerEvent += new MyCliClass.MyListenerEventHandler(underManaged_ListenerEvent);
        propGrid1.SelectedObject = underManaged;
    }
    private void btnFun0_Click(object sender, EventArgs e)
    {
        float f0 = float.Parse(txtN0.Text);
        float f1 = float.Parse(txtN1.Text);
        float f2 = float.Parse(txtN2.Text);
        List<object> p = new List<object>();
        p.Add(f0);
        p.Add(f1);
        p.Add(f2);
        underManaged.Fun0(p);
    }
    private void btnFun1_Click(object sender, EventArgs e)
    {
        string k0 = txtKey0.Text;
        string k1 = txtKey1.Text;
        string k2 = txtKey2.Text;
        string v0 = txtVal0.Text;
        string v1 = txtVal1.Text;
        string v2 = txtVal2.Text;
        Dictionary<object, object> p = new Dictionary<object, object>();
        p[k0] = v0;
        p[k1] = v1;
        p[k2] = v2;
        underManaged.Fun1(p);
    }
    private void underManaged_FuncPtrEvent(List<object> p)
    {
        MessageBox.Show(this, p[0].ToString(), "Native under Managed", MessageBoxButtons.OK, MessageBoxIcon.Information); 
    }
    private void underManaged_ListenerEvent(Dictionary<object, object> p)
    {
        MessageBox.Show(this, p["RET"].ToString(), "Native under Managed", MessageBoxButtons.OK, MessageBoxIcon.Information);
    }
}

Effective Design

Using different languages in one solution would bring some unavoidable gaps between them; it would bring some disadvantages like time complexity on function invoking, time/space complexity on data types adapting while making up those gaps acrossing language edges. Also we have to rewrite something twice in two languages while coding a wrapper; the more stuff to be wrapped, the more a project is close to the duplicated code hell. Thus it's very important to keep interaction operations inside a single layer as much as possible and using crossing language invoking occasionally. Some abstract approach such as message dispatching may be very helpful to make a flexible and stable middle layer wrapper.

Beyond

Although this article is only about native under managed, I think now it's easy for you to conclude how to do it in another direction, if you'd really want to.

I did throw out a brick to attract a jade in this article, it'd be my pleasure to know this helpful for your native and managed mixed programming.

License

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