Не удается передать экземпляр 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);
}