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