C# 中的泛型 PInvoke

本文关键字:PInvoke 泛型 | 更新日期: 2023-09-27 18:30:03

我正在与具有以下形式的一些函数的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 获取指针并将其传递到一个需要IntPtr的基本GetInfo:效果很好,但需要对枚举进行特殊处理,因为遗憾的是它们不是可 blit(是的,我知道我可以简单地GetInfo<[underlying enum type]>并投射到枚举,但这违背了目的,因为您无法在没有反射的情况下调用具有运行时确定类型的泛型方法(并且字符串也需要特殊代码

所以我最终认为,也许正好有三个重载,即GetInfoBlittable<T>GetInfoEnum<T>GetInfoStr,将是代码复制和反射巫毒教之间的最佳折衷方案。但是,有没有更好的方法?有没有更好的方法来尽可能接近第一个GetInfo<T>片段?理想情况下,无需切换类型。感谢您的帮助!


FWIW,总的来说,我需要用intlonguintulongstringstring[]UIntPtrUIntPtr[]、枚举类型、可拼结构体以及可能的string[][]来完成这项工作。

C# 中的泛型 PInvoke

  1. 当你使用诸如 intlong 、...等结构时,您可以使用 Marshal.SizeOf 来获取大小和new IntPtr(&GCHandle.Alloc(...))
  2. 当你使用枚举时,你可以使用.GetEnumUnderlyingType()来获取其中的原始类型,并获取值,获取它的字段名为value__ by 反射,并在枚举对象上使用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. 用 int、long 等ValueType制作数组。您需要执行类似于字符串的交付方法的操作。
  2. 要从string中制作数组,您需要进行多分配和一些繁琐的工作。(代码看起来很原生(

由于大多数搜索"通用 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);
     }
    

请注意,仅当至少ref一个参数或outExpression.GetDelegateType才会返回非泛型类型。否则,它将返回 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);
    }