混合编程:原生与托管代码的结合

在桌面软件开发中,尤其是涉及到用户界面(UI)的开发时,原生C/C++语言因其性能优势仍然是许多性能敏感功能的首选。与此同时,托管代码如C#因其易用性和强大的开发工具支持,也常被用于UI层的开发。本文旨在为那些尚未体验过混合编程便利性的开发者提供指导。

概述

假设已经熟悉C#和原生C++的编码方式,并且可能了解一些CLI C++。如果不熟悉CLI C++,可以通过搜索引擎获取许多资料;"C++/CLI 语言规范"也是一个很好的参考手册。

将原生和托管程序结合在一起有两种方法:一种是托管代码在原生代码之下,另一种是原生代码在托管代码之下。本文将重点介绍后者,即使用托管代码作为UI层更为常见。

尽管P/Invoke可以在托管环境中调用原生接口,但在许多情况下它的功能非常有限。

使用代码

本文相关的所有源代码都是使用NativeC++/CLI C++/C#与VS2008(SP1)构建的;但这些源代码仍然可以移植到VS2005及以后的版本。

如果想在混合环境中调试原生代码,需要在入口托管项目上勾选“启用非托管代码调试”选项,详细信息请参见MSDN。

原生代码示例

以下是一个简单的原生C++类示例,展示了如何通过CLI C++将原生C++与C#结合。例如,有一个纯原生C++类:

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; };

在这个类中,可以找到许多常用的编程概念,如被调用的函数、函数指针/监听器回调、带有getter/setter的字段等。

CLI包装器

CLI C++是标准C++的超集,因此可以在CLI包装器项目中编写原生(使用原始纯原生C/C++语法)和托管(使用CLI C++语法)代码,CLI包装器充当原生和托管之间的粘合剂。

1. 数据类型适配

将基本数据类型从原生转换为托管,反之亦然非常简单。可以直接将值赋给原生/托管变量,例如:

int ni = 1986; Int32 mi = ni; Single ms = 3.14f; float ns = ms;

对于原生/托管数据类型对应关系,请参阅下表:

2. 原生字段的getter/setter转换为托管属性

要将原生字段的getter/setter适配为托管属性,只需将它们封装在CLI C++属性定义中,例如:

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

3. 原生回调转换为托管委托/事件

在CLI C++中定义事件可能会有些复杂,例如:

public: delegate Void SomeEventHandler(List^ 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^ p) { if (OnSomeEvent) OnSomeEvent->Invoke(p); } }

4. std::exception转换为System::Exception

总是在CLI C++中用try-catch语句包围异常的原生调用,然后反向抛出一个托管版本的异常,例如:

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

托管代码集成

在引用了包装后的CLI 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 p = new List(); 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 p = new Dictionary(); p[k0] = v0; p[k1] = v1; p[k2] = v2; underManaged.Fun1(p); } private void underManaged_FuncPtrEvent(List p) { MessageBox.Show(this, p[0].ToString(), "Native under Managed", MessageBoxButtons.OK, MessageBoxIcon.Information); } private void underManaged_ListenerEvent(Dictionary p) { MessageBox.Show(this, p["RET"].ToString(), "Native under Managed", MessageBoxButtons.OK, MessageBoxIcon.Information); } }

有效设计

在一个解决方案中使用不同的语言会带来一些不可避免的间隙;这会带来一些缺点,如跨语言边界时函数调用的时间复杂度、数据类型适配的时间/空间复杂度。同时,还需要在两种语言中重写一些东西,包装得越多,项目就越接近重复代码地狱。因此,尽可能保持交互操作在单个层内,并偶尔使用跨语言调用非常重要。一些抽象方法,如消息派发,可能非常有助于制作灵活且稳定的中间层包装器。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485