C#使用struct直接轉(zhuǎn)換下位機數(shù)據(jù)的示例代碼
編寫上位機與下位機通信的時候,涉及到協(xié)議的轉(zhuǎn)換,比較多會使用到二進制。傳統(tǒng)的方法,是將數(shù)據(jù)整體獲取到byte數(shù)組中,然后逐字節(jié)對數(shù)據(jù)進行解析。這樣操作工作量比較大,對于較長數(shù)據(jù)段更容易計算位置出錯。
其實,對于下位機給出通訊的數(shù)據(jù)結(jié)構(gòu)的情況下,可以直接使用C#的struct將數(shù)據(jù)直接轉(zhuǎn)換。需要使用到Marshal
。
數(shù)據(jù)結(jié)構(gòu)
假定下位機(C語言編寫)給到我們的數(shù)據(jù)結(jié)構(gòu)是這個,傳輸方式為小端方式
typedef struct { unsigned long int time; // 4個字節(jié) float tmpr[3]; // 4*3 個字節(jié) float forces[6]; // 4*6個字節(jié) float distance[6]; // 4*6個字節(jié) } dataItem_t;
方法1
首先需要定義一個struct:
[StructLayout(LayoutKind.Sequential, Size = 64, Pack = 1)] public struct HardwareData { //[FieldOffset(0)] public UInt32 Time; // 4個字節(jié) [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)] //[FieldOffset(4)] public float[] Tmpr; // 3* 4個字節(jié) //[FieldOffset(16)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] public float[] Forces; // 6* 4個字節(jié) //[FieldOffset(40)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] public float[] Distance; // 6*4個字節(jié) }
然后使用以下代碼進行轉(zhuǎn)換
// 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
并設(shè)置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位是相關(guān)的,詳細數(shù)據(jù)封送對齊的操作我不就詳細說了,貼下代碼。
//強制指定x86編譯 [StructLayout(LayoutKind.Explicit, Size = 64, Pack = 1)] public struct DataItem { [MarshalAs(UnmanagedType.U4)] [FieldOffset(0)] public UInt32 time; // 4個字節(jié) [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3, ArraySubType = UnmanagedType.R4)] [FieldOffset(4)] public float[] tmpr; // 3* 4個字節(jié) [FieldOffset(16)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6, ArraySubType = UnmanagedType.R4)] public float[] forces; // 6* 4個字節(jié) [FieldOffset(40)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6, ArraySubType = UnmanagedType.R4)] public float[] distance; // 6*4個字節(jié) }
強制指定x64編譯沒有成功,因為數(shù)據(jù)對齊后和從下位機上來的數(shù)據(jù)長度是不符的。
方法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個字節(jié) [FieldOffset(4)] public float tmpr; // 3* 4個字節(jié) [FieldOffset(16)] public float forces; // 6* 4個字節(jié) [FieldOffset(40)] public float distance; // 6*4個字節(jié) }
方法4
感覺寫起來還是很麻煩,既然用上了unsafe
,就干脆直接一點。
[StructLayout(LayoutKind.Sequential, Pack = 1)] public unsafe struct DataItem { public UInt32 time; // 4個字節(jié) public fixed float tmpr[3]; // 3* 4個字節(jié) public fixed float forces[6]; // 6* 4個字節(jié) public fixed float distance[6]; // 6*4個字節(jié) }
這樣,獲得數(shù)組可以直接正常訪問,不再需要unsafe
了。
總結(jié)
數(shù)據(jù)解析作為上下位機通訊的常用操作,使用struct直接轉(zhuǎn)換數(shù)據(jù)可以大大簡化工作量。建議還是使用LayoutKind.Sequential
來進行封送數(shù)據(jù),有關(guān)于數(shù)據(jù)在托管與非托管中的轉(zhuǎn)換,可以詳細看看微軟有關(guān)互操作的內(nèi)容。
以上代碼在.NET 5.0下編譯通過并能正常執(zhí)行。
補充
注意上面的前提要求是字節(jié)序為小端字節(jié)序(一般計算機都是小端字節(jié)序),對于大端字節(jié)序發(fā)送過來的數(shù)據(jù),需要進行字節(jié)序轉(zhuǎn)換。我找到一處代碼寫的很好:
//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
到此這篇關(guān)于C#使用struct直接轉(zhuǎn)換下位機數(shù)據(jù)的文章就介紹到這了,更多相關(guān)C#下位機數(shù)據(jù)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- C#調(diào)用C類型dll入?yún)閟truct的問題詳解
- C#中的只讀結(jié)構(gòu)體(readonly struct)詳解
- 區(qū)分C# 中的 Struct 和 Class
- 淺析C# 結(jié)構(gòu)體struct
- C#如何從byte[]中直接讀取Structure實例詳解
- 深入解析C#編程中struct所定義的結(jié)構(gòu)
- C#中結(jié)構(gòu)(struct)的部分初始化和完全初始化實例分析
- C#中struct和class的區(qū)別詳解
- C# Struct的內(nèi)存布局問題解答
- 深入探討C#中的結(jié)構(gòu)struct
- c# Struct的一些問題分析
相關(guān)文章
C#實現(xiàn)windows系統(tǒng)重啟和關(guān)機的代碼詳解
這篇文章主要介紹了C#實現(xiàn)windows系統(tǒng)重啟和關(guān)機的的方法,涉及C#調(diào)用windows系統(tǒng)命令實現(xiàn)控制開機、關(guān)機等操作的技巧,非常簡單實用,需要的朋友可以參考下2024-02-02C# 并發(fā)控制框架之單線程環(huán)境下實現(xiàn)每秒百萬級調(diào)度
本文介紹了一款專為工業(yè)自動化及機器視覺開發(fā)的C#并發(fā)流程控制框架,通過模仿Go語言并發(fā)模式設(shè)計,支持高頻調(diào)度及復(fù)雜任務(wù)處理,已在多個項目中驗證其穩(wěn)定性和可靠性2024-10-10