委托 P/Invoke 传递的实例方法
Delegate on instance method passed by P/Invoke
令我惊讶的是,我今天发现了一个强大的功能。因为它看起来好得令人难以置信,所以我想确保它不仅仅是因为一些奇怪的巧合而起作用。
我一直认为,当我的 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 对象怎么办?每个这样的委托 "unrolled"/是否在运行时编译成它自己的静态函数定义?
Does this mean that any such delegate is translated (marshalled?) individually to a static function...
是的,您猜对了。不完全是 "static function",CLR 中有大量代码可以执行此魔术。它 auto-generates 一个 thunk 的机器代码,它使从本机代码到托管代码的调用适应。本机代码获取指向该 thunk 的函数指针。可能必须转换参数值,这是标准的 pinvoke 编组器职责。并且总是四处乱窜以匹配对托管方法的调用。挖掘存储委托的目标 属性 以提供 this
是其中的一部分。它会调整堆栈帧,将 link 绑定到先前的托管帧,因此 GC 可以看到它再次需要查找对象根。
然而,有一个令人讨厌的小细节几乎让每个人都陷入困境。当不再需要回调时,这些 thunk 会自动再次 cleaned-up。 CLR 没有从本机代码获得帮助来确定这一点,它发生在委托对象获得 garbage-collected 时。也许你闻到了老鼠的味道,什么决定了你的程序何时会发生这种情况?
var myFunctor = new MySystemFunctor();
那是一个方法的局部变量。它不会存活很长时间,下一次收集将摧毁它。坏消息是,如果本机代码不断通过 thunk 进行回调,它将不再存在,这将是一次严重的崩溃。尝试代码时不太容易看到,因为它需要一段时间。
你必须确保这不会发生。将委托对象存储在 class 中可能有效,但随后您必须确保 class 对象存在足够长的时间。不管需要什么,都不要从片段中猜测。当您还确保再次取消注册这些回调时,它往往会自行解决,因为这需要存储对象引用以供以后使用。您也可以将它们存储在静态变量中或使用 GCHandle.Alloc(),但这当然会失去快速进行实例回调的好处。通过测试正确完成此操作感觉很好,在调用方中调用 GC.Collect()。
值得注意的是,new-ing 委托人明确地做到了这一点。 C# 语法糖不要求这样做,因此更难做到这一点。如果回调只发生 而 你将 pinvoke 调用到本机代码中,并不少见(如 EnumWindows),那么你不必担心它,因为 pinvoke 编组器确保委托对象保持引用状态。
记录:汉斯·帕桑特提到,我已经走进了陷阱。强制垃圾回收导致空引用异常,因为委托是瞬态的:
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、KINIinit(设置回调委托)和 KINSolve(实际使用回调)封装到一个专用的托管 class 中。如前所述,解决方案是让 class 成员引用委托:
// 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 发生,它就会起作用!
令我惊讶的是,我今天发现了一个强大的功能。因为它看起来好得令人难以置信,所以我想确保它不仅仅是因为一些奇怪的巧合而起作用。
我一直认为,当我的 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 对象怎么办?每个这样的委托 "unrolled"/是否在运行时编译成它自己的静态函数定义?
Does this mean that any such delegate is translated (marshalled?) individually to a static function...
是的,您猜对了。不完全是 "static function",CLR 中有大量代码可以执行此魔术。它 auto-generates 一个 thunk 的机器代码,它使从本机代码到托管代码的调用适应。本机代码获取指向该 thunk 的函数指针。可能必须转换参数值,这是标准的 pinvoke 编组器职责。并且总是四处乱窜以匹配对托管方法的调用。挖掘存储委托的目标 属性 以提供 this
是其中的一部分。它会调整堆栈帧,将 link 绑定到先前的托管帧,因此 GC 可以看到它再次需要查找对象根。
然而,有一个令人讨厌的小细节几乎让每个人都陷入困境。当不再需要回调时,这些 thunk 会自动再次 cleaned-up。 CLR 没有从本机代码获得帮助来确定这一点,它发生在委托对象获得 garbage-collected 时。也许你闻到了老鼠的味道,什么决定了你的程序何时会发生这种情况?
var myFunctor = new MySystemFunctor();
那是一个方法的局部变量。它不会存活很长时间,下一次收集将摧毁它。坏消息是,如果本机代码不断通过 thunk 进行回调,它将不再存在,这将是一次严重的崩溃。尝试代码时不太容易看到,因为它需要一段时间。
你必须确保这不会发生。将委托对象存储在 class 中可能有效,但随后您必须确保 class 对象存在足够长的时间。不管需要什么,都不要从片段中猜测。当您还确保再次取消注册这些回调时,它往往会自行解决,因为这需要存储对象引用以供以后使用。您也可以将它们存储在静态变量中或使用 GCHandle.Alloc(),但这当然会失去快速进行实例回调的好处。通过测试正确完成此操作感觉很好,在调用方中调用 GC.Collect()。
值得注意的是,new-ing 委托人明确地做到了这一点。 C# 语法糖不要求这样做,因此更难做到这一点。如果回调只发生 而 你将 pinvoke 调用到本机代码中,并不少见(如 EnumWindows),那么你不必担心它,因为 pinvoke 编组器确保委托对象保持引用状态。
记录:汉斯·帕桑特提到,我已经走进了陷阱。强制垃圾回收导致空引用异常,因为委托是瞬态的:
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、KINIinit(设置回调委托)和 KINSolve(实际使用回调)封装到一个专用的托管 class 中。如前所述,解决方案是让 class 成员引用委托:
// 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 发生,它就会起作用!