Um Guia Abrangente para Ler Estruturas de Dados C/C++ em C# de um Array byte[]

Ao migrar ou trabalhar com estruturas de dados entre linguagens, especialmente do C/C++ para o C#, os desenvolvedores frequentemente enfrentam o desafio de traduzir a representação em bytes de uma struct C/C++ em um equivalente gerenciável em C#. Este post no blog aborda como converter eficientemente um array byte[] contendo dados de structs C/C++ em uma struct C#, sem a desordem que frequentemente decorre da cópia desnecessária de dados.

Compreendendo o Problema

Considere a seguinte definição de struct em C:

typedef struct OldStuff {
    CHAR Name[8];
    UInt32 User;
    CHAR Location[8];
    UInt32 TimeStamp;
    UInt32 Sequence;
    CHAR Tracking[16];
    CHAR Filler[12];
} OldStuff;

Essa representação consiste em uma sequência fixa de caracteres e inteiros. Ao trabalhar com esse tipo de dados binários em C#, os desenvolvedores enfrentam o desafio de alinhar a estrutura corretamente e convertê-la para uso em código gerenciado.

A struct correspondente em C# utiliza StructLayout para controlar como os dados são organizados na memória:

[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;
}

A Abordagem Convencional

Uma abordagem típica que os desenvolvedores adotam envolve copiar o array de bytes para um buffer separado antes de tentar marshallá-lo na struct C# desejada. Aqui está um método frequentemente utilizado:

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();

Embora essa abordagem funcione, ela introduz sobrecarga desnecessária, especialmente em relação ao uso de memória e desempenho, ao criar uma cópia intermediária dos dados.

Uma Solução Melhor

Em vez de duplicar os dados, você pode utilizar diretamente o array de bytes para extrair sua struct. Aqui está um método mais eficiente:

Usando Diretamente 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();
    }
}

Este método aloca um handle para o array de bytes original e elimina a necessidade de criar um buffer adicional.

Método Genérico para Flexibilidade

Se você deseja converter qualquer struct de um array de bytes, você pode criar um método genérico:

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;
}

Abordagem Unsafe para Melhor Desempenho

Para usuários avançados, utilizar o contexto unsafe pode gerar um desempenho ainda melhor, pois permite que você trabalhe diretamente com ponteiros:

unsafe T ByteArrayToStructure<T>(byte[] bytes) where T : struct
{
    fixed (byte* ptr = &bytes[0])
    {
        return (T)Marshal.PtrToStructure((IntPtr)ptr, typeof(T));
    }
}

Conclusão

Ler efetivamente uma estrutura de dados C/C++ em C# a partir de um array byte[] não precisa ser complicado. Ao empregar o GCHandle diretamente no array de bytes ou usando código unsafe, você melhora o desempenho e simplifica seu código. Adotar essas abordagens resulta em um código mais limpo e de fácil manutenção, evitando sobrecarga desnecessária de memória.

Portanto, da próxima vez que você se deparar com a conversão de dados entre linguagens, lembre-se desses métodos para simplificar seu processo!