• 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传递指针:

    1 public delegate void EKFRenderCallback(string data, string colors);
    2  
    3 public class EKFLib
    4 {
    5     [DllImport("EKFLib.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
    6     public static extern void SetRenderCallback(EKFRenderCallback render);

    C#中如下传递被回调的函数:

    01 public void RenderCallback(string data, string color)
    02 {
    03     // rendering
    04 }
    05  
    06 private void Window_Loaded(object sender, RoutedEventArgs e)
    07 {
    08     EKFLib.SetRenderCallback(RenderCallback);
    09     EKFLib.Init();
    10 }

    这虽然没什么问题,但是通过SetRenderCallback()传入到C++的指针不受托管代码管理,在C#中认为此指针对象未被任何代码引用,GC做垃圾回收时,将会把C#本地的空指针回收,导致C++无法执行回调,出现“CallbackOnCollectedDelegate”错误:

    对“MotionCapture!MotionCapture.EKFRenderCallback::Invoke”类型的已垃圾回收委托进行了回调。这可能会导致应用程序崩溃、损坏和数据丢失。向非托管代码传递委托时,托管应用程序必须让这些委托保持活动状态,直到确信不会再次调用它们。

    微软官网的例子是控制GC回收机制,这是个比较笨拙的方法,更加理所当然的方法是把委托定义成一个属性,指向一个new出来的callback,然后再把这个callback传递进C++dll中,这样,在C#端有对象引用,保证了GC不会回收此callback:

    01 public void RenderCallback(string data, string color)
    02 {
    03     // rendering
    04 }
    05  
    06 private EKFRenderCallback render;
    07 private void Window_Loaded(object sender, RoutedEventArgs e)
    08 {
    09     render = new EKFRenderCallback(RenderCallback);
    10     EKFLib.SetRenderCallback(render);
    11     EKFLib.Init();
    12 }

    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#回收掉。

  • 相关阅读:
    Safe Path(bfs+一维数组存图)
    一维树状数组入门
    Ancient Go(简单DFS)
    Sudoku(简单DFS)
    Strategic game(树形DP入门)
    多线程源码分析
    Navicat premium12的破解方法,无需注册码
    MySQL和Oracle的区别
    java Web项目中什么场景中会用到java多线程?
    TCP协议三次握手、四次断开 过程分析
  • 原文地址:https://www.cnblogs.com/y114113/p/6289798.html
Copyright © 2020-2023  润新知