C# 将SafeHandles从非托管封送到托管

C# 将SafeHandles从非托管封送到托管,c#,interop,pinvoke,marshalling,C#,Interop,Pinvoke,Marshalling,在我正在编写的本机dll包装中,我刚刚用SafeHandles替换了IntPtr的所有用法来封送句柄。我的印象是,正确编写的SafeHandle类型可以通过这种方式与IntPtr互换 但是,我的Marshal.GetFunctionPointerForDelegate调用现在引发异常: Cannot marshal 'parameter #n': SafeHandles cannot be marshaled from unmanaged to managed. delegate void

在我正在编写的本机dll包装中,我刚刚用SafeHandles替换了IntPtr的所有用法来封送句柄。我的印象是,正确编写的SafeHandle类型可以通过这种方式与IntPtr互换

但是,我的Marshal.GetFunctionPointerForDelegate调用现在引发异常:

Cannot marshal 'parameter #n': SafeHandles cannot be marshaled from unmanaged to managed.
 delegate void CallBackType(MySafeHandle instance, int argument);
回调函数在参数列表中包含一个句柄,因此委托在其位置包含一个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);
然后:

这很好用,而且一直都是这样。然而,我想从IntPtr转移到safehandle来管理句柄,并读到这是一个替代品。但是,在上述C代码中用SafeHandle子类替换IntPtr会导致报告的异常:

Cannot marshal 'parameter #n': SafeHandles cannot be marshaled from unmanaged to managed.
 delegate void CallBackType(MySafeHandle instance, int argument);

嗯…只是想大声说出来,但我想你必须做一些内部包装;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);
}

这样,您仍然可以保留您的类型safety wrt-SafeHandle用法,同时让您以您想要的方式处理编组…

嗯…只是大声思考,但我认为您必须实现某种内部包装;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);
}

这样,您仍然可以保留类型safety wrt和SafeHandle用法,同时允许您以您希望的方式处理编组…

错误消息具有误导性。将safehandles从非托管封送到托管封送是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的原因是安全句柄是特殊的。每当您作为安全句柄封送时,CLR封送器会自动将不透明的IntPtr句柄转换为拥有该句柄的新CLR SafeHandle对象。请注意,安全句柄是对象,而不是结构

如果每次调用委托时都为OS句柄创建一个新的所有者对象,那么很快就会遇到很大的麻烦,因为一旦从委托返回,对象就会被垃圾收集


因此,我想也许编译器只是想把我们从这个错误中解救出来——用它自己令人困惑的方式?

错误信息是误导性的。将safehandles从非托管封送到托管封送是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的原因是安全句柄是特殊的。每当您作为安全句柄封送时,CLR封送器会自动将不透明的IntPtr句柄转换为拥有该句柄的新CLR SafeHandle对象。请注意,安全句柄是对象,而不是结构

如果每次调用委托时都为操作系统句柄创建了一个新的所有者对象,那么 这可能会有很大的麻烦,因为一旦您从委托返回,您的对象就会被垃圾收集

因此,我猜编译器可能只是想让我们避免这个错误——用它自己令人困惑的方式?

通过一些额外的步骤,可以将IntPtr转换为MySafeHandle,而不用使用ICustomMarshaler代理将委托转换为另一个委托

委托void CallBackTypeMySafeHandle实例,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。

通过一些额外的步骤,可以将IntPtr转换为MySafeHandle,而无需使用ICustomMarshaler将代理转换为另一个代理

委托void CallBackTypeMySafeHandle实例,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。

SafeHandle不是.NET的一个方面,而不是本机概念吗?您的互操作声明是什么样子的?SafeHandle确实是一个.NETside概念,就像IntPtr一样。将为我的原始问题添加更多细节。SafeHandle是一个抽象类。pinvoke封送处理程序无法从声明中选择正确的派生类类型,它只有一个IntPtr。对不起,我应该编写MySafeHandle。显然,我必须实现自己的SafeHandle,并使用适当的释放覆盖。我将对我的问题进行编辑以使其更清楚。SafeHandle不是一个与本机概念相反的.NET方面吗?您的互操作声明是什么样子的?SafeHandle确实是一个.NETside概念,就像IntPtr一样。将为我的原始问题添加更多细节。SafeHandle是一个抽象类。pinvoke封送处理程序无法从声明中选择正确的派生类类型,它只有一个IntPtr。对不起,我应该编写MySafeHandle。显然,我必须实现自己的SafeHandle,并使用适当的释放覆盖。我将对我的问题进行编辑以使其更清楚。我想接下来的困难在于,对我的库的回调是使用IntPtr而不是MySafeHandle进行的-因此,我需要维护所有MySafeHandle的某种缓存字典,以便在给定IntPtr的情况下检索正确的一个。@TomDavies呃,没有想到……我的意思是,这是一件小事,但很烦人……假设您可以将“缓存”烘焙到SafeHandle类中,甚至可以使用隐式运算符转换为IntPtr或从IntPtr转换为IntPtr…您可能还需要使用弱字典来实现它-这样就不会阻止安全句柄对象在使用完后被垃圾收集和最终确定。也可以使用弱GC句柄来代替弱字典,然后传递这些函数的int值……我想困难在于,对我的库的回调是用IntPtr而不是MySafeHandle进行的——所以我需要维护所有MySafeHandle的某种缓存字典,以便在给定IntPtr的情况下检索正确的一个。@TomDavies呃,没有想到这一点……我的意思是,这是一件小事,但很烦人……假设您可以将“缓存”烘焙到SafeHandle类中,甚至可以使用隐式运算符转换为IntPtr或从IntPtr转换为IntPtr…您可能还需要使用弱字典来实现它-这样就不会阻止安全句柄对象在使用完后被垃圾收集和最终确定。也可以使用弱GC句柄来代替弱字典,并传递这些的int值。。。