Маршалинг SafeHandles от неуправляемого к управляемому

В собственной оболочке dll, которую я пишу, я только что заменил все варианты использования IntPtr (для маршалинга дескрипторов) на SafeHandles. У меня сложилось впечатление, что правильно написанный тип SafeHandle взаимозаменяем с IntPtr таким образом.

Однако мои вызовы Marshal.GetFunctionPointerForDelegate теперь вызывают исключение:

Cannot marshal 'parameter #n': SafeHandles cannot be marshaled from unmanaged to managed.

Обратный вызов содержит дескриптор в списке аргументов, поэтому вместо него делегат содержит SafeHandle (вместо IntPtr, как раньше). Так я могу просто не делать этого? Если да, то каковы мои варианты использования SafeHandles, учитывая, что мне нужно маршалировать обратные вызовы?

Вот отредактированный пример собственного заголовка dll:

struct aType aType;
typedef void (*CallBackType)(aType*, int);
aType* create(); // Must be released
void   release(aType* instance);
int    doSomething(aType* instance, int argumnet);
void   setCallback(CallbackType func);

Бит, который вызывает у меня проблемы, - это обратный вызов. Сторона C# выглядела так:

delegate void CallBackType(IntPtr instance, int argument);

Потом:

var funcPtr = Marshal.GetFunctionPointerForDelegate(del = new CallbackType(somefunc)):

NativeFunction.setCallback(funcPtr)

Это отлично работает, и всегда так было. Однако я хотел перейти от IntPtr для управления дескриптором к safehandle и прочитал, что это замена. Однако замена IntPtr подклассом SafeHandle в приведенном выше коде C# вызывает сообщение об исключении:

 delegate void CallBackType(MySafeHandle instance, int argument);

person Tom Davies    schedule 24.01.2013    source источник
comment
Разве SafeHandle не является концепцией стороны .NET (в отличие от родной)? Как выглядят ваши объявления взаимодействия?   -  person 500 - Internal Server Error    schedule 25.01.2013
comment
SafeHandle действительно является концепцией .NETside (как и IntPtr). Добавлю больше деталей к моему первоначальному вопросу.   -  person Tom Davies    schedule 25.01.2013
comment
SafeHandle — это абстрактный класс. Нет никакого способа, которым маршаллер pinvoke мог бы выбрать правильный тип производного класса из объявления, все, что у него есть, это IntPtr.   -  person Hans Passant    schedule 25.01.2013
comment
Извините, я должен написать MySafeHandle. Очевидно, мне пришлось реализовать свой собственный SafeHandle с соответствующим переопределением Release. Я отредактирую свой вопрос, чтобы сделать его более понятным.   -  person Tom Davies    schedule 25.01.2013


Ответы (3)


Сообщение об ошибке вводит в заблуждение. 100% возможно маршалировать безопасные дескрипторы из неуправляемых в управляемые, потому что именно так SafeHandles должны создаваться. Например, посмотрите, как определяется CreateFile:

[DllImport("kernel32.dll", CharSet=CharSet.Auto, SetLastError=true)]
private static extern SafeFileHandle CreateFile(string lpFileName, int dwDesiredAccess, 
    FileShare dwShareMode, SECURITY_ATTRIBUTES securityAttrs,
    FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr hTemplateFile);

Причина, по которой компилятор генерирует сообщения об ошибках, заключается в том, как вы объявляете делегата. Я сделал ту же ошибку, что и вы, и попытался использовать тип MySafeHandle в качестве параметра делегата, когда я объявил свой делегат обратного вызова (здесь неуправляемый код будет вызывать ваш управляемый код):

delegate void TimerCallback(IntPtr pCallbackInstance, IntPtr context, MySafeHandle ptpTimer);

Для чего я получил точно такое же сообщение об ошибке, как и у вас. Однако, как только я изменил свою подпись делегата на IntPtr, ошибка исчезла, поэтому мы видим, что наша наивная интуиция неверна...

delegate void TimerCallback(IntPtr pCallbackInstance, IntPtr context, IntPtr ptpTimer);

Видите, вуаля, ошибка исчезла! Теперь нам просто нужно решить, как использовать IntPtr, который входит в делегат, для поиска правильного объекта MySafeHandle...!

Как только я выясню, какое изменение устранило ошибку, я также смогу придумать теорию относительно того, почему оно устраняет ошибку.

Теория: (непроверенная)

Причина, по которой вы должны использовать IntPtr в подписи делегата, заключается в том, что SafeHandles являются особыми. Всякий раз, когда вы маршалируете как SafeHandle, маршаллер CLR автоматически преобразует непрозрачный дескриптор IntPtr в новый объект CLR SafeHandle, которому владеет рассматриваемый HANDLE. (Обратите внимание, что SafeHandles — это объекты, а не структуры!)

Если бы вы создавали новый объект-владелец для OS HANDLE каждый раз, когда вызывается делегат, у вас скоро были бы очень большие проблемы, потому что ваш объект будет собирать мусор, как только вы вернетесь из делегата!

Так что я предполагаю, что, возможно, компилятор просто пытается спасти нас от этой ошибки - в своей запутанной формулировке?

person Tim Lovell-Smith    schedule 28.01.2014

Хм ... просто мысли вслух, но я думаю, вам придется применить какую-то промежуточную обертку; SafeHandle работает с реализацией P/invoke во время базовой сортировки, но не с "ручной сортировкой", как вы здесь... попробуйте что-нибудь подобное, может быть?

internal delegate void InnerCallbackType(IntPtr instance, int argument);
public delegate void MyCallBackType(MySafeHandle instance, int argument);

public void SetCallback(Action<MySafeHandle, int> someFunc) 
{
    InnerCallbackType innerFunc = (rawHandle, rawArg) => 
    {
        someFunc(new MySafeHandle(rawHandle, true), rawArg);
    };
    var funcPtr = Marshal.GetFunctionPointerForDelegate(innerFunc);
    NativeFunction.setCallback(funcPtr);
}

Таким образом, вы все равно сохраните свою «безопасность типов» по ​​отношению к использованию SafeHandle, позволяя вам обрабатывать сортировку так, как вы хотите...

person JerKimball    schedule 25.01.2013
comment
Я предполагаю, что сложность заключается в том, что обратный вызов моей библиотеки выполняется с помощью IntPtr, а не MySafeHandle, поэтому мне нужно будет поддерживать какой-то кеш-словарь всех MySafeHandle, чтобы получить правильный, учитывая IntPtr. - person Tom Davies; 25.01.2013
comment
@TomDavies тьфу, не подумал об этом ... Я имею в виду, что это было бы тривиально, но раздражает ... предположим, вы могли бы испечь «кэш» в свой класс SafeHandle, может быть, даже с implicit operator для преобразовать в/из IntPtr... - person JerKimball; 25.01.2013
comment
Вам, вероятно, также необходимо реализовать его с использованием слабого словаря, чтобы он не мешал вашим объектам безопасного дескриптора собирать мусор и финализировать их после того, как вы закончите их использовать. - person Tim Lovell-Smith; 06.02.2015
comment
Также возможно вместо использования слабого словаря использовать слабые дескрипторы GC и передать значение int этих... - person Tim Lovell-Smith; 06.02.2015

Возможно с некоторыми дополнительными шагами превратить IntPtr в MySafeHandle без прокси-передачи делегата в другого с помощью ICustomMarshaler.

делегат недействителен CallBackType (экземпляр MySafeHandle, аргумент int);

так что давайте сделаем с ним какую-нибудь причудливую штуку:

представьте, что мы передаем такой делегат в неуправляемую среду с соглашением о вызовах __cdecl (просто для примера):

[DllImport("some.dll", ExactSpelling = true, CallingConvention = CallingConvention.Cdecl)]
public static extern void setCallback([MarshalAs(UnmanagedType.FunctionPtr)] CallBackType callBackType);

а затем мы хотим украсить нашего делегата:

[UnmanagedFunctionPointer(CallingConvention.Cdecl)] // depends on unmanaged implementation, but for the example sake
delegate void CallBackType([MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(MyCustomMarshaler))] MySafeHandle instance, int argument);

и, наконец, реализация ICustomMarshaler:

public sealed class MyCustomMarshaler : ICustomMarshaler
{
    private static MyCustomMarshaler _instance = new MyCustomMarshaler();

    public void CleanUpManagedData(object o)
    {
    }

    public void CleanUpNativeData(IntPtr ptr)
    {
    }

    public int GetNativeDataSize()
    {
        return IntPtr.Size;
    }

    public IntPtr MarshalManagedToNative(object o)
    {
        return IntPtr.Zero;
    }

    public object MarshalNativeToManaged(IntPtr ptr)
    {
        return new MySafeHandle()
        {
            handle = ptr
        };
    }

    public static ICustomMarshaler GetInstance(string s)
    {
        return _instance;
    }
}

Я опубликовал ответ, потому что ничего не нашел об этом в Интернете (вне зависимости от того, безопасно это или нет, или почему маршалинг не автоматически превращает IntPtr в SafeHandle).

person Eric Liu    schedule 17.11.2019