C# 에서 C의 구조체 형식 데이터 읽기

C 또는 C++ 에서 구조체 형식으로 파일을 저장한 경우가 많습니다. C# 에서 그 파일들을 읽으려면 약간의 테크닉이 필요합니다. 다음 항목에서 그 방법을 소개시켜 드립니다.

C 의 구조체를 C# 구조체로 캐스팅 하는 방법입니다.

메모리에서 일일히 몇바이트씩 쪼개서 생성된 클래스 또는 구조체에 넣어주는 방법이 있지만 여간 귀찮은일이 아닙니다.
다음과 같이 일일히 변수 하나하나 넣어주는 방식이죠

public static TestStruct FromBinaryReaderField(BinaryReader br)
{
     TestStruct s = new TestStruct();//New struct
     s.longField = br.ReadInt64();//Fill the first field
     s.byteField = br.ReadByte();//Fill the second field
     s.byteArrayField = br.ReadBytes(16);
     s.floatField = br.ReadSingle();
     return s;
}

노동입니다.

이제부터 C에서 저장한 구조체를 그대로 C#에서도 사용하는 방법을 알려드리겠습니다.

C# 에서 의 구조체 생성

핵심 Attribute들이 있습니다. 다음 두가지 입니다.

StructLayoutAttribute 는 메모리에 있는 클래스 또는 구조체의 데이터 필드에 대한 실제 레이아웃을 제어할 수 있습니다.
MarshalAsAttribute 를 사용하여 관리 코드와 비관리 코드 간에 데이터를 마샬링해야 합니다.
즉 C의 char 배열의 사이즈만큼 C# 에서도 정의를 해줘야 합니다.
위의 TestStruct 구조체에 대한 매핑된 C# 예제입니다.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct TestStruct
{
	public long longField;
	public byte byteField;
	[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
	public byte[] byteArrayField;
	public float floatField;
}

그런데 여기에서 한가지 약간의 함정이 존재합니다.
C 구조체의 Padding 입니다.
C 에서 1Byte 패딩으로 되어 있지 않고 그대로 파일로 저장하였다면
C# 구조체 생성시 패딩을 넣어줘야 합니다.
C에서 구조체 선언시 #pragma pack(1) 를 넣어줬다면 좋았을텐데요

StructLayoutAttribute.Pack

Pack 필드는 메모리에서 형식의 필드 맞춤을 제어합니다. C 구조체의 #pragma pack(1) 과 같은 역할을 합니다. 기본적으로 값은 0이며 현재 플랫폼의 기본 압축 크기를 나타냅니다. 값 Pack 은 0, 1, 2, 4, 8, 16, 32, 64 또는 128이어야 합니다.

예제를 한번 보겠습니다.
C에서 다음처럼 만들었다면 #pragma pack(1) 을 붙이지 않았을경우

struct MyStructure 
{
    char myLetter;       // Member (char variable) 
	int myNum;           // Member (int variable)
};

C에서 위와 같이 만들면 기본 32비트 프로그램에서 4Byte Packaging 이므로 입니다. 즉 sizeof(MyStrructure) 했을경우 size는 8이 나옵니다.

C# 으로 구조체를 사용하면

[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct MyStructure
{
	public byte myLetter
	public int myNum;
}

가 되며 “Pack = 4 가 되어야 합니다.

만약 C# 에서 어떠한 이유로 Pack=1 로 선언해야 한다면 아래와 같이 3byte Padding 이 붙어야 합니다.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct MyStructure
{
	public byte myLetter
	[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
	public byte[] Padding;
	public int myNum;
}

패딩 문제를 잘 계산하셔야 합니다. 분석이 필요 합니다.

C# 구조체로 캐스팅 방법

아주 간단히 코드를 보여드립니다.

var pData = GCHandle.Alloc(data, GCHandleType.Pinned);
TestStruct result = (TestStruct)Marshal.PtrToStructure(pData.AddrOfPinnedObject(), typeof(TestStruct));
pData.Free();

위의 코드를 사용함으로써 캐스팅이 완료 됩니다.

예제코드

그럼 위의 예제 구조체를 기반으로 예제인 콘솔앱을 하나 만들어 보도록 하겠습니다.
위의 캐스팅 예제 부분을 클래스로 정리좀 하겠습니다.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace ReadTestStructure
{
    /* C 의 구조체 형식입니다. 
    #pragma pack(push, 1)
    struct TestStruct
    {
        long long longField;
        char byteField;
        char byteArrayField[16];
        float floatField;
    }
    #pragma pack(pop)
    */

    [StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct TestStruct
    {
        public long longField;
        public byte byteField;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
        public byte[] byteArrayField;
        public float floatField;
    }

    public static class CastingHelper
    {
        public static T CastToStruct<T>(this byte[] data) where T : struct
        {
            var pData = GCHandle.Alloc(data, GCHandleType.Pinned);
            var result = (T)Marshal.PtrToStructure(pData.AddrOfPinnedObject(), typeof(T));
            pData.Free();
            return result;
        }

        public static byte[] CastToArray<T>(this T data) where T : struct
        {
            var result = new byte[Marshal.SizeOf(typeof(T))];
            var pResult = GCHandle.Alloc(result, GCHandleType.Pinned);
            Marshal.StructureToPtr(data, pResult.AddrOfPinnedObject(), true);
            pResult.Free();
            return result;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            byte[] sroucefile = File.ReadAllBytes(".\\struct.bin");
            TestStruct teststruct = CastingHelper.CastToStruct<TestStruct>(sroucefile);
        }
    }
}

위에 보시는것처럼 CastingHelper 라는 클래스로 간단히 정리 하였습니다.

콘솔앱에서 실행시켜 디버깅 해 보시면 teststruct 변수에 C/C++ 구조체가 캐스팅된 것을 보실수 있습니다.

결론

위 처럼 C 에서 구조체 그대로 저장되어 있는 부분을 C# 에서 읽는 방법을 보여드렸습니다.
소켓통신도 마찬가지입니다. 구조체 형식으로 되어 있는 프로토콜을 파싱할때 한패킷을 받아서 이렇게 캐스팅 하면 쉽게 데이터를 구성할 수 있습니다.

2 thoughts on “C# 에서 C의 구조체 형식 데이터 읽기”

  1. 안녕하세요….
    구조체에 대해서 질문이있습니다..
    이런 에러가 계속나타나는데
    ( Error => 포함된 배열 인스턴스의 길이가 레이아웃에서 선언된 길이와 일치하지 않으므로 형식을 마샬링할 수 없습니다 ) 이런 에러가 계속 나타나는데 어떤게 문제일까요 ??

    소스====
    [StructLayout(LayoutKind.Sequential, Pack = 4)] 인 경우
    —————————–
    public uint iSTX;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)]
    public byte[] nCode;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public byte[] fillerA;
    public uint sellhoga1;
    public uint buyhoga1;
    public uint sellvol1;
    public uint buyvol1;
    ————————————————

    응답
    • 위 소스에선 문제가 없고 사용함에 따라 해당 에러가 발생합니다.
      [MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)]
      public byte[] nCode;
      라는 nCode 변수를 사용할 경우
      nCode = new byte[9] 라고 nCode 자체에 SizeConst = 9 라고 지정한 크기 만큼 메모리를 할당하고 사용 하셔야 합니다.
      만약 nCode = new byte[3] 등의 지정한 크기보다 작거나 메모리를 할당하지 않고 캐스팅 등을 사용할 경우 위와 같은 에러가 발생합니다.

      응답

Leave a Comment