Eine umfassende Anleitung zum Lesen von C/C++ Datenstrukturen in C# aus einem byte[]
Array
Bei der Migration oder der Arbeit mit Datenstrukturen über verschiedene Programmiersprachen hinweg, insbesondere von C/C++ nach C#, sehen sich Entwickler häufig der Herausforderung gegenüber, die Byte-Darstellung einer C/C++ Struktur in eine handhabbare C#-Entsprechung zu übersetzen. Dieser Blogbeitrag behandelt, wie man effizient ein byte[]
Array, das C/C++ Strukturdaten enthält, in eine C#-Struktur umwandelt, ohne die Unbeholfenheit, die oft durch unnötiges Kopieren von Daten entsteht.
Das Problem Verstehen
Betrachten Sie die folgende C-Strukturdefinition:
typedef OldStuff {
CHAR Name[8];
UInt32 User;
CHAR Location[8];
UInt32 TimeStamp;
UInt32 Sequence;
CHAR Tracking[16];
CHAR Filler[12];
}
Diese Darstellung besteht aus einer festen Abfolge von Zeichen und Ganzzahlen. Bei der Arbeit mit solchen Binärdaten in C# stehen Entwickler vor der Herausforderung, die Struktur genau auszurichten und für die Verwendung in verwaltetem Code umzuwandeln.
Die entsprechende C#-Struktur verwendet StructLayout
, um zu steuern, wie die Daten im Speicher organisiert sind:
[StructLayout(LayoutKind.Explicit, Size = 56, Pack = 1)]
public struct NewStuff
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8)]
[FieldOffset(0)]
public string Name;
[MarshalAs(UnmanagedType.U4)]
[FieldOffset(8)]
public uint User;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 8)]
[FieldOffset(12)]
public string Location;
[MarshalAs(UnmanagedType.U4)]
[FieldOffset(20)]
public uint TimeStamp;
[MarshalAs(UnmanagedType.U4)]
[FieldOffset(24)]
public uint Sequence;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)]
[FieldOffset(28)]
public string Tracking;
}
Der Konventionelle Ansatz
Ein typischer Ansatz, den Entwickler wählen, besteht darin, das Byte-Array in einen separaten Puffer zu kopieren, bevor sie versuchen, es in die gewünschte C#-Struktur zu marshallieren. Hier ist eine häufig verwendete Methode:
GCHandle handle;
NewStuff MyStuff;
int BufferSize = Marshal.SizeOf(typeof(NewStuff));
byte[] buff = new byte[BufferSize];
Array.Copy(SomeByteArray, 0, buff, 0, BufferSize);
handle = GCHandle.Alloc(buff, GCHandleType.Pinned);
MyStuff = (NewStuff)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(NewStuff));
handle.Free();
Obwohl dieser Ansatz funktioniert, führt er zu unnötigem Overhead, insbesondere in Bezug auf den Speicherverbrauch und die Leistung, da eine Zwischenkopie der Daten erstellt wird.
Eine Bessere Lösung
Statt die Daten zu duplizieren, können Sie direkt das Byte-Array nutzen, um Ihre Struktur extrahieren. Hier ist eine effizientere Methode:
Direkte Verwendung von GCHandle
NewStuff ByteArrayToNewStuff(byte[] bytes)
{
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
NewStuff stuff = (NewStuff)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(NewStuff));
return stuff;
}
finally
{
handle.Free();
}
}
Diese Methode allokiert einen Handle für das ursprüngliche Byte-Array und beseitigt die Notwendigkeit, einen zusätzlichen Puffer zu erstellen.
Generische Methode für Flexibilität
Wenn Sie jede Struktur aus einem Byte-Array umwandeln möchten, können Sie eine generische Methode erstellen:
T ByteArrayToStructure<T>(byte[] bytes) where T : struct
{
T stuff;
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
try
{
stuff = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
}
finally
{
handle.Free();
}
return stuff;
}
Unsafe-Ansatz für Erhöhte Leistung
Für fortgeschrittene Benutzer kann die Nutzung des unsafe
Kontexts sogar noch bessere Leistung bieten, da sie direkt mit Zeigern arbeiten können:
unsafe T ByteArrayToStructure<T>(byte[] bytes) where T : struct
{
fixed (byte* ptr = &bytes[0])
{
return (T)Marshal.PtrToStructure((IntPtr)ptr, typeof(T));
}
}
Fazit
Das effektive Lesen einer C/C++ Datenstruktur in C# aus einem byte[]
Array muss nicht umständlich sein. Durch die Verwendung von GCHandle
direkt auf dem Byte-Array oder durch die Verwendung von unsafe
-Code erhöhen Sie die Leistung und vereinfachen Ihren Code. Die Annahme dieser Ansätze führt zu saubererem, wartbarem Code, während unnötige Speicher-Overheads vermieden werden.
Wenn Sie das nächste Mal mit der Datenumwandlung zwischen verschiedenen Programmiersprachen zu tun haben, denken Sie an diese Methoden, um Ihren Prozess zu optimieren!