代码之家  ›  专栏  ›  技术社区  ›  codesniffer

通过PInvoke实现与尺寸互操作的建议

  •  1
  • codesniffer  · 技术社区  · 6 年前

    我们有一个主要使用C/C++的本机代码SDK。 size_t 键入数组大小之类的内容。此外,我们还提供了一个.NET包装器(用C#编写),它使用PInvoke来调用本机代码,供那些希望将我们的SDK集成到其.NET应用程序中的用户使用。

    .NET具有 System.UIntPtr 完美搭配的类型 尺寸 在功能上,一切都按预期工作。提供给本机端的一些C#结构包含 系统.UIntPtr 类型,它们暴露给.NETAPI的使用者,而.NETAPI要求它们使用 系统.UIntPtr 类型。问题是 系统.UIntPtr 无法与.NET中的典型整数类型进行良好的互操作。强制转换是必需的,而且各种“基本”的东西,如与整数/文字的比较,如果不进行更多的强制转换,就无法工作。

    我们试着申报出口 尺寸 参数为 uint 并应用 MarshalAsAttribute(UnmanagedType.SysUInt) 但这会导致无效封送处理的运行时错误。例如:

    [DllImport("Native.dll", EntryPoint = "GetVersion")]
    private static extern System.Int32 GetVersion(
        [Out, MarshalAs(UnmanagedType.LPStr, SizeParamIndex = 1)]
        StringBuilder strVersion,
        [In, MarshalAs(UnmanagedType.SysUInt)]
        uint uiVersionSize
    );
    

    在C中调用GetVersion;为第二个参数传递uint会在运行时导致此封送处理错误:

    System.Runtime.InteropServices.MarshalDirectiveException: Cannot marshal 'parameter #2': Invalid managed/unmanaged type combination (Int32/UInt32 must be paired with I4, U4, or Error).
    

    我们可以创建facade包装器,在.NET中公开“int”类型,并在内部对 系统.UIntPtr 对于本机兼容类,但是(a)我们担心在几乎重复的类之间复制缓冲区(可能非常大)的性能,以及(b)这是一堆工作。

    有什么建议吗 尺寸 在.NET中维护方便的API时键入?


    下面是一个例子,它与我们的真实代码实际上是一样的,但是有简化/剥离的名称。 注意 此代码是从我们的手工生产代码派生出来的。它为我编译,但我没有运行它。

    本机代码(C/C++):

    #ifdef __cplusplus
    extern "C"
    {
    #endif
    
    
    enum Flags
    {
        DEFAULT_FLAGS = 0x00,
    
        LEVEL_1 = 0x01,
    };
    
    
    struct Options
    {
        Flags flags;
    
        size_t a;
    
        size_t b;
    
        size_t c;
    };
    
    
    int __declspec(dllexport) __stdcall InitOptions(
        Options * const pOptions)
    {
        if(pOptions == nullptr)
        {
            return(-1);
        }
    
        pOptions->flags = DEFAULT_FLAGS;
        pOptions->a = 1234;
        pOptions->b = static_cast<size_t>(0xFFFFFFFF);
        pOptions->c = (1024 * 1024 * 1234);
    
        return(0);
    }
    
    
    #ifdef __cplusplus
    }
    #endif
    

    托管(C)代码: (这个 应该 对错误的编组进行重新编码。将结构中的字段a、b和c更改为uintpttr类型可以使其正常工作。

    using System;
    using System.Runtime.InteropServices;
    
    namespace Test
    {
        public enum Flags
        {
            DEFAULT_FLAGS = 0x00,
    
            LEVEL_1 = 0x01,
        }
    
    
        [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
        public struct Options
        {
            public Flags flags;
    
            public uint a;
    
            public uint b;
    
            public uint c;
        }
    
    
        public class Test
        {
            [DllImport("my.dll", EntryPoint = "InitOptions", CallingConvention = CallingConvention.StdCall)]
            internal static extern Int32 InitOptions(
                [In, Out]
                ref Options options
            );
    
            static void Main(string[] args)
            {
                Options options = new Options
                {
                    flags = DEFAULT_FLAGS,
                    a = 111,
                    b = 222,
                    c = (1024 * 1024 * 1)
                };
    
                Int32 nResultCode = InitOptions(
                    ref options
                );
    
                if(nResultCode != 0)
                {
                    System.Console.Error.WriteLine("Failed to initialize options.");
                }
    
                if(   options.flags != DEFAULT_FLAGS
                    || options.a != 1234
                    || options.b != static_cast<size_t>(-1)
                    || options.c != (1024 * 1024 * 1234) )
                {
                    System.Console.Error.WriteLine("Options initialization failed.");
                }
            }
        }
    
    }
    

    我试图将托管结构中的枚举字段更改为in t类型,但它仍然不起作用。

    下一步我将用size_t函数参数测试更多。

    2 回复  |  直到 6 年前
        1
  •  0
  •   Simon Mourier    6 年前

    相当于 size_t IntPtr (或 UIntPtr ). 但是对于参数,您可以使用 int uint 没有任何附加属性。

    所以,如果你在C/C++中有这个:

    int InitOptions(size_t param1, size_t param2);
    

    然后您可以用C#声明它,它将在x86和x64上工作(好吧,您不会得到任何高于32的位值,当然,hi uint会丢失):

    [DllImport("my.dll")]
    static extern int InitOptions(int param1, int param2); // or uint
    

    对于x86来说,它是有效的,因为,嗯,它应该是这样的。

    对于x64,它神奇地工作,因为参数是 always 64-bit 幸运的是,额外的hi位被errrhh归零。。。系统的一些组件(CLR?C/C++编译器?我不确定)。

    对于struct字段这是一个完全不同的故事,最简单的(对我来说)似乎使用IntPtr并添加一些帮助程序来简化编程。

    但是,如果您真的想为使用您的结构的开发人员添加一些糖,我已经添加了一些额外的示例代码。重要的是,这个代码可以从C/C++定义中生成。

    public static int InitOptions(ref Options options)
    {
        if (IntPtr.Size == 4)
            return InitOptions32(ref options);
    
        Options64 o64 = options;
        var i = InitOptions64(ref o64);
        options = o64;
        return i;
    }
    
    [DllImport("my64.dll", EntryPoint = "InitOptions")]
    private static extern int InitOptions64(ref Options64 options);
    
    [DllImport("my32.dll", EntryPoint = "InitOptions")]
    private static extern int InitOptions32(ref Options options);
    
    [StructLayout(LayoutKind.Sequential)]
    public struct Options // could be class instead (remove ref)
    {
        public Flags flags;
        public uint a;
        public uint b;
        public uint c;
    
        public static implicit operator Options64(Options value) => new Options64 { flags = value.flags, a = value.a, b = value.b, c = value.c };
    }
    
    [StructLayout(LayoutKind.Sequential)]
    public struct Options64 // could be class instead (remove ref)
    {
        public Flags flags;
        public ulong a;
        public ulong b;
        public ulong c;
    
        public static implicit operator Options(Options64 value) => new Options { flags = value.flags, a = (uint)value.a, b = (uint)value.b, c = (uint)value.c };
    }
    

    注意,如果对选项和选项64使用类而不是结构,则可以删除 ref 参数指示并避免从结构中进行痛苦的复制(运算符重载不能很好地处理 裁判 ). 但这还有其他含义,所以这取决于你。

    下面是关于同一主题的另一个讨论: C# conditional compilation based on 32-bit/64-bit executable target

    基本上,您还可以对x86和x64目标使用条件编译常量,并让您的代码使用这些常量。

        2
  •  0
  •   codesniffer    6 年前

    我最后做的是:

    首先是一些目标:

    1. 向.NET库用户公开.NET友好类型和习惯类型。
    2. 避免在与本机代码进行互操作时数据会自动丢失。
    3. 避免将32位/64位差异传播给.NET库用户(换句话说,避免由于底层的本机DLL位而在.NET API之外产生类型差异;争取使用一种(主要)隐藏位问题的数据类型)。
    4. 最好将32-vs-64位的独立结构和/或代码路径最小化。
    5. 当然,所有开发人员都喜欢(编写和维护的代码更少,更容易保持同步,等等)。

    功能

    从DLL导出的C函数以尽可能接近本机(C)类型的.NET类型显示在DllImport中。然后每个函数都用一个更内联的-NET facade包装。

    这完成了两件事:

    1. 在DllImport中保留本机类型可以避免 沉默的 (!) 数据丢失。正如Simon Mourier指出的,.NET可以使用 uint 在里面 地点 size_t 在功能上。虽然这似乎有效,但它也 会自动删除超出范围的数据。所以如果本地代码 返回一个大于uint.MaxValue的值,.NET代码永远不会 知道。我宁愿处理这种情况,也不愿有一些虚假的错误。
    2. 特定于C和/或的各种技术和类型 非面向对象的呈现方式更适合.NET。 例如,C API中以字节表示的缓冲区 指针加上大小参数在 .NET。另一个例子是以非零结尾的字符串(例如UTF、XML) 在.NET中显示为字符串或Xml对象,而不是字节 数组和大小参数。

    专门为 尺寸 函数参数,它们表示为 UIntPtr 在DllImport中(根据上面的1),如果仍然需要向库用户公开,它们将显示为 无符号整型 ulong 如适用。然后,facade验证每个(in/out,视情况而定)的值,如果 有一种不相容。

    下面是一个使用伪代码的示例:

    C函数:

    // Consume & return data in buf and pBufSize
    int __declspec(dllexport) __stdcall Foo(
        byte * buf,
        size_t * pBufSize
    );
    

    C#DllImport公司:

    [DllImport("my.dll", EntryPoint = "Foo", CallingConvention = CallingConvention.StdCall)]
    private static extern System.Int32 Foo(
        [In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)]
        System.Byte[] buf,
        [In, Out]
        ref System.UIntPtr pBufSize
    );
    

    C#门面(伪代码):

    void Foo(System.Byte[] buf)
    {
        // Verify buffer size will fit
        if buf.LongLength > UIntPtrMaxValue
            throw ...
    
        UIntPtr bufSize = buf.LongLength;
    
        Int32 nResult = Foo(
            buf,
            bufSize
        );
    
        if nResult == FAILURE
            throw ...
    
        // Verify return size is valid
        if (UInt64)bufSize > int.MaxValue   // .NET array size type is 'int'
            throw ...
    
        buf.resize((int)bufSize);
    }
    

    结构

    与包含 尺寸 (甚至在一般情况下),我采用了与函数类似的方法:创建一个与本机代码结构最相似的.NET结构(“Interop结构”),然后在其周围放置一个.NET友好的外观。然后,facade根据需要执行值检查。

    我对facade采取的具体实现方法是将每个字段设置为一个属性,并将Interop结构作为后备存储。下面是一个小例子:

    C结构:

    struct Bar
    {
        MyEnum e;
        size_t s;
    }
    

    C#(伪代码):

    public class Bar
    {
        // Optional c'tor if param(s) are required to be initialized for typical use
    
        // Accessor for e
        public MyEnum e
        {
            get
            {
                return m_BarInterop.e;
            }
            set
            {
                m_BarInterop.e = value;
            }
        }
    
        // Accessor for s
        public uint s
        {
            get
            {
                VerifyUIntPtrFitsInUint(m_BarInterop.s);   // will throw an exception if value out of range
                return (uint)m_BarInterop.s;
            }
            set
            {
                // uint will always fit in UIntPtr
                m_BarInterop.s = (UIntPtr)value;
            }
        }
    
        // Interop-compatible 'Bar' structure (not required to be inner struct)
        [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
        internal struct Bar_Interop
        {
            public MyEnum e;
            public System.UIntPtr s;
        }
    
        // Instance of interop-compatible 'Bar' structure
        internal Bar_Interop m_BarInterop;
    }
    

    虽然有时有点乏味,但我发现,到目前为止,这种方法只使用了2种结构,所以它产生了很大的灵活性,并将一个干净的API暴露在我的.NET包装器的消费者中。