I am allocating some unmanaged memory in my application via Marshal.AllocHGlobal
. I'm then copying a set of bytes to this location and converting the resulting segment of memory to a struct
before freeing the memory again via Marshal.FreeHGlobal
.
Here's the method:
public static T Deserialize<T>(byte[] messageBytes, int start, int length)
where T : struct
{
if (start + length > messageBytes.Length)
throw new ArgumentOutOfRangeException();
int typeSize = Marshal.SizeOf(typeof(T));
int bytesToCopy = Math.Min(typeSize, length);
IntPtr targetBytes = Marshal.AllocHGlobal(typeSize);
Marshal.Copy(messageBytes, start, targetBytes, bytesToCopy);
if (length < typeSize)
{
// Zero out additional bytes at the end of the struct
}
T item = (T)Marshal.PtrToStructure(targetBytes, typeof(T));
Marshal.FreeHGlobal(targetBytes);
return item;
}
This works for the most part, however if I have fewer bytes than the size of the struct
requires, then 'random' values are assigned to the last fields (I am using LayoutKind.Sequential
on the target struct). I'd like to zero out these hanging fields as efficiently as possible.
For context, this code is deserializing high-frequency multicast messages sent from C++ on Linux.
Here is a failing test case:
// Give only one byte, which is too few for the struct
var s3 = MessageSerializer.Deserialize<S3>(new[] { (byte)0x21 });
Assert.AreEqual(0x21, s3.Byte);
Assert.AreEqual(0x0000, s3.Int); // hanging field should be zero, but isn't
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
private struct S3
{
public byte Byte;
public int Int;
}
Running this test repeatedly causes the second assert to fail with a different value each time.
EDIT
In the end, I used leppie's suggestion of going unsafe
and using stackalloc
. This allocated a byte array that was zeroed as needed, and improved throughput from between 50% and 100%, depending upon the message size (larger messages saw greater benefit).
The final method ended up resembling:
public static T Deserialize<T>(byte[] messageBytes, int startIndex, int length)
where T : struct
{
if (length <= 0)
throw new ArgumentOutOfRangeException("length", length, "Must be greater than zero.");
if (startIndex < 0)
throw new ArgumentOutOfRangeException("startIndex", startIndex, "Must be greater than or equal to zero.");
if (startIndex + length > messageBytes.Length)
throw new ArgumentOutOfRangeException("length", length, "startIndex + length must be <= messageBytes.Length");
int typeSize = Marshal.SizeOf(typeof(T));
unsafe
{
byte* basePtr = stackalloc byte[typeSize];
byte* b = basePtr;
int end = startIndex + Math.Min(length, typeSize);
for (int srcPos = startIndex; srcPos < end; srcPos++)
*b++ = messageBytes[srcPos];
return (T)Marshal.PtrToStructure(new IntPtr(basePtr), typeof(T));
}
}
Unfortunately this still requires a call to Marshal.PtrToStructure
to convert the bytes into the target type.