دليل شامل لقراءة هياكل بيانات C/C++ في C# من مصفوفة byte[]
عند الانتقال أو العمل مع هياكل البيانات عبر اللغات، وخاصة من C/C++ إلى C#، يواجه المطورون غالبًا تحدي ترجمة التمثيل الثنائي لهيكل بيانات C/C++ إلى ما يعادله القابل للإدارة في C#. يتناول هذا المنشور كيف يمكن تحويل مصفوفة byte[]
تحتوي على بيانات هيكل C/C++ إلى هيكل C# بكفاءة، دون الإحراج الذي ينجم غالبًا عن نسخ البيانات غير الضرورية.
فهم المشكلة
نعتبر تعريف الهيكل C التالي:
typedef OldStuff {
CHAR Name[8];
UInt32 User;
CHAR Location[8];
UInt32 TimeStamp;
UInt32 Sequence;
CHAR Tracking[16];
CHAR Filler[12];
}
يتكون هذا التمثيل من تسلسل ثابت من الأحرف والأعداد. عند العمل مع مثل هذه البيانات الثنائية في C#، يواجه المطورون تحدي محاذاة الهيكل بدقة وتحويله للاستخدام في الكود المدعوم.
يستخدم هيكل C# المقابل StructLayout
للتحكم في كيفية تنظيم البيانات في الذاكرة:
[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;
}
النهج التقليدي
النهج النموذجي الذي يتبعه المطورون يتضمن نسخ مصفوفة البايت إلى مخزن مؤقت منفصل قبل محاولة نقلها إلى الهيكل C# المطلوب. إليك طريقة شائعة:
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();
بينما يعمل هذا النهج، فإنه يقدم عبءًا غير ضروري، خاصةً فيما يتعلق باستخدام الذاكرة والأداء، من خلال إنشاء نسخة وسيطة من البيانات.
حل أفضل
بدلاً من تكرار البيانات، يمكنك استخدام مصفوفة البايت مباشرةً لاستخراج الهيكل الخاص بك. إليك طريقة أكثر كفاءة:
استخدام 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();
}
}
تخصص هذه الطريقة مقبضًا لمصفوفة البايت الأصلية وتلغي الحاجة لإنشاء مخزن مؤقت إضافي.
طريقة عامة للمرونة
إذا كنت ترغب في تحويل أي بنية من مصفوفة بايت، يمكنك إنشاء طريقة عامة:
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
إلى الحصول على أداء أفضل، حيث يسمح لك بالعمل مباشرة مع المؤشرات:
unsafe T ByteArrayToStructure<T>(byte[] bytes) where T : struct
{
fixed (byte* ptr = &bytes[0])
{
return (T)Marshal.PtrToStructure((IntPtr)ptr, typeof(T));
}
}
الخلاصة
إن قراءة بنية بيانات C/C++ بفعالية في C# من مصفوفة byte[]
لا يجب أن تكون مرهقة. من خلال استخدام GCHandle
مباشرةً على مصفوفة البايت أو استخدام كود unsafe
، يمكنك تحسين الأداء وتبسيط الكود الخاص بك. يؤدي اعتماد هذه الطرق إلى كود أكثر نظافة وقابلية للصيانة مع تجنب الحمل الزائد غير الضروري على الذاكرة.
لذا، في المرة القادمة التي تجد فيها نفسك تتعامل مع تحويل البيانات عبر اللغات، تذكر هذه الطرق لتبسيط عمليتك!