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
코드를 활용함으로써 성능을 향상시키고 코드를 단순화할 수 있습니다. 이러한 접근 방식을 채택하면 불필요한 메모리 오버헤드를 피하며 더 깨끗하고 유지 관리하기 쉬운 코드를 작성할 수 있습니다.
그러니 다음 번에 언어 간 데이터 변환을 처리하게 된다면, 이 방법들을 기억하여 작업 과정을 간소화하세요!