Struct within Struct, able to change inner Struct type
Solution 1
public struct PACKET_DATA
{
public IData data;
public T GetData<T>() where T : IDATA
{
return (T)data;
}
}
public interface IDATA { }
public struct DATA_MESSAGE : IDATA
{
public string message;
}
public struct DATA_FILE : IDATA
{
public string fileName;
public long fileSize;
}
PACKET_DATA packetData = new PACKET_DATA();
packetData.data = new DATA_MESSAGE();
var message = packetData.GetData<DATA_MESSAGE>().message;
Solution 2
This question needs a clear answer, so I'll try to sum up:
If you want to take a C# data structure and convert it into a byte array, you can do it with structs and Marshaling, or with classes (or structs, but why would you) and a serialization framework (like BinaryFormatter
), or custom serialization logic (like with BinaryWriter
). We could have a debate about which is better, but lets assume for now that we're going with structs, and we're using Marshaling. Although I will say, that structs are very limited, and should be used mainly as necessary for interop with Win32 API functions.
So the issue is, we have a container struct, which may contain one of two types of child structs. If you're going to Marshal a struct, things like generics and or using a common Interface for your child struct types are not going to fly. Basically you're only option is to have the container have both structs and a bool flag indicating which of the structures is to be used. This has the downside of increasing the size of your packets because you're sending the unused child struct as well.
In the case at hand, the result looks like this:
public struct PACKET_DATA
{
public Command command;
public string data;
public bool is_message_packet;
public DATA_MESSAGE data_message;
public DATA_FILE data_file;
};
That said, in your case using structs and Marshalling is only really going to work within your own process, because your structs contain Strings. When a struct contains pointers to non-fixedlength strings, those strings are allocated elsewhere and will not be part of the byte array you copy, only pointers to them will be. You also need to call Marshal.DestroyStructure at some point, with the IntPtr you passed to StructureToPtr, in order to clean up these string resources.
So the moral of the story: can you make structs that do what you asked initially: yes. Should you be using them like you are: no. Because you have a variable size data structure that you are trying to send over a network (I presume since the struct is called PACKET), structs are not going to work, you really need to use some kind of serialization framework or custom serialization logic.

Metalstorm
Updated on June 08, 2022Comments
-
Metalstorm 12 months
There isn't much explaining to this, this is what I have:
public struct PACKET_HEADER { public string computerIp; public string computerName; public string computerCustomName; }; public struct PACKET { public PACKET_HEADER pktHdr; public PACKET_DATA pktData; }; public struct PACKET_DATA { public Command command; public string data; }; public struct DATA_MESSAGE { public string message; }; public struct DATA_FILE { public string fileName; public long fileSize; };
Basically I want the data field in PACKET_DATA to be able to be either DATA_FILE or DATA_MESSAGE. I know the type needs to be changed but I don't know what to, is generics an option?
the end result should be so that I can do either:
pktData.data.fileName or pktData.data.message
EDIT
i could do:
public struct PACKET_DATA { public Command command; public string data; public DATA_MESSAGE data_message; public DATA_FILE data_file; };
and just set the data_message or file to null when ever i don't need them? how would this impact the serialization / byte array and the data being sent. If I used classes would I not have the same problem
EDIT 2
public struct PACKET_MESSAGE { public PACKET_HEADER pktHdr; public Command command; public DATA_MESSAGE pktData; }; public struct PACKET_FILE { public PACKET_HEADER pktHdr; public Command command; public DATA_FILE pktData; };
Edit 3
I have a sterilizer and de-sterilizer that works with my original example, if there are no hiccups with that then the actual serialization is done.
EDIT 4
everything seems to be working, apart from one things my serializer is getting "Attempted to read or write protected memory. This is often an indication that other memory is corrupt." gunna have a look at it when post my working solution :)
EDIT 5
public static byte[] Serialize(object anything) { int rawsize = Marshal.SizeOf(anything); byte[] rawdatas = new byte[rawsize]; GCHandle handle = GCHandle.Alloc(rawdatas, GCHandleType.Pinned); IntPtr buffer = handle.AddrOfPinnedObject(); Marshal.StructureToPtr(anything, buffer, false); handle.Free(); return rawdatas; } public static object Deserialize(byte[] rawdatas, Type anytype) { int rawsize = Marshal.SizeOf(anytype); if (rawsize > rawdatas.Length) return null; GCHandle handle = GCHandle.Alloc(rawdatas, GCHandleType.Pinned); IntPtr buffer = handle.AddrOfPinnedObject(); object retobj = Marshal.PtrToStructure(buffer, anytype); handle.Free(); return retobj; }
FINAL
The structs:
public struct PACKET_HEADER { public string computerIp; public string computerName; public string computerCustomName; }; public struct PACKET { public PACKET_HEADER pktHdr; public PACKET_DATA pktData; }; public struct PACKET_DATA { public Command command; public IDATA data; public T GetData<T>() where T : IDATA { return (T)(data); } } public interface IDATA { } public struct DATA_MESSAGE : IDATA { public string message; } public struct DATA_FILE : IDATA { public string fileName; public long fileSize; }
How to create a new Packet (probally could combine together tbh):
public static PACKET CreatePacket(Command command) { PACKET packet; packet.pktHdr.computerIp = Settings.ComputerIP; packet.pktHdr.computerName = Settings.ComputerName; packet.pktHdr.computerCustomName = Settings.ComputerCustomName; packet.pktData.command = command; packet.pktData.data = null; return packet; } public static PACKET CreatePacket(Command command, DATA_MESSAGE data_message) { PACKET packet; packet.pktHdr.computerIp = Settings.ComputerIP; packet.pktHdr.computerName = Settings.ComputerName; packet.pktHdr.computerCustomName = Settings.ComputerCustomName; packet.pktData.command = command; packet.pktData.data = data_message; return packet; } public static PACKET CreatePacket(Command command, DATA_FILE data_file) { PACKET packet; packet.pktHdr.computerIp = Settings.ComputerIP; packet.pktHdr.computerName = Settings.ComputerName; packet.pktHdr.computerCustomName = Settings.ComputerCustomName; packet.pktData.command = command; packet.pktData.data = data_file; return packet; }
(de) serialization above.
Simple example:
PACKET packet = Packet.CreatePacket(command, data_file); byte[] byData = Packet.Serialize(packet);
other end:
PACKET returnPacket = (PACKET)Packet.Deserialize(socketData.dataBuffer, typeof(PACKET)); // Get file string fileName = returnPacket.pktData.GetData<DATA_FILE>().fileName; long fileSize = returnPacket.pktData.GetData<DATA_FILE>().fileSize;
All seems to be working nice and dandy :)
-
Metalstorm over 12 years"You can do it with generics, but then a type parameter will propagate to the PACKET struct which I'm guessing will make it awkward to work with and not be what you want." yep i had a look at that but indeed it does get very messy. The Packet struct ( and its contents) gets serialized and send over a network as a byte array. The purpose is just to store a small amount of information for a short amount of time, i read that structs are better for this rather than classes.
-
Joe Albahari over 12 yearsIs it mainly that you want efficient serialization? If so, the optimial approach is to write a custom serializer. The overhead of making these types classes instead of structs (in terms of the GC) is not great.
-
Metalstorm over 12 yearspublic struct PACKET { public PACKET_HEADER pktHdr; public PACKET_DATA<> pktData; }; isnt that the problem like before? what goes between the <> if its T then the PACKET struct will also have to take a T ? making it messy
-
Metalstorm over 12 yearsthis looks nice, however when i try PACKET packet; packet.pktData.data. nothing comes next apart from the standard ToString etc
-
Metalstorm over 12 yearsive tried implementing this, it look all good apart from under the T in "return T(data);" it comes back with the error 'T' is a 'type parameter' but is used like a 'variable'
-
Metalstorm over 12 yearsHave a look at my final edit Paul, all seems to be working fine
-
Cheng Chen over 12 years@Metalstorm: It's a typo. Should be
(T)data
. -
Paul Wheeler over 12 yearsI think the fact that it is "working" is a fantasy because you're using test code in a single process. Take that code, and try sending the data via a network socket, and I'm almost certain it will fail. This is because the strings your struct contains are not stored with the struct, the marshaled struct just has pointers to them, they are allocated elsewhere.
-
Paul Wheeler over 12 yearsAlso, when a struct contains an interface member, it treats that member as a reference. In other words, the parent struct contains a pointer, instead of containing the child struct inline. This definitely will not fly.