This answer assumes a few things:
- Your structs only contain primitive value types (i.e. no arrays, no reference types, no
decimal
values, no string
and so on).
- You have an array of bytes containing one or more structs at various offsets.
- You are using .Net Core 3.1 or later.
- You are using C# 7.0 or later.
It's possible to "overlay" a struct over a section of a byte array so that you can access the data in the array via the struct without making additional copies of the data. To do this, you use Span<byte>
and MemoryMarshal.AsRef<T>
.
When doing this, the structure packing is important of course. You must specify the packing for the structs to match the packing in the byte array. This is exactly the same as you have to do when declaring structs for use with P/Invoke.
(Despite what some may claim, the StructLayoutAttribute.Pack
setting does affect the layout of structs in memory; it's not just for P/Invoke.)
If you are using char
values in your structs, you must also specify the CharSet
as Unicode. If your source structs are not using UTF16 for the chars, (if they are using ANSI or ASCII) then you will need to declare those fields as byte
rather than char
because C# chars are always UTF16.
Once you've laid out your structs correctly and you have a byte[]
array containing one or more structs you can overlay a struct over a section of the array as follows:
- Create a
Span<byte>
using the Span<byte>(byte[] array, int start, int length)
constructor to reference the subset of the array that corresponds to the bytes of the struct.
- Use
MemoryMarshal.AsRef<T>
to reference the struct within the byte array. This references the struct directly in the byte array without copying any data.
The following program demonstrates this approach.
It defines three different structs, Demo1
, Demo2
and Demo3
each with a different packing.
It implements a toByteArray()
method, the only purpose of which is to convert the structs to a byte array with some spare bytes at the start and end. The implementation of this isn't important; you'd get your byte array from P/Invoke.
It also implements a helper method AsSpan()
. This is quite a useful method; it converts any blittable (unmanaged) struct to a Span<byte>()
.
The interesting method is consumeStructs()
. This overlays three different structs over the given byte[]
array and then prints out their contents.
If you run this program, you will see that the output matches the struct initialisation in Main()
.
Note that the structs are declared as ref var
. This means that no data is copied; the structs are actually referencing data directly in the data[]
array passed to the method.
I demonstrate that this is the case by modifying one of the bytes in the data[]
array that corresponds to the start of the Demo2
struct like so:
data[prefixByteCount + Marshal.SizeOf<Demo1>()] = (byte)'*';
After making that change and reprinting the struct, the output shows that the CharValue
has changed from 0
to *
. This demonstrates that the struct really does reference data directly in the data[]
array.
Here's the compilable Console application:
(Try it online)
using System;
using System.Linq;
using System.Runtime.InteropServices;
namespace Demo
{
static class Program
{
static void Main()
{
var demo1 = new Demo1
{
BoolValue = true,
DoubleValue = -1
};
var demo2 = new Demo2
{
CharValue = '0',
ShortValue = 1,
IntValue = 2,
LongValue = 3,
FloatValue = 4,
DoubleValue = 5.4321
};
var demo3 = new Demo3
{
ByteValue = 128,
FloatValue = 1.23f
};
const int PREFIX_BYTE_COUNT = 29;
const int POSTFIX_BYTE_COUNT = 17;
var bytes = toByteArray(PREFIX_BYTE_COUNT, POSTFIX_BYTE_COUNT, ref demo1, ref demo2, ref demo3);
consumeStructs(PREFIX_BYTE_COUNT, bytes);
}
static byte[] toByteArray(int prefixByteCount, int postfixByteCount, ref Demo1 demo1, ref Demo2 demo2, ref Demo3 demo3)
{
var demo1Bytes = AsSpan(ref demo1);
var demo2Bytes = AsSpan(ref demo2);
var demo3Bytes = AsSpan(ref demo3);
var prefixBytes = Enumerable.Repeat((byte)0, prefixByteCount);
var postfixBytes = Enumerable.Repeat((byte)0, postfixByteCount);
return prefixBytes
.Concat(demo1Bytes.ToArray())
.Concat(demo2Bytes.ToArray())
.Concat(demo3Bytes.ToArray())
.Concat(postfixBytes)
.ToArray();
}
static void consumeStructs(int prefixByteCount, byte[] data)
{
var demo1Bytes = new Span<byte>(data, prefixByteCount, Marshal.SizeOf<Demo1>());
var demo2Bytes = new Span<byte>(data, prefixByteCount + Marshal.SizeOf<Demo1>(), Marshal.SizeOf<Demo2>());
var demo3Bytes = new Span<byte>(data, prefixByteCount + Marshal.SizeOf<Demo1>() + Marshal.SizeOf<Demo2>(), Marshal.SizeOf<Demo3>());
ref var demo1Ref = ref MemoryMarshal.AsRef<Demo1>(demo1Bytes);
ref var demo2Ref = ref MemoryMarshal.AsRef<Demo2>(demo2Bytes);
ref var demo3Ref = ref MemoryMarshal.AsRef<Demo3>(demo3Bytes);
Console.WriteLine(demo1Ref);
Console.WriteLine(demo2Ref);
Console.WriteLine(demo3Ref);
Console.WriteLine("Modifying first byte of Demo2 struct in byte buffer.");
data[prefixByteCount + Marshal.SizeOf<Demo1>()] = (byte)'*';
Console.WriteLine(demo2Ref);
}
public static Span<byte> AsSpan<T>(ref T val) where T : unmanaged
{
var valSpan = MemoryMarshal.CreateSpan(ref val, 1);
return MemoryMarshal.Cast<T, byte>(valSpan);
}
}
[StructLayout(LayoutKind.Sequential, Pack = 8)]
public struct Demo1
{
public bool BoolValue;
public double DoubleValue;
public override string ToString()
{
return $"byte = {BoolValue}, double = {DoubleValue}";
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)]
public struct Demo2
{
public char CharValue;
public short ShortValue;
public int IntValue;
public long LongValue;
public float FloatValue;
public double DoubleValue;
public override string ToString()
{
return $"char = {CharValue}, short = {ShortValue}, int = {IntValue}, long = {LongValue}, float = {FloatValue}, double = {DoubleValue}";
}
}
[StructLayout(LayoutKind.Sequential, Pack = 2)]
public struct Demo3
{
public byte ByteValue;
public float FloatValue;
public override string ToString()
{
return $"byte = {ByteValue}, float = {FloatValue}";
}
}
}