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

时间:2017-12-21 23:51:59

标签: c# pinvoke marshalling safehandle

我刚刚了解了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属性是IsClosed,我此时并不期望这样做。显然,它会在内部尝试增加引用计数,但通知truetrue。根据{{​​3}},它被设置为Dispose,因为“调用了Dispose方法或Close方法,并且在其他线程上没有对SafeHandle对象的引用。”所以我猜{{1}之前在调用堆栈中隐式调用,以调用我的ReleaseHandle

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

ReleaseHandle显然不是正确的清理位置,所以我想知道是否有任何方法可以清除而不会破坏{{ 1}}内部结构?

1 个答案:

答案 0 :(得分:0)

我上面的问题稍微被我从SafeHandle学到的错误信息误导了(通过一些我不会提及的博客文章)。虽然我被告知在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);
}

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

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

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

无法在SafeHandle

中传递ReleaseHandle

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

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

我最好再使用IntPtr获取P / invoke参数吗?

我会这样说,我的最终实现看起来像这样并且在仍然使用SafeHandle的优点的同时解决了上述两个问题,只是没有花哨的P / Invoke参数替换。作为一个额外的功能,我仍然可以使用using别名来暗示IntPtr参数“接受”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);
}