C++通过Callback向C#传递数据的方法
现在比较流行c#与c++融合:c#做gui,开发效率高,c++做运算,运行效率高,二者兼得。
但是c++与c#必然存在数据交互,c#与c++dll的数据交互从来都是一个让人头疼的问题。
从调用方式看也有两种情况:
1、c#调用c++函数
这种情况用的比较多,数据流向可以是c#流向c++,通过参数将数据传递给c++(如:setdata(double[] data));也可以是c++流向c#(如:getdata(double[] data))。
2、c++ callback
这种情况是c++中通过callback的方式调用c#代码,类似于c++做过一些处理后向c#发送事件,事件可以携带数据(如处理后的数据)。则c++中定义函数指针的方式是:
typedef void(*render)(double* data, bool* color);
c#作为委托,定义的函数被c++ callback:
public delegate void rendercallback([marshalas(unmanagedtype.lparray, sizeconst =23)]double[] data, [marshalas(unmanagedtype.lparray, sizeconst = 23)]int[] colors);
千万注意,delegate中的double[]数组一定要加上marshalas标记,标记为传递数组,而且必须指定传递的数量,如果不标记数量,则每次只传递一个数值,这个问题折磨我很久才搞定!
其他注意事项:
1、如何在c#中保持c++的函数指针
回调函数的另一个注意事项是向c++ dll传递回调函数指针的问题
假设有个函数向c++dll传递指针:
public delegate void ekfrendercallback(string data, string colors); public class ekflib { [dllimport("ekflib.dll", callingconvention = callingconvention.cdecl, charset = charset.ansi)] public static extern void setrendercallback(ekfrendercallback render);
c#中如下传递被回调的函数:
public void rendercallback(string data, string color) { // rendering } private void window_loaded(object sender, routedeventargs e) { ekflib.setrendercallback(rendercallback); ekflib.init(); }
这虽然没什么问题,但是通过setrendercallback()传入到c++的指针不受托管代码管理,在c#中认为此指针对象未被任何代码引用,gc做垃圾回收时,将会把c#本地的空指针回收,导致c++无法执行回调,出现“callbackoncollecteddelegate”错误:
对“motioncapture!motioncapture.ekfrendercallback::invoke”类型的已垃圾回收委托进行了回调。这可能会导致应用程序崩溃、损坏和数据丢失。向非托管代码传递委托时,托管应用程序必须让这些委托保持活动状态,直到确信不会再次调用它们。
微软官网的例子是控制gc回收机制,这是个比较笨拙的方法,更加理所当然的方法是把委托定义成一个属性,指向一个new出来的callback,然后再把这个callback传递进c++dll中,这样,在c#端有对象引用,保证了gc不会回收此callback:
public void rendercallback(string data, string color) { // rendering } private ekfrendercallback render; private void window_loaded(object sender, routedeventargs e) { render = new ekfrendercallback(rendercallback); ekflib.setrendercallback(render); ekflib.init(); }
2、__stdcall与_cdecl传递数据
最近一个项目是通过c++ 的 dll做高速运算,然后把结果数据通过callback的方式回调给c#(界面部分),结果总是在c#中接到回调事件后就直接挂掉(程序直接在毫无提示的情况下退出,没有任何调试信息或者提示)。
导致问题的原因是,默认情况下,c++中如下定义的函数指针,默认是以_cdecl方式调用的:
typedef void(*render)(double* data, bool* color);
这种情况下,参数堆栈是由调用者(c++一侧)维护的,在c++调用此回调函数后,会把参数弹出堆栈而释放,导致c#读取数据时出现莫名其妙的错误。
以上是回调函数传递数组可能出现的情况,而如下所示,只传递一个参数的情况,甚至会在c#方莫名其妙的卡死:
typedef void (*calibrationprogresscallback)(double percent);
改为__stdcall的方式即可解决问题,申明如下:
typedef void(__stdcall *render)(double* data, bool* color);
以下来自网络的一段_cdecl和__stdcall的解释,必须牢记:
1. __cdecl
即所谓的c调用规则,按从右至左的顺序压参数入栈,由调用者把参数弹出栈。切记:对于传送参数的内存栈是由调用者来维护的。返回值在eax中。因此,对于象printf这样变参数的函数必须用这种规则。编译器在编译的时候对这种调用规则的函数生成修饰名的饿时候,仅在输出函数名前加上一个下划线前缀,格式为_functionname。
2. __stdcall
按从右至左的顺序压参数入栈,由被调用者把参数弹出栈。_stdcall是pascal程序的缺省调用方式,通常用于win32 api中,切记:函数自己在退出时清空堆栈,返回值在eax中。 __stdcall调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数,格式为_functionname@number。如函数int func(int a, double b)的修饰名是_func@12
所以,从c++ dll中回调函数给c#传递数据,必须由c#函数在使用完数据后(退出函数时)自己清空堆栈!所c++中的回调函数指针应该如下定义:
typedef void (_stdcall *calibrationprogresscallback)(double percent);
总结:
c++通过callback向c#传递数据必须注意以下几点:
1、c++中的回调函数必须用_stdcall标记,使用stdcall方式回调;
2、如果是数组,必须用 [marshalas(unmanagedtype.lparray, sizeconst = 23)]标记参数,指定为数组且标记数组长度;
3、c#方必须申明一个变量,用来指向c++的回调指针函数,避免被c#回收掉。
以上这篇c++通过callback向c#传递数据的方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。