令我惊讶的是,我今天发现了一个强大的功能。因为它看起来好得令人难以置信,所以我想确保它不仅仅是因为一些奇怪的巧合而起作用。
我一直认为当我的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对象怎么办?是否每个这样的委托"展开" /在运行时被编译为自己的静态函数定义?
答案 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发生就会有效!