zoukankan      html  css  js  c++  java
  • C#使用struct直接转换下位机数据

    编写上位机与下位机通信的时候,涉及到协议的转换,比较多会使用到二进制。传统的方法,是将数据整体获取到byte数组中,然后逐字节对数据进行解析。这样操作工作量比较大,对于较长数据段更容易计算位置出错。

    其实,对于下位机给出通讯的数据结构的情况下,可以直接使用C#的struct将数据直接转换。需要使用到Marshal

    数据结构

    假定下位机(C语言编写)给到我们的数据结构是这个,传输方式为小端方式

    typedef struct {
    	unsigned long int time;          // 4个字节
    	float tmpr[3];                   //  4*3 个字节
    	float forces[6];                 //  4*6个字节
    	float distance[6];               // 4*6个字节
    } dataItem_t;
    

    方法1

    首先需要定义一个struct:

    [StructLayout(LayoutKind.Sequential, Size = 64, Pack = 1)]
    public struct HardwareData
    {
        //[FieldOffset(0)]
        public UInt32 Time;          // 4个字节
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
        //[FieldOffset(4)]
        public float[] Tmpr;                   //  3* 4个字节
        //[FieldOffset(16)]
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
        public float[] Forces;                 //  6* 4个字节
        //[FieldOffset(40)]
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
        public float[] Distance;               //  6*4个字节
    }
    

    然后使用以下代码进行转换

    // code from https://stackoverflow.com/questions/628843/byte-for-byte-serialization-of-a-struct-in-c-sharp/629120#629120
    /// <summary>
    /// converts byte[] to struct
    /// </summary>
    public static T RawDeserialize<T>(byte[] rawData, int position)
    {
        int rawsize = Marshal.SizeOf(typeof(T));
        if (rawsize > rawData.Length - position)
            throw new ArgumentException("Not enough data to fill struct. Array length from position: " + (rawData.Length - position) + ", Struct length: " + rawsize);
        IntPtr buffer = Marshal.AllocHGlobal(rawsize);
        Marshal.Copy(rawData, position, buffer, rawsize);
        T retobj = (T)Marshal.PtrToStructure(buffer, typeof(T));
        Marshal.FreeHGlobal(buffer);
        return retobj;
    }
    
    /// <summary>
    /// converts a struct to byte[]
    /// </summary>
    public static byte[] RawSerialize(object anything)
    {
        int rawSize = Marshal.SizeOf(anything);
        IntPtr buffer = Marshal.AllocHGlobal(rawSize);
        Marshal.StructureToPtr(anything, buffer, false);
        byte[] rawDatas = new byte[rawSize];
        Marshal.Copy(buffer, rawDatas, 0, rawSize);
        Marshal.FreeHGlobal(buffer);
        return rawDatas;
    }
    

    注意这里我使用的方式为LayoutKind.Sequential,如果直接使用LayoutKind.Explicit并设置FieldOffset会弹出一个诡异的错误System.TypeLoadException:“Could not load type 'ConsoleApp3.DataItem' from assembly 'ConsoleApp3, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' because it contains an object field at offset 4 that is incorrectly aligned or overlapped by a non-object field.”

    方法2

    提示是对齐的错误,这个和编译的时候使用的32bit和64位是相关的,详细数据封送对齐的操作我不就详细说了,贴下代码。

    //强制指定x86编译
    [StructLayout(LayoutKind.Explicit, Size = 64, Pack = 1)]
    public struct DataItem
    {
        [MarshalAs(UnmanagedType.U4)]
        [FieldOffset(0)]
        public UInt32 time;         // 4个字节
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3, ArraySubType = UnmanagedType.R4)]
        [FieldOffset(4)]
        public float[] tmpr;                   //  3* 4个字节
        [FieldOffset(16)]
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6, ArraySubType = UnmanagedType.R4)]
        public float[] forces;                 //  6* 4个字节
        [FieldOffset(40)]
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6, ArraySubType = UnmanagedType.R4)]
        public float[] distance;               //  6*4个字节
    }
    

    强制指定x64编译没有成功,因为数据对齐后和从下位机上来的数据长度是不符的。

    方法3

    微软不是很推荐使用LayoutKind.Explicit,如果非要用并且不想指定平台的话,可以使用指针来操作,当然,这个需要unsafe

    var item = RawDeserialize<DataItem>(tail.ToArray(), 0);
    unsafe
    {
        float* p = &item.forces;
        for (int i = 0; i < 6; i++)
        {
            Console.WriteLine(*p);
            p++;
        }
    }
    
    [StructLayout(LayoutKind.Explicit, Size = 64, Pack = 1)]
    public struct DataItem
    {
        [FieldOffset(0)]
        public UInt32 time;         // 4个字节
        [FieldOffset(4)]
        public float tmpr;                   //  3* 4个字节
        [FieldOffset(16)]
        public float forces;                 //  6* 4个字节
        [FieldOffset(40)]
        public float distance;               //  6*4个字节
    }
    

    方法4

    感觉写起来还是很麻烦,既然用上了unsafe,就干脆直接一点。

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public unsafe struct DataItem
    {
        public UInt32 time;         // 4个字节
        public fixed float tmpr[3];                   //  3* 4个字节
        public fixed float forces[6];                 //  6* 4个字节
        public fixed float distance[6];               //  6*4个字节
    }
    

    这样,获得数组可以直接正常访问,不再需要unsafe了。

    总结

    数据解析作为上下位机通讯的常用操作,使用struct直接转换数据可以大大简化工作量。建议还是使用LayoutKind.Sequential来进行封送数据,有关于数据在托管与非托管中的转换,可以详细看看微软有关互操作的内容。

    以上代码在.NET 5.0下编译通过并能正常执行。

    补充

    注意上面的前提要求是字节序为小端字节序(一般计算机都是小端字节序),对于大端字节序发送过来的数据,需要进行字节序转换。我找到一处代码写的很好:

    //CODE FROM https://stackoverflow.com/a/15020402
    public static class FooTest
    {
        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        public struct Foo2
        {
            public byte b1;
            public short s;
            public ushort S;
            public int i;
            public uint I;
            public long l;
            public ulong L;
            public float f;
            public double d;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 10)]
            public string MyString;
        }
    
        [StructLayout(LayoutKind.Sequential, Pack = 1)]
        public struct Foo
        {
            public byte b1;
            public short s;
            public ushort S;
            public int i;
            public uint I;
            public long l;
            public ulong L;
            public float f;
            public double d;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 10)]
            public string MyString;
            public Foo2 foo2;
        }
    
        public static void test()
        {
            Foo2 sample2 = new Foo2()
            {
                b1 = 0x01,
                s = 0x0203,
                S = 0x0405,
                i = 0x06070809,
                I = 0x0a0b0c0d,
                l = 0xe0f101112131415,
                L = 0x161718191a1b1c,
                f = 1.234f,
                d = 4.56789,
                MyString = @"123456789", // null terminated => only 9 characters!
            };
    
            Foo sample = new Foo()
            {
                b1 = 0x01,
                s = 0x0203,
                S = 0x0405,
                i = 0x06070809,
                I = 0x0a0b0c0d,
                l = 0xe0f101112131415,
                L = 0x161718191a1b1c,
                f = 1.234f,
                d = 4.56789,
                MyString = @"123456789", // null terminated => only 9 characters!
                foo2 = sample2,
            };
    
            var bytes_LE = Dummy.StructToBytes(sample, Endianness.LittleEndian);
            var restoredLEAsLE = Dummy.BytesToStruct<Foo>(bytes_LE, Endianness.LittleEndian);
            var restoredLEAsBE = Dummy.BytesToStruct<Foo>(bytes_LE, Endianness.BigEndian);
    
            var bytes_BE = Dummy.StructToBytes(sample, Endianness.BigEndian);
            var restoredBEAsLE = Dummy.BytesToStruct<Foo>(bytes_BE, Endianness.LittleEndian);
            var restoredBEAsBE = Dummy.BytesToStruct<Foo>(bytes_BE, Endianness.BigEndian);
    
            Debug.Assert(sample.Equals(restoredLEAsLE));
            Debug.Assert(sample.Equals(restoredBEAsBE));
            Debug.Assert(restoredBEAsLE.Equals(restoredLEAsBE));
        }
    
        public enum Endianness
        {
            BigEndian,
            LittleEndian
        }
    
        private static void MaybeAdjustEndianness(Type type, byte[] data, Endianness endianness, int startOffset = 0)
        {
            if ((BitConverter.IsLittleEndian) == (endianness == Endianness.LittleEndian))
            {
                // nothing to change => return
                return;
            }
    
            foreach (var field in type.GetFields())
            {
                var fieldType = field.FieldType;
                if (field.IsStatic)
                    // don't process static fields
                    continue;
    
                if (fieldType == typeof(string)) 
                    // don't swap bytes for strings
                    continue;
    
                var offset = Marshal.OffsetOf(type, field.Name).ToInt32();
    
                // handle enums
                if (fieldType.IsEnum)
                    fieldType = Enum.GetUnderlyingType(fieldType);
    
                // check for sub-fields to recurse if necessary
                var subFields = fieldType.GetFields().Where(subField => subField.IsStatic == false).ToArray();
    
                var effectiveOffset = startOffset + offset;
    
                if (subFields.Length == 0)
                {
                    Array.Reverse(data, effectiveOffset, Marshal.SizeOf(fieldType));
                }
                else
                {
                    // recurse
                    MaybeAdjustEndianness(fieldType, data, endianness, effectiveOffset);
                }
            }
        }
    
        internal static T BytesToStruct<T>(byte[] rawData, Endianness endianness) where T : struct
        {
            T result = default(T);
    
            MaybeAdjustEndianness(typeof(T), rawData, endianness);
    
            GCHandle handle = GCHandle.Alloc(rawData, GCHandleType.Pinned);
    
            try
            {
                IntPtr rawDataPtr = handle.AddrOfPinnedObject();
                result = (T)Marshal.PtrToStructure(rawDataPtr, typeof(T));
            }
            finally
            {
                handle.Free();
            }
    
            return result;
        }
    
        internal static byte[] StructToBytes<T>(T data, Endianness endianness) where T : struct
        {
            byte[] rawData = new byte[Marshal.SizeOf(data)];
            GCHandle handle = GCHandle.Alloc(rawData, GCHandleType.Pinned);
            try
            {
                IntPtr rawDataPtr = handle.AddrOfPinnedObject();
                Marshal.StructureToPtr(data, rawDataPtr, false);
            }
            finally
            {
                handle.Free();
            }
    
            MaybeAdjustEndianness(typeof(T), rawData, endianness);
    
            return rawData;
        }
    
    }
    

    参考资料

    除非特殊说明,本作品由podolski创作,采用知识共享署名 4.0 国际许可协议进行许可。欢迎转载,转载请保留原文链接~喜欢的观众老爷们可以点下关注或者推荐~
  • 相关阅读:
    Proguard打包混淆报错:can't find superclass or interface
    proguard returned with error code 1.异常的解决方法
    android 混淆配置
    解决android混淆编译出现Proguard returned with error code 1和文件名、目录名或卷标语法不正确错误
    Eclipse提示No java virtual machine
    [mysql]数据库查询实例
    [算法]高效求素数
    [笔试]程序员面试宝典
    [linux]进程间通信IPC
    [linux]信号的捕获和处理
  • 原文地址:https://www.cnblogs.com/podolski/p/14295500.html
Copyright © 2011-2022 走看看