【问题标题】:Generic PInvoke in C#C# 中的通用 PInvoke
【发布时间】:2014-11-12 08:37:45
【问题描述】:

我正在与具有以下形式的一些函数的 C API 交互:

int get_info(/* stuff */, size_t in_size, void* value, size_t *out_size);

这是一个著名的 C 习惯用法,从单个函数返回一堆不同类型的数据:in_size 参数包含传入value 的缓冲区大小,以及实际写入的字节数通过out_size指针写入,调用者可以在value缓冲区中读回它的数据。

我正在尝试从 C# 中很好地调用这些类型的函数(即,无需为每种不同的类型进行重载,而不必单独调用它们,这很丑陋并引入了很多重复的代码)。所以我很自然地尝试了这样的事情:

[DllImport(DllName, EntryPoint = "get_info")]
int GetInfo<T>(/* stuff */, UIntPtr inSize, out T value, out UIntPtr outSize);

令我惊讶的是,它在 Mono 中编译并完美运行。不幸的是,我的希望很快就被 VS 不想编译它所打消,事实上,泛型方法被禁止作为 DllImport 目标。但这基本上是我正在寻找的。我尝试了几件事:

  • 使用反射基于泛型类型动态生成适当的 PInvoke 目标:绝对糟糕,而且非常容易出错(也失去了自动编组提供的所有好处)

  • 按类型有一个重载(GetInfoIntGetInfoStr、..):很快就会失控

  • 有一个通用方法使用GCHandle.Alloc 获取指针并将其传递给一个基本的GetInfo,它需要一个IntPtr:效果很好,但需要对枚举进行特殊处理,因为遗憾的是它们不是 blittable (是的,我知道我可以简单地GetInfo&lt;[underlying enum type]&gt; 并强制转换为枚举,但这违背了目的,因为你不能调用具有运行时确定类型的泛型方法而不需要反射)并且字符串也需要特殊代码

所以我最终认为可能恰好有三个重载,GetInfoBlittable&lt;T&gt;GetInfoEnum&lt;T&gt;GetInfoStr 将是代码重复和反射巫术之间的最佳折衷。但是,有没有更好的方法?有没有更好的方法来尽可能接近第一个GetInfo&lt;T&gt; sn-p?理想情况下不需要切换类型。感谢您的帮助!


FWIW,总的来说,我需要使用intlonguintulongstringstring[]UIntPtrUIntPtr[]、枚举类型来完成这项工作, blittable 结构,可能还有string[][]

【问题讨论】:

标签: c# interop dllimport


【解决方案1】:
  1. 使用intlong、...等结构体时,可以使用Marshal.SizeOf获取大小,使用new IntPtr(&amp;GCHandle.Alloc(...))
  2. 当您使用枚举时,您可以使用.GetEnumUnderlyingType() 来获取其中的原始类型,并通过反射获取名为value__ 的字段并在枚举对象上使用GetValue,您将收到它。
  3. 当你使用字符串时,你可以用它创建数组并给它指针。

我做了一个测试,所以你可以理解它:

internal class Program {
    public unsafe static int GetInfo(IntPtr t,UIntPtr size) {
        if(size.ToUInt32( ) == 4)
            Console.WriteLine( *( int* )t.ToPointer( ) );
        else //Is it our string?
            Console.WriteLine( new string( ( char* )t.ToPointer( ) ) );
        return 1;
    }
    public static unsafe int ManagedGetInfo<T>(T t) {
        if (t.GetType().IsEnum) {
            var handle = GCHandle.Alloc( t.GetType( ).GetField( "value__" ).GetValue( t ), GCHandleType.Pinned );
            var result = GetInfo( handle.AddrOfPinnedObject( ), new UIntPtr( (uint)Marshal.SizeOf( t.GetType().GetEnumUnderlyingType() ) ) );
            handle.Free( );
            return result;
        }
        else if (t.GetType().IsValueType) {
            var handle = GCHandle.Alloc( t, GCHandleType.Pinned );
            var result = GetInfo( handle.AddrOfPinnedObject( ), new UIntPtr( ( uint )Marshal.SizeOf( t ) ) );
            handle.Free( );
            return result;
        }
        else if (t is string) {
            var str = t as string;
            var arr = ( str + "\0" ).ToArray( );
            fixed (char *ptr = &arr[0])
            {
                return GetInfo( new IntPtr( ptr ), new UIntPtr( ( uint )( arr.Length * Marshal.SizeOf( typeof(char) ) ) ) );
            }
        }
        return -1;
    }
    enum A {
       x,y,z
    }
    private static void Main( ) {
        string str = "1234";
        int i = 1234;
        A a = A.y;
        Console.WriteLine( "Should print: " + str );
        ManagedGetInfo( str );
        Console.WriteLine( "Should print: " + i );
        ManagedGetInfo( i );
        Console.WriteLine( "Should print: " + ( int )a );
        ManagedGetInfo( a );
    }
}

哪些输出:

Should print: 1234
1234
Should print: 1234
1234
Should print: 1
1

注意:您需要在项目的属性中启用不安全代码才能对其进行测试。

为了制作数组,我会给你提示:

  1. 要使用ValueType 制作数组,例如int、long 等。你需要做一些类似于string 的传递方法。
  2. 要从string 中创建数组,您需要进行多重分配和一些繁琐的工作。 (代码看起来很原生)

【讨论】:

  • 不知道t.GetType( ).GetField( "value__" ).GetValue( t ),这似乎很有用,谢谢。会尝试并回复您!
  • 这工作起来很舒服,我也可以使用委托为所有此类功能编写一次,这非常酷。感谢您的回答!
【解决方案2】:

由于大多数搜索“通用 P/Invoke”的人可能会来这里,我想我可以分享一个有趣的解决方案,它适用于 OP 的情况,并且任何情况下都需要 refout 参数。

即使不直接支持通用 P/Invoke,我们仍然可以使用通用委托和 Marshal.GetDelegateForFunctionPointer 来模拟它。

很遗憾,GetDelegateForFunctionPointer 不接受泛型类型,所以我们需要:

  • 使用反射调用GetDelegateForFunctionPointerInternal(这很糟糕,因为它是不受支持的实现细节),或者

  • 从泛型生成非泛型委托类型,可以使用Expression.GetDelegateType 完成,如下所示:

     public static Type GetNonGenericDelegateType<T>() where T: Delegate
     {
         var method = typeof(T).GetMethod("Invoke");
         var types = method.GetParameters()
             .Select(p => p.ParameterType)
             .Concat(new[] { method.ReturnType })
             .ToArray();
    
         return Expression.GetDelegateType(types);
     }
    

请注意Expression.GetDelegateType 只会返回非泛型类型如果至少有一个参数是 ref out。否则,它返回System.FuncSystem.Action 的实例。

然后,在获得非泛型委托后,您可以将其Invoke 方法绑定到泛型的实例:

    //Converts an unmanaged function pointer to a generic delegate of the specified type.
    public static T GetGenericDelegateForFunctionPointer<T>(IntPtr ptr) where T: Delegate
    {
        var delegateType = GetNonGenericDelegateType<T>();
        var delegateInstance = Marshal.GetDelegateForFunctionPointer(ptr, delegateType);
        return (T)Delegate.CreateDelegate(typeof(T), delegateInstance, "Invoke");
    }

最后,对于 OP 的情况,代码将简化为:

    IntPtr GetInfoPtr = GetProcAddress(DllName, "get_info");
    delegate int GetInfoDelegate<T>(/* stuff */, UIntPtr inSize, out T value, out UIntPtr outSize);

    int GetInfo<T>(/* stuff */, UIntPtr inSize, out T value, out UIntPtr outSize)
    {
        var getInfo = GetGenericDelegateForFunctionPointer<GetInfoDelegate<T>>(GetInfoPtr);
        return getInfo(/* stuff */, inSize, out value, out outSize);
    }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-26
    • 2010-12-16
    • 1970-01-01
    • 2023-04-05
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多