I want to use structs as a container for data packets for asynchronous networking in C#. Found out that you can create a union style struct without the need to mark the struct itself as unsafe--instead marking the field as unsafe.
Example:
[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Ansi, Size = 8, Pack = 4)]
struct DataPacketStruct {
private unsafe fixed byte bytes[8];
// Publicly accessible fields.
public byte header;
public int size;
public void Serialize(ref byte[] buffer, int startIndex) {
unsafe {
for (int i = 0; i < 8; i++) buffer[startIndex + i] = bytes[i];
}
}
public void Deserialize(byte[] buffer) {
unsafe {
for (int i = 0; i < 8; i++) bytes[i] = buffer[i];
}
}
public int SizeOf() { unsafe { return sizeof(DataPacketStruct); } }
}
I know I need array out of bounds checking--but apart from that. Potential downsides or undefined behaviors? Or is this usage valid? Also are there any potential performance concerns or any alternatives with similar performance?
Also an issue I can see right off the bat is not having compile-time numeric constants like in C++. Unfortunately have to hard code in the field offsets and structure size because of this: Size = 8
and byte[8]
.
1 Answer 1
There's a safe
way to do that using System.Runtime.InteropServices.Marshal
(link).
[StructLayout(LayoutKind.Sequential, Size = 8, Pack = 4)]
struct DataPacketStruct
{
public byte Header { get; set; }
public int Size { get; set; }
public void Serialize(byte[] buffer, int startIndex)
{
int size = SizeOf();
IntPtr ptr = Marshal.AllocHGlobal(size);
Marshal.StructureToPtr(this, ptr, true);
Marshal.Copy(ptr, buffer, startIndex, size);
Marshal.FreeHGlobal(ptr);
}
public static DataPacketStruct Deserialize(byte[] buffer, int startIndex)
{
var result = new DataPacketStruct();
int size = result.SizeOf();
IntPtr ptr = Marshal.AllocHGlobal(size);
Marshal.Copy(buffer, startIndex, ptr, size);
result = (DataPacketStruct)Marshal.PtrToStructure(ptr, typeof(DataPacketStruct));
Marshal.FreeHGlobal(ptr);
return result;
}
public int SizeOf() => Marshal.SizeOf(this);
}
Also I used public properties and followed the Naming Guidelines in the example.
static void Main(string[] args)
{
var data = new DataPacketStruct() { Header = byte.MaxValue, Size = int.MaxValue };
byte[] a = new byte[8];
data.Serialize(a, 0);
Console.WriteLine(string.Join(" ", a.Select(x => x.ToString("X2"))));
var data2 = DataPacketStruct.Deserialize(a, 0);
Console.WriteLine(JsonSerializer.Serialize(data2, new JsonSerializerOptions { WriteIndented = true }));
Console.ReadKey();
}
Output:
FF 00 00 00 FF FF FF 7F
{
"Header": 255,
"Size": 2147483647
}
-
1\$\begingroup\$ Nice! I've seen this implementation using Marshal, problem is if you run a test on it, I bet it's not nearly as fast. \$\endgroup\$FatalSleep– FatalSleep2020年11月29日 09:24:24 +00:00Commented Nov 29, 2020 at 9:24
-
1\$\begingroup\$ @FatalSleep there's no performance concern in the question above. Anyway, you may test it as you have the more real environment. In production environment I prefer something like JSON and HTTPS to transfer the data. It's more safe in terms of security. Writing network input directly to memory can be done only if you 100% trust the source. For socket-like data exchange i use WebSockets. That allows me to make browser client and desktop or mobile app clients using the same server. \$\endgroup\$aepot– aepot2020年11月29日 09:34:12 +00:00Commented Nov 29, 2020 at 9:34
-
\$\begingroup\$ I have updated my question accordingly. I like the answer, but it doesn't really address the question, more so just says do it
safe
. Which, while I am using an unsafe methodology here, I did it in a specific manner to avoid having to mark the struct unsafe. Which normally one wouldn't do, but from a safety aspect not much is happening here IMO that is entirely problematic. \$\endgroup\$FatalSleep– FatalSleep2020年11月29日 09:51:32 +00:00Commented Nov 29, 2020 at 9:51 -
1\$\begingroup\$ @FatalSleep Thank you for the feedback. I'm not sure that your way guarantee GC will not move the fields across the memory. You're making
unsafe
access to managed fields with nofixed
directive which tells GC not to move that fields. That's why i can't confirm the correctness of your solution. By the first sight it looks suspicious. Maybe I'm wrong. \$\endgroup\$aepot– aepot2020年11月29日 10:10:01 +00:00Commented Nov 29, 2020 at 10:10 -
\$\begingroup\$ could you put that in your answer? \$\endgroup\$FatalSleep– FatalSleep2020年11月29日 22:24:21 +00:00Commented Nov 29, 2020 at 22:24
unsafe
stuff. What's the purpose of the structure? Can you show and explain the usage example?Explicit
structure can be needed for marshaling it to P/Invoke unmanaged function call e.g. send it as byte array. As option you may useBinaryWriter
as managed alternative if you need just a byte array not P/Invoke. \$\endgroup\$Buffer.BlockCopy
fromSystem.Buffers
as loop replacement. \$\endgroup\$