【问题标题】:Delegate on instance method passed by P/Invoke委托 P/Invoke 传递的实例方法
【发布时间】:2018-03-24 15:31:52
【问题描述】:

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

我一直认为,当我的 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.SystemFunction 的“this”对象也找到了通往本机 dll 的方式,它只接受静态函数,不包含任何“this”对象的功能或者和函数一起打包。

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

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

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

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

【问题讨论】:

  • 我想你知道这个问题的答案。您的代码证明了这一点。
  • @DavidHeffernan:您的意思是关于它如何在内部实现的问题?我刚刚提出了一个假设,如果知道它是真的还是有什么我没有想到的,那就太好了。

标签: c# delegates pinvoke


【解决方案1】:

这是否意味着任何此类委托都被单独转换(编组?)为静态函数...

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

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

 var myFunctor = new MySystemFunctor();

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

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

值得注意的是,您通过明确地新建委托来做对了。 C# 语法糖不需要这样做,因此更难做到这一点。如果回调只发生在 您对本机代码进行 pinvoke 调用时,并不少见(如 EnumWindows),那么您不必担心它,因为 pinvoke 编组器确保委托对象保持引用.

【讨论】:

  • 在我的例子中,对 KINInit 的调用只存储回调。然后调用另一个例程 KINSolve,猜猜看,它假定回调仍然存在。所以我从你的评论中得出的结论是,只要我在对 KINInit 和 KINSolve 的调用之间保留对 mySystemFunctor 的引用,这一切都是安全的,对吧?
  • 编辑让您感觉更好。将它们存储在类的字段中,当 KINSolve() 是实例方法时,你会没事的。尽管这听起来不像是确保没有进一步回调的清理方法。想想可能的 KINTerminate() 或 Dispose() 方法。
  • 但是仅仅将委托实例存储在一个字段中可能还不够,因为可以在调用KINSolve 的方法期间收集实例(与委托一起)(假设它不使用该委托或其他实例字段)。
【解决方案2】:

记录在案: Hans Passant 提到过,我已经走进了陷阱。强制垃圾回收导致空引用异常,因为委托是瞬态的:

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 发生,它就会起作用!

【讨论】:

  • 我的猜测是不回答这个问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-10
  • 1970-01-01
  • 1970-01-01
  • 2013-08-13
  • 2013-10-19
相关资源
最近更新 更多