无法将 ReleaseHandle 中的 SafeHandle 实例传递给本机方法

Cannot pass SafeHandle instance in ReleaseHandle to native method

我最近才了解到 SafeHandle,为了测试,我为 SDL2 库实现了它,创建和销毁了 window:

[DllImport(_libName, CallingConvention = CallingConvention.Cdecl)]
internal static extern IntPtr SDL_CreateWindow(
    [MarshalAs(UnmanagedType.LPStr)] string title, int x, int y, int w, int h, uint flags);

[DllImport(_libName, CallingConvention = CallingConvention.Cdecl)]
internal static extern void SDL_DestroyWindow(IntPtr window);

public class Window : SafeHandleZeroOrMinusOneIsInvalid
{
    public Window() : base(true)
    {
        SetHandle(SDL_CreateWindow("Hello", 400, 400, 800, 600, 0));
    }

    protected override bool ReleaseHandle()
    {
        SDL_DestroyWindow(handle);
        return true;
    }
}

这很好用,然后我了解到使用 SafeHandle 的另一个优点:可以直接在 p/invoke 签名中使用 class,如下所示:

[DllImport(_libName, CallingConvention = CallingConvention.Cdecl)]
internal static extern Window SDL_CreateWindow(
    [MarshalAs(UnmanagedType.LPStr)] string title, int x, int y, int w, int h, uint flags);

[DllImport(_libName, CallingConvention = CallingConvention.Cdecl)]
internal static extern void SDL_DestroyWindow(Window window);

这当然比通用 IntPtr 参数/returns 好得多,因为我有类型安全传递/检索实际 Window(句柄)到/从这些方法。

虽然这适用于 SDL_CreateWindow,它现在正确地 returns 一个 Window 实例,但它不适用于 SDL_DestroyWindow,这是我在Window.ReleaseHandle 像这样:

public Window() : base(true)
{
    SetHandle(SDL_CreateWindow("Hello", 400, 400, 800, 600, 0).handle);
}

protected override bool ReleaseHandle()
{
    SDL_DestroyWindow(this);
    return true;
}

当试图将 this 传递给 SDL_DestroyWindow 时,我得到一个 ObjectDisposedException安全句柄已关闭。确实IsClosed属性是true,没想到这个时候是。显然它在内部尝试增加引用计数,但注意到 IsClosedtrue。根据 documentation,它已被设置为 true 因为 "The Dispose method or Close method was called and there are no references to the SafeHandle object on other threads.",所以我猜 Dispose 之前在调用堆栈中被隐式调用以调用我的 ReleaseHandle .

如果我想在 p/invoke 签名中使用 class 参数,

ReleaseHandle 显然不是清理的正确位置,所以我想知道是否有任何方法我可以在不破坏 SafeHandle 内部结构的情况下进行清理吗?

我上面的问题被我了解到的关于 SafeHandle 的错误信息稍微误导了(通过一些我不会提及的博客文章)。虽然我被告知用 class 实例替换 P/Invoke 方法中的 IntPtr 参数是“ SafeHandle 提供的主要优势”和绝对不错,事实证明它只是部分有用:

小心自动 SafeHandle 编组器

创建

一方面,我这样说是因为我上面的代码有一个我一开始没有看到的大问题。我写了这段代码:

void DoStuff()
{
    Window window = new Window();
}

public class Window : SafeHandleZeroOrMinusOneIsInvalid
{
    public Window() : base(true)
    {
        // SDL_CreateWindow will create another `Window` instance internally!!
        SetHandle(SDL_CreateWindow("Hello", 400, 400, 800, 600, 0).handle);
    }

    protected override bool ReleaseHandle()
    {
        SDL_DestroyWindow(handle); // Since "this" won't work here (s. below)
        return true;
    }

    // Returns Window instance rather than IntPtr via the automatic SafeHandle creation
    [DllImport(_libName, CallingConvention = CallingConvention.Cdecl)]
    private static extern Window SDL_CreateWindow(
        [MarshalAs(UnmanagedType.LPStr)] string title, int x, int y, int w, int h, uint flags);

    // Accept Window instance rather than IntPtr (won't work out, s. below)
    [DllImport(_libName, CallingConvention = CallingConvention.Cdecl)]
    private static extern void SDL_DestroyWindow(Window window);
}

当编组器在Window构造函数中调用SDL_CreateWindow的P/Invoke方法时,它会在内部创建另一个实例Window class 用于 return 值(调用所需的无参数构造函数,然后在内部设置 handle 成员)。这意味着我现在有两个 SafeHandle 实例:

  • 一个 return 由 SDL_CreateWindow 方法编辑 - 我没有在任何地方使用它(只剥离 handle 属性)
  • 一个由我的用户代码调用 new Window() 创建的,SafeHandle class 本身

这里实现SafeHandle的唯一正确方法是再次让SDL_CreateWindowreturn一个IntPtr,所以没有内部编组SafeHandle实例已创建。

无法在 ReleaseHandle

中传递 SafeHandle

正如 Simon Mourier 在评论中解释/引用的那样,在 ReleaseHandle 中清理时根本不能再使用 SafeHandle 本身,因为该对象已被垃圾收集并试图做 "fancy" 将它传递给 P/Invoke 方法之类的事情不再安全/注定要失败。 (鉴于我被告知 P/Invoke 中 IntPtr 参数的替换是 SafeHandle 的 "the main features" 之一,首先让我感到惊讶的是这不受支持并被视为 "fancy").这也是为什么我收到的ObjectDisposedException是非常有道理的。

我仍然可以在这里访问 handle 属性,但是,我的 P/Invoke 方法不再接受 Window 实例,但是 "classic" IntPtr.

我再次为 P/invoke 参数使用 IntPtr 会更好吗?

我会这么说,我的最终实现看起来像这样并解决了上述两个问题,同时仍然使用 SafeHandle 的优点,只是没有花哨的 P/Invoke 参数替换。作为一个额外的功能,我仍然可以将 IntPtr 参数表示为 "accept" 一个 SDL_Window(指向的本机类型),并带有 using 别名。

using SDL_Window = System.IntPtr;

public class Window : SafeHandleZeroOrMinusOneIsInvalid
{
    private Window(IntPtr handle) : base(true)
    {
        SetHandle(handle);
    }

    public Window() : this(SDL_CreateWindow("Hello", 400, 400, 800, 600, 0)) { }

    protected override bool ReleaseHandle()
    {
        SDL_DestroyWindow(handle);
        return true;
    }

    [DllImport(_libName, CallingConvention = CallingConvention.Cdecl)]
    private static extern SDL_Window SDL_CreateWindow(
        [MarshalAs(UnmanagedType.LPStr)] string title, int x, int y, int w, int h, uint flags);

    [DllImport(_libName, CallingConvention = CallingConvention.Cdecl)]
    private static extern void SDL_DestroyWindow(SDL_Window window);
}