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

Posted

技术标签:

【中文标题】无法将 ReleaseHandle 中的 SafeHandle 实例传递给本机方法【英文标题】:Cannot pass SafeHandle instance in ReleaseHandle to native method 【发布时间】:2017-12-21 23:51:59 【问题描述】:

我最近才了解到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。根据documentation,它已设置为true,因为“调用了Dispose方法或Close方法,并且没有引用其他线程上的SafeHandle对象。”,所以我猜Dispose之前被隐式调用过在调用堆栈中调用我的ReleaseHandle

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

【问题讨论】:

您不应将SDL_DestroyWindow 中的IntPtr 更改为Window 是的,但是我失去了类型安全性。我的问题是是否仍然可以在不丢失特定签名的情况下进行清理。 这里的“类型安全”是什么意思?在 ReleaseHandle 中,您应该使用受保护的成员 handle,而不是 this,因此释放句柄的 p/invoke 方法必须使用 IntPtr 参数。这是最后一次调用,文档说垃圾收集器在为同时被垃圾收集的对象运行正常终结器后调用 ReleaseHandle,你不应该失败或做一些花哨的事情 我的意思是我可以通过在签名中使用类而不是 IntPtr 来实现“类型安全”(IntPtr 可能指向对参数没有意义的结构,而 SafeHandle基于类确保我使用指向正确不透明类型的指针)。无论如何,正如您所确认的,使用 SafeHandle 类的这个“功能”似乎确实不适用于清理调用,我认为这将是使用此类“优势”的一部分。我在上面的代码中也意识到了一个严重的问题,所以如果你愿意,我可以指出来回答,或者接受你的评论作为答案。 Microsoft 的含义与 SafeHandle 中的“安全”一词根本不同。与类型安全无关。这是关于在执行互操作调用时句柄不会变得无效,当终结器失败时不会使程序崩溃并且不能被句柄回收攻击利用。它仅在引用计数句柄上真正有用,而不是窗口句柄。 【参考方案1】:

我上面的问题被我了解到的关于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);

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

SDL_CreateWindow 方法返回的一个 - 我不会在任何地方使用它(只去除了 handle 属性) 由我的用户代码调用new Window() 创建的SafeHandle 类本身

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

无法在ReleaseHandle 中传递SafeHandle

正如 Simon Mourier 在 cmets 中解释/引用的那样,SafeHandle 本身在清理 ReleaseHandle 时根本无法再使用,因为该对象已被垃圾收集并试图做“花哨”的事情,例如将其传递给P/Invoke 方法不再安全/注定要失败。 (假设我被告知在 P/Invoke 中替换 IntPtr 参数是 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);

【讨论】:

我知道这是一个老问题,但我只是想重申一下,这确实是一种奇怪的行为。能够将其作为最终版本的一部分传递给发布功能,感觉是一件非常明显的事情。

以上是关于无法将 ReleaseHandle 中的 SafeHandle 实例传递给本机方法的主要内容,如果未能解决你的问题,请参考以下文章

Dlib 错误:错误 C1083:无法打开包含文件:'type_safe_union/type_safe_union_kernel.h'

无法在 linux 上启动 mysqld_safe

ImportError:无法导入名称“_safe_split”

YellowBrick ImportError:无法从“sklearn.utils”导入名称“safe_indexing”

mysqld_safe 无法启动的原因

收到此错误:错误:捆绑失败:错误:无法解析模块`react-native-safe-area-context`