byte[] 배열에서 C#으로 C/C++ 데이터 구조 읽기 종합 가이드

다양한 언어 간 데이터 구조를 이식하거나 작업할 때, 특히 C/C++에서 C#로의 변환 시, 개발자들은 C/C++ 구조체의 바이트 표현을 관리 가능한 C# 동등체로 변환하는데 어려움을 겪곤 합니다. 이 블로그 포스트에서는 C/C++ 구조체 데이터를 포함하는 byte[] 배열을 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));
    }
}

결론

byte[] 배열에서 C#으로 C/C++ 데이터 구조를 효과적으로 읽는 것은 번거로울 필요가 없습니다. 바이트 배열에서 GCHandle을 직접 사용하거나 unsafe 코드를 활용함으로써 성능을 향상시키고 코드를 단순화할 수 있습니다. 이러한 접근 방식을 채택하면 불필요한 메모리 오버헤드를 피하며 더 깨끗하고 유지 관리하기 쉬운 코드를 작성할 수 있습니다.

그러니 다음 번에 언어 간 데이터 변환을 처리하게 된다면, 이 방법들을 기억하여 작업 과정을 간소화하세요!