คู่มือครบวงจรในการอ่านโครงสร้างข้อมูล 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();
    }
}

วิธีการนี้จัดสรร handle ให้กับอาร์เรย์ไบต์ต้นฉบับและกำจัดความจำเป็นในการสร้างบัฟเฟอร์เพิ่มเติม

วิธีการทั่วไปสำหรับความยืดหยุ่น

หากคุณต้องการแปลงโครงสร้างใด ๆ จากอาร์เรย์ไบต์ คุณสามารถสร้างวิธีทั่วไปได้:

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 คุณจะเพิ่มประสิทธิภาพและทำให้โค้ดของคุณเรียบง่ายขึ้น การนำวิธีการเหล่านี้ไปใช้จะก่อให้เกิดโค้ดที่สะอาดและบำรุงรักษาได้ง่ายขึ้นในขณะที่หลีกเลี่ยงภาระหน่วยความจำที่ไม่จำเป็น

ดังนั้น ครั้งต่อไปที่คุณต้องจัดการการแปลงข้อมูลข้ามภาษา ให้นึกถึงวิธีการเหล่านี้เพื่อทำให้กระบวนการของคุณมีความคล่องตัวขึ้น!