代码之家  ›  专栏  ›  技术社区  ›  oliver

P/Invoke传递的实例方法上的委托

  •  5
  • oliver  · 技术社区  · 7 年前

    令我惊讶的是,我今天发现了一个强大的功能。因为它看起来太好了以至于不可能是真的,我想确保它不只是因为一些奇怪的巧合才起作用。

    我一直认为,当我的p/invoke(到c/c++库)调用需要(回调)函数指针时,我必须在静态c#函数上传递委托。例如,在下文中,我总是将KINSysFn的委托引用到该签名的静态函数。

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate int KINSysFn(IntPtr uu, IntPtr fval, IntPtr user_data );
    

    并使用以下委托参数调用我的P/Invoke:

    [DllImport("some.dll", EntryPoint = "KINInit", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
    public static extern int KINInit(IntPtr kinmem, KINSysFn func, IntPtr tmpl);
    

    但现在我只是尝试在一个实例方法上传递一个委托,它也起了作用!例如:

    public class MySystemFunctor
    {
        double c = 3.0;
        public int SystemFunction(IntPtr u, IntPtr v, IntPtr userData) {}
    }
    
    // ...
    
    var myFunctor = new MySystemFunctor();
    KINInit(kinmem, myFunctor.SystemFunction, IntPtr.Zero);
    

    当然,我理解,在托管代码中,将“this”对象与实例方法打包在一起以形成各自的委托根本没有技术问题。

    但令我惊讶的是,MySystemFunctor的“this”对象。SystemFunction也可以使用本机dll,它只接受静态函数,不包含任何用于“this”对象的工具,也不将其与函数打包在一起。

    这是否意味着任何此类委托都被翻译(编组?)单独指向静态函数,其中对相应“this”对象的引用在函数定义中以某种方式硬编码?如何在不同的委托实例之间进行区分,例如

    var myFunctor01 = new MySystemFunctor();
    // ...
    var myFunctor99 = new MySystemFunctor();
    
    KINInit(kinmem, myFunctor01.SystemFunction, IntPtr.Zero);
    // ...
    KINInit(kinmem, myFunctor99.SystemFunction, IntPtr.Zero);
    

    这些不能都指向同一个函数。如果我动态创建数量不定的MySystemFunctor对象会怎么样?是否在运行时将每个这样的委托“展开”/编译为自己的静态函数定义?

    2 回复  |  直到 7 年前
        1
  •  10
  •   Hans Passant    7 年前

    这是否意味着任何此类委托都被翻译(编组?)单独到静态函数。。。

    是的,你猜对了。不完全是一个“静态函数”,CLR中有一大堆代码来执行这个神奇的功能。它会自动为thunk生成机器代码,将本地代码的调用转换为托管代码。本机代码获取指向该thunk的函数指针。可能必须转换参数值,这是标准的pinvoke marshaller职责。并且总是来回移动以匹配对托管方法的调用。挖掘存储的委托的目标属性以提供 this 是其中的一部分。它会抖动堆栈帧,将链接绑定到之前的托管帧,因此GC可以看到它需要再次查找对象根。

    然而,有一个令人讨厌的小细节让几乎每个人都陷入了麻烦。当不再需要回调时,会再次自动清除这些thunk。CLR没有从本机代码获得任何帮助来确定这一点,当委托对象被垃圾收集时就会发生这种情况。也许你闻到了老鼠的味道,是什么决定了你的程序何时发生这种情况?

     var myFunctor = new MySystemFunctor();
    

    这是方法的局部变量。它不会存活很长时间,下一次收集将摧毁它。坏消息是,如果本机代码继续通过thunk进行回调,它将不再存在,这是一个严重的崩溃。当您尝试代码时,不太容易看到,因为这需要一段时间。

    你必须确保这不会发生。在类中存储委托对象可能会起作用,但您必须确保类对象存在足够长的时间。无论如何,从片段中猜不出答案。当您还确保再次注销这些回调时,它往往会自行解决,因为这需要存储对象引用以供以后使用。您还可以将它们存储在静态变量中或使用GCHandle。Alloc(),但这当然失去了快速进行实例回调的好处。通过测试正确完成这项工作,感觉很好,请调用GC。调用方中的Collect()。

    值得注意的是,通过显式地新建委托,您做得很好。C#语法sugar不需要这样做,因此很难做到这一点。如果仅发生回调 虽然 您对本机代码进行pinvoke调用,这种情况并不少见(如EnumWindows),因此您不必担心,因为pinvoke封送拆收器确保委托对象保持被引用状态。

        2
  •  3
  •   oliver    7 年前

    记录如下: 汉斯·帕桑特(HansPassant)提到,我已经直接走进了陷阱。强制垃圾回收导致了空引用异常,因为委托是暂时的:

    KINInit(kinmem, myFunctor.SystemFunction, IntPtr.Zero);
    // BTW: same with:
    // KINInit(kinmem, new KINSysFn(myFunctor.SystemFunction), IntPtr.Zero);
    
    GC.Collect();
    GC.WaitForPendingFinalizers();
    
    KINSol(/*...*); // BAAM! NullReferenceException
    

    幸运的是,我已经将关键的两个P/invoke,KINInit(设置回调委托)和KINSolve(实际使用回调)打包到一个专用的托管类中。如前所述,解决方案是让委托由类成员引用:

    // ksf is a class member of delegate type KINSysFn that keeps ref to delegate instance
    ksf = new KINSysFn(myFunctor.SystemFunction); 
    KINInit(kinmem, ksf, IntPtr.Zero);
    
    GC.Collect();
    GC.WaitForPendingFinalizers();
    
    KINSol(/*...*);
    

    再次感谢你,汉斯,我从来没有注意到这个缺陷,因为只要没有GC发生,它就可以工作!