C#使用struct直接转换下位机数据

  • A+
所属分类:.NET技术
摘要

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

编写上位机与下位机通信的时候,涉及到协议的转换,比较多会使用到二进制。传统的方法,是将数据整体获取到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;     }  } 

参考资料