C#
C/C++
interop
byte array
data structures

Reading a C/C data structure in C from a byte array

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

Reading a native C or C++ structure from a C# byte array is an interop problem, not just a casting problem. It only works safely when the structure layout, packing, field sizes, and endianness are all known and compatible. If any of those assumptions are wrong, the parsed data may look valid while actually being wrong.

Start with a Matching Managed Struct

If the native layout is fixed and simple, declare a C# struct with explicit layout metadata.

csharp
1using System.Runtime.InteropServices;
2
3[StructLayout(LayoutKind.Sequential, Pack = 1)]
4public struct Header
5{
6    public int Id;
7    public short Version;
8    public short Flags;
9}

Pack = 1 is only correct if the native structure was packed that way. Do not guess here. Match the native definition precisely.

Read the Struct from a Byte Span

For blittable structs, MemoryMarshal.Read is a clean modern option.

csharp
1using System;
2using System.Runtime.InteropServices;
3
4byte[] data = {
5    1, 0, 0, 0,
6    2, 0,
7    3, 0
8};
9
10Header header = MemoryMarshal.Read<Header>(data);
11Console.WriteLine($"{header.Id} {header.Version} {header.Flags}");

This is fast and avoids manual pointer code, but it assumes the byte array matches the managed struct layout exactly.

Handle Endianness Explicitly When Needed

If the bytes come from a system or protocol with known endianness, explicit field parsing is often safer than direct struct reads.

csharp
1using System;
2using System.Buffers.Binary;
3
4byte[] data = {
5    0, 0, 0, 1,
6    0, 2,
7    0, 3
8};
9
10int id = BinaryPrimitives.ReadInt32BigEndian(data.AsSpan(0, 4));
11short version = BinaryPrimitives.ReadInt16BigEndian(data.AsSpan(4, 2));
12short flags = BinaryPrimitives.ReadInt16BigEndian(data.AsSpan(6, 2));
13
14Console.WriteLine($"{id} {version} {flags}");

This is more verbose, but often more robust for network or file formats.

Be Careful with Strings, Pointers, and Unions

Direct struct mapping works best for blittable numeric fields. Native pointers, variable-length strings, and unions often need custom parsing rather than a one-shot struct read.

That is the line between "interop layout match" and "protocol decoder". If the data format is complex, manual parsing is usually the safer engineering choice.

Document the Native Definition Beside the Parser

Interop code becomes fragile when the managed parser is separated from the native contract it depends on. Keep the original C or C++ definition near the C# parser, or at least document the field sizes and packing assumptions in comments or tests.

That small discipline matters because structure changes in native code are otherwise easy to miss, and the C# side may continue reading bytes without an obvious runtime failure.

Validate Size Before Reading

Before deserializing, check that the byte array is at least as large as the target structure.

csharp
1using System;
2using System.Runtime.InteropServices;
3
4int size = Marshal.SizeOf<Header>();
5if (data.Length < size)
6{
7    throw new InvalidOperationException("Insufficient data for Header");
8}

This prevents partial reads from silently producing garbage values.

Common Pitfalls

  • Assuming C# struct layout automatically matches a native C or C++ struct.
  • Ignoring native packing and alignment rules.
  • Using direct struct reads when endianness differs from the current machine.
  • Treating pointers or variable-length data as if they were ordinary inline fields.
  • Failing to validate byte length before attempting the read.

Interop bugs are dangerous because they can produce believable but incorrect values. Add tests with known byte patterns whenever possible. Regression tests help.

Summary

  • Reading a native structure from bytes in C# only works when layout assumptions are correct.
  • Use StructLayout carefully and match the native packing rules exactly.
  • 'MemoryMarshal.Read is good for simple blittable layouts.'
  • Use explicit field parsing when endianness or protocol clarity matters more than brevity.
  • Keep the managed parser close to the native contract it is supposed to represent.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.