編寫上位機與下位機通訊的時候,涉及到協議的轉換,比較多會使用到二進位制。傳統的方法,是將資料整體獲取到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#629120
- https://stackoverflow.com/questions/2871/reading-a-c-c-data-structure-in-c-sharp-from-a-byte-array/41836532#41836532