通过 P/Invoke 使用回调和堆对象的安全方法

Safe way to use callbacks and heap objects with P/Invoke

以下样本来自Microsoft's documentation

public delegate bool CallBack(int handle, IntPtr param);

public class LibWrap
{
  // passing managed object as LPARAM
  // BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);

  [DllImport("user32.dll")]
  public static extern bool EnumWindows(CallBack cb, IntPtr param);
}

public class App
{
  public static void Main()
  {
    Run();
  }

  [SecurityPermission(SecurityAction.Demand, UnmanagedCode=true)]
  public static void Run()
  {
    TextWriter tw = System.Console.Out;
    GCHandle gch = GCHandle.Alloc(tw);

    CallBack cewp = new CallBack(CaptureEnumWindowsProc);

    // platform invoke will prevent delegate to be garbage collected
    // before call ends

    LibWrap.EnumWindows(cewp, GCHandle.ToIntPtr(gch));
    gch.Free();
  }

  private static bool CaptureEnumWindowsProc(int handle, IntPtr param)
  {
    GCHandle gch = GCHandle.FromIntPtr(param);
    TextWriter tw = (TextWriter)gch.Target;
    tw.WriteLine(handle);
    return true;
  }
}

有两件事让我很困惑。

首先,GCHandle.Alloc的文档只谈到防止对象被垃圾回收。如果仅此而已,您就不需要 GCHandle.Alloc:显然在示例中,在调用 EnumWindows 期间不会收集 tw - 在功能范围。

这里的问题是需要确保它没有移动。但是 GCHandle.Alloc 的文档并没有谈到这一点。那么这是怎么回事?

其次,代表呢?这个示例中可能没有问题,但是如果委托绑定到一个对象(带有闭包的 lambda 或 class 的非静态方法)怎么办?在那种情况下,一个人也需要照顾代表,对吧?是另一个 GCHandle.Alloc(myDelegate) 还是有更多的事情要考虑?

搬家不是问题。 GCHandle.ToIntPtr 承诺为您提供一个整数值,您可以在以后将其传递给 GCHandle.FromIntPtr 以检索原始句柄。就这样。如果您需要停止对象在内存中的移动,则必须将其固定。但是您实际上不需要固定对象,您只需要停止收集它,并能够在您的回调中检索它。

委托的生命周期在这里不是问题,因为 p/invoke 框架将确保它不会在对 EnumWindows 的外部调用期间被收集,如评论中所述。如果您将委托传递给非托管代码,并且该非托管代码持有对委托的引用,那么您还有工作要做。您必须确保委托比对它的非托管引用更有效。