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;
 }

}

参考资料

https://www.developerfusion.com/article/84519/mastering-structs-in-c/

https://stackoverflow.com/a/15020402

https://stackoverflow.com/questions/628843/byte-for-byte-serialization-of-a-struct-in-c-sharp/629120

https://stackoverflow.com/questions/2871/reading-a-c-c-data-structure-in-c-sharp-from-a-byte-array/41836532

到此这篇关于C#使用struct直接转换下位机数据的文章就介绍到这了,更多相关C#下位机数据内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 深入解析C#编程中struct所定义的结构

    结构是使用 struct 关键字定义的,例如: public struct PostalAddress { // Fields, properties, methods and events go here... } 结构与类共享大多数相同的语法,但结构比类受到的限制更多: 在结构声明中,除非字段被声明为 const 或 static,否则无法初始化. 结构不能声明默认构造函数(没有参数的构造函数)或析构函数. 结构在赋值时进行复制.将结构赋值给新变量时,将复制所有数据,并且对新副本所做的任何修

  • 浅析C# 结构体struct

    结构体 有时候我们仅需要一个小的数据结构,类提供的功能多于我们需要的功能:考虑到性能原因,最好使用结构体. 结构体是值类型,存储在栈中或存储为内联(如果结构体是存储在堆中的另一个对象的一部分). 例如类class: public class Dimensions { public Dimensions(double length, double width) { Length = length; Width = width; } public double Length { get; set;

  • 深入探讨C#中的结构struct

    一.结构和类的区别 1.结构的级别和类一致,写在命名空间下面,可以定义字段.属性.方法.构造方法也可以通过关键字new创建对象. 2.结构中的字段不能赋初始值. 3.无参数的构造函数无论如何C#编译器都会自动生成,所以不能为结构定义一个无参构造函数. 4.在构造函数中,必须给结构体的所有字段赋值. 5.在构造函数中,为属性赋值,不认为是对字段赋值,因为属性不一定是去操作字段. 6.结构是值类型,在传递结构变量的时候,会将结构对象里的每一个字段复制一份拷贝到新的结构变量的字段中. 7.不能定义自动

  • C#调用C类型dll入参为struct的问题详解

    前言 C# 可以通过 DllImport 的方式引用 C 类型的 dll.但很多 dll 的参数不会是简单的基础类型,而是结构体 struct .因此就需要在 C# 端定义同样的结构体类型,才能实现调用 C 类型 dll.这里例举几种不同的结构体情况,以及其对应的解决方案. 基础调用方式 对于一个结构体类型: typedef struct DATA { int nNumber; float fDecimal; }; 在 C# 端就需要定义为 [StructLayout(LayoutKind.Se

  • c# Struct的一些问题分析

    目录 与类的区别: Struct的理论看过好一些,可是工作上基本没有应用过,Class倒处处都有.难道Struct就没有什么使用价值吗?搜了一下如何在类和结构中做出选择? ✔️ 如果类型的实例很小且通常寿命很短或通常嵌入其他对象中,请考虑定义结构而不是类. ❌ 避免定义结构,除非该类型具有以下所有特征: 它在逻辑上表示单个值,类似于原始类型(int,double等). 它的实例大小低于 16 字节. 它是不可变的. 它不必经常装箱. 在开发软件时,常常会有页面弹窗,而主页面经常需要传一些参数到窗

  • C#如何从byte[]中直接读取Structure实例详解

    序.前言 emmmmm,首先这篇文章讲的不是用BinaryFormatter来进行结构体的二进制转换,说真的BinaryFormatter这个类其实现在的作用并不是特别大了,因为BinaryFormatter二进制序列化出来的结果只能用于.net平台,现在可能就用于如存入Redis这种情况下会在使用. 去年年尾的样子,我阅读学习某C++开发的程序源码时,发现作者用了一个很骚的操作直接将byte[]数组转为了结构体对象: 上面的data变量是一个指向unsigned char类型的指针,就只要一个

  • C#中struct和class的区别详解

    本文详细分析了C#中struct和class的区别,对于C#初学者来说是有必要加以了解并掌握的. 简单来说,struct是值类型,创建一个struct类型的实例被分配在栈上.class是引用类型,创建一个class类型实例被分配在托管堆上.但struct和class的区别远不止这么简单. 概括来讲,struct和class的不同体现在: ● 类是引用类型,struct是值类型 ● 在托管堆上创建类的实例,在栈上创建struct实例 ● 类实例的赋值,赋的是引用地址,struct实例的赋值,赋的是

  • C#中的只读结构体(readonly struct)详解

    翻译自 John Demetriou 2018年4月8日 的文章 <C# 7.2 – Let's Talk About Readonly Structs>[1] 在本文中,我们来聊一聊从 C# 7.2 开始出现的一个特性 readonly struct. 任一结构体都可以有公共属性.私有属性访问器等等.我们从以下结构体示例来开始讨论: public struct Person { public string Name { get; set; } public string Surname {

  • 区分C# 中的 Struct 和 Class

    翻译自 Manju lata Yadav 2019年6月2日 的博文 <Difference Between Struct And Class In C#>,补充了一些内容和示例. 结构体(struct)是类(class)的轻量级版本.结构体是值类型,可用于创建行为类似于内置类型的对象. 比较 结构体和类共享许多特性,但与类相比有以下局限性. 结构体不能有默认构造函数(无参构造函数)或析构函数,构造函数中必须给所有字段赋值. public struct Coords { public doub

  • C# Struct的内存布局问题解答

    问题:请说出以下struct的实例大小以及内存布局 复制代码 代码如下: struct Struct1{    public byte a;    public short b;    public string c;    public int d;} struct Struct2{    public byte a;    public long b;    public byte c;    public string d;} struct Struct3{    byte a;    b

  • C#中结构(struct)的部分初始化和完全初始化实例分析

    本文实例分析了C#中结构(struct)的部分初始化和完全初始化,分享给大家供大家参考.具体分析如下: 假设有这样一个值类型struct,如下所示: public struct Size { public int Length; public int Width; public int Area() { return Length*Width; } } 一.客户端,给所有struct字段初始化后调用方法 class Program { static void Main(string[] args

随机推荐