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

    from:http://www.roboby.com/

  • 相关阅读:
    游标cursor
    SQL: EXISTS
    LeetCode Reverse Integer
    LeetCode Same Tree
    LeetCode Maximum Depth of Binary Tree
    LeetCode 3Sum Closest
    LeetCode Linked List Cycle
    LeetCode Best Time to Buy and Sell Stock II
    LeetCode Balanced Binary Tree
    LeetCode Validate Binary Search Tree
  • 原文地址:https://www.cnblogs.com/tewuapple/p/8205893.html
Copyright © 2020-2023  润新知