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

时间:2018-03-24 15:31:53

标签: c# delegates pinvoke

令我惊讶的是,我今天发现了一个强大的功能。因为它看起来好得令人难以置信,所以我想确保它不仅仅是因为一些奇怪的巧合而起作用。

我一直认为当我的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);

当然,据我所知,在托管代码内部,没有任何技术问题包装"这个"对象与实例方法一起构成相应的委托。

但令我惊讶的是,"这个" MySystemFunctor.SystemFunction的对象也找到了本地dll的方式,它只接受一个静态函数,并且不包含任何用于"这个"对象或将其与功能一起包装。

这是否意味着任何此类委托被单独翻译(编组?)到静态函数,其中对相应的" this"对象在函数定义中以某种方式硬编码?如何在不同的委托实例之间区分,例如,如果我有

var myFunctor01 = new MySystemFunctor();
// ...
var myFunctor99 = new MySystemFunctor();

KINInit(kinmem, myFunctor01.SystemFunction, IntPtr.Zero);
// ...
KINInit(kinmem, myFunctor99.SystemFunction, IntPtr.Zero);

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

2 个答案:

答案 0 :(得分:8)

  

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

是的,你猜错了。不完全是一个"静态函数",CLR中有一大堆代码执行这种魔术。它为thunk自动生成机器代码,以适应从本机代码到托管代码的调用。本机代码获取指向该thunk的函数指针。可能必须转换参数值,标准的pinvoke marshaller职责。并且总是随意调整以匹配对托管方法的调用。挖掘存储的委托的Target属性以提供this是其中的一部分。它会跳过堆栈帧,将链接绑定到前一个托管帧,这样GC就可以看到它再次需要查找对象根。

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

 var myFunctor = new MySystemFunctor();

这是方法的局部变量。它不会存活很长时间,下一个系列会破坏它。糟糕的消息,如果本机代码不断通过thunk进行回调,它将不再存在,而且这是一次严重的崩溃。在您尝试使用代码时不太容易看到,因为它需要一段时间。

你必须确保不会发生这种情况。在您的类中存储委托对象可能有效,但是您必须确保您的类对象能够存活足够长的时间。无论需要什么,都不能从片段中猜出来。当您还确保再次注销这些回调时,它往往会自行解决,因为这需要存储对象引用以供以后使用。您也可以将它们存储在静态变量中或使用GCHandle.Alloc(),但这当然会失去使用实例回调的好处。通过测试正确完成此操作感觉很好,在调用者中调用GC.Collect()。

值得注意的是,您通过明确地新建了委托来做到了。 C#语法糖不需要这样做,这使得更难做到这一点。如果回调只发生你将pinvoke调用到本机代码中,并不罕见(如EnumWindows),那么你不必担心它,因为pinvoke marshaller确保了委托对象保持参考。

答案 1 :(得分:1)

对于记录:我已经走进陷阱,汉斯帕斯特提到过。强制垃圾收集导致空引用异常,因为委托是瞬态的:

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 / Invokes,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(/*...*);

再次感谢Hans,我从来没有注意到这个漏洞,因为只要没有GC发生就会有效!