0

I have an unmanaged C/C++ library that I use to decode a variable sized buffer of bytes. These bytes constituent a complex datagram that is extracted from a SQLite BLOB. In the C/C++ code it is very easy to overlay a struct on top of he data using pointers. Is there anyway to do the same thing in C#? I am passing a ref to the Byte[] to the unmanaged side by using the Marshal class. I can also pass references to structures on the managed side that get filled in. I would to pull all this over to C#. Any thoughts, anyone?

TIA, Doug

AeroClassics
  • 1,074
  • 9
  • 19
  • 1
    It's less easy (need `unsafe` code, attributes to force the struct layout, and possibly some Marshalling or p/invoke code), but possible... but also not something I've ever needed to do personally, so I'm stopping at just a comment. – Joel Coehoorn May 25 '22 at 20:36
  • https://stackoverflow.com/questions/2871/reading-a-c-c-data-structure-in-c-sharp-from-a-byte-array ? – Caius Jard May 25 '22 at 20:39
  • It's been a long time since I've done that but if I had to I'd start by reviewing [Pin](https://learn.microsoft.com/en-us/dotnet/api/system.memory-1.pin?view=net-6.0) (prevents your block of memory from being moved by GC) and also [Blittable and Non-Blittable Types](https://learn.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types). – IVSoftware May 25 '22 at 20:48
  • There's `Marshal.PtrToStructure` but it requires you to have a pointer not an array. The function that takes an array and copies the bytes into a different type is `Buffer.BlockCopy` but it has overly pessimistic restrictions that prevent using it with structures, even what in C++ we call trivially-copyable standard-layout structures (you may also have heard the term "POD" for plain-old-data). – Ben Voigt May 25 '22 at 21:02
  • Does the struct contain only blittable types? (i.e. primitive value types but NOT arrays and NOT decimal). – Matthew Watson May 26 '22 at 08:01
  • Thanks everyone! Unfortunately there are non-blittable types involved – AeroClassics May 26 '22 at 14:44
  • Ah, in that case I'm afraid there are no shortcuts then. – Matthew Watson May 26 '22 at 14:47
  • Argh! Well it was worth asking! – AeroClassics Jun 02 '22 at 22:42

1 Answers1

3

This answer assumes a few things:

  1. Your structs only contain primitive value types (i.e. no arrays, no reference types, no decimal values, no string and so on).
  2. You have an array of bytes containing one or more structs at various offsets.
  3. You are using .Net Core 3.1 or later.
  4. 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:

  1. 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.
  2. 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}";
        }
    }
}
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276