1

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

[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 签名中的类,如下所示:

[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参数/返回要好得多,因为我有类型安全Window向/从这些方法传递/检索实际(句柄)。

虽然这适用于SDL_CreateWindow,它现在正确返回一个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. 根据文档,它已设置为true因为“调用了 Dispose 方法或 Close 方法,并且在其他线程上没有对 SafeHandle 对象的引用。”,所以我猜Dispose之前在调用堆栈中隐式调用了我的ReleaseHandle.

ReleaseHandle如果我想在 p/invoke 签名中使用类参数,显然不是清理的正确位置,所以我想知道是否有任何方法可以在不破坏SafeHandle内部结构的情况下进行清理?

4

1 回答 1

1

我上面的问题被我了解到的错误信息稍微误导了SafeHandle(通过一些我不会提及的博客文章)。虽然有人告诉我IntPtr用类实例替换 P/Invoke 方法中的参数是“由”提供主要优势,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);
}

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

  • 方法返回的一个SDL_CreateWindow- 我不在任何地方使用(仅剥离handle属性)
  • 由我的用户代码调用创建的一个new Window()SafeHandle本身

实现SafeHandle此处的唯一正确方法是再次SDL_CreateWindow返回IntPtr,因此不再创建内部编组SafeHandle实例。

进不去SafeHandle_ReleaseHandle

正如 Simon Mourier 在评论中解释/引用的那样,SafeHandle在清理时它本身根本不能再使用ReleaseHandle,因为对象被垃圾收集并且试图做“花哨”的事情,比如将它传递给 P/Invoke 方法不再安全了/ 注定失败。(鉴于我被告知IntPtrP/Invoke 中的参数替换是 的“主要功能”之一SafeHandle,首先让我感到惊讶的是,这不受支持并被认为是“花哨的”)。这也是ObjectDisposedException我收到的非常有道理的原因。

我仍然可以在handle此处访问该属性,但是,我的 P/Invoke 方法不再接受Window实例,而是接受“经典” IntPtr

再次将 IntPtr 用于 P/invoke 参数是否会更好?

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

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);
}
于 2017-12-22T10:39:09.700 回答