无法将 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
,没想到这个时候是。显然它在内部尝试增加引用计数,但注意到 IsClosed
是 true
。根据 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_CreateWindow
return一个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);
}
我最近才了解到 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
,没想到这个时候是。显然它在内部尝试增加引用计数,但注意到 IsClosed
是 true
。根据 documentation,它已被设置为 true
因为 "The Dispose method or Close method was called and there are no references to the SafeHandle object on other threads.",所以我猜 Dispose
之前在调用堆栈中被隐式调用以调用我的 ReleaseHandle
.
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_CreateWindow
return一个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);
}