Не удается передать экземпляр SafeHandle в ReleaseHandle нативному методу

Я только недавно узнал о 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 является true, Согласно документации было установлено true потому что "Метод Dispose или метод Close был вызван, и в других потоках нет ссылок на объект SafeHandle". Dispose ранее неявно вызывался в стеке вызовов для вызова моего ReleaseHandle,

ReleaseHandle по-видимому, это не правильное место для очистки, если я хочу использовать параметр класса в сигнатуре p/invoke, поэтому мне интересно, есть ли какой-нибудь метод, где я мог бы очистить, не нарушая SafeHandle Внутренности?

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);
}

Когда маршаллер вызывает метод P/Invoke SDL_CreateWindow в Window конструктор, он внутренне создает другой экземпляр Window класс для возвращаемого значения (вызов требуемого конструктора без параметров и затем установка handle член внутри). Это означает, что у меня теперь есть два экземпляра SafeHandle:

  • один вернулся SDL_CreateWindow метод - который я нигде не использую (только зачистка handle имущество)
  • один созданный моим кодом вызова пользователя new Window(), SafeHandle сам класс

Единственно правильный способ реализации SafeHandle здесь, чтобы позволить SDL_CreateWindow вернуть IntPtr опять же, поэтому нет внутренней сортировки SafeHandle экземпляры создаются больше.

Не могу пройти SafeHandle вокруг в ReleaseHandle

Как объяснял / цитировал в комментариях Саймон Мурье, SafeHandle сам по себе больше не может быть использован при очистке в ReleaseHandle, поскольку объект является сборщиком мусора, и попытка сделать "причудливые" вещи, такие как передача его методу P/Invoke, больше не является безопасной / обречена на неудачу. (Учитывая, что мне сказали, что замена IntPtr Параметры в P/Invoke - это одна из "основных функций" SafeHandle, меня сначала удивило, что это не поддерживается и считается "модным"). Вот почему ObjectDisposedException Я получаю это очень оправданно.

Я все еще могу получить доступ к handle свойство здесь, но, опять же, мой метод P/Invoke больше не принимает Window экземпляр, но "классический" IntPtr,

Мне лучше снова использовать IntPtr для параметров P/invoke?

Я бы сказал, что моя окончательная реализация выглядит следующим образом и решает две вышеупомянутые проблемы, все еще используя преимущества SafeHandle Просто без причудливых замен аргументов P/Invoke. И как дополнительная функция, я все еще могу связать параметры IntPtr, чтобы "принять" 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);
}
Другие вопросы по тегам