Unions, bitfields, and nested structs
Composite shapes are where a layout stops being a flat list of numbers. The unions and bitfields pages explain the rules with diagrams; this lesson shows what the generated code makes of them.
[CStructLayout("""
struct flags { uint8 version : 4; uint8 priority : 3; uint8 urgent : 1; };
union payload { uint32 word; uint8 octets[4]; };
struct packet { flags head; payload body; struct { uint16 x; uint16 y; } position; };
""", Root = "packet")]
public static partial class Packets
{
}
private static void GeneratedUnionsBitfieldsNested()
{
byte[] bytes = [0b1_101_0011, 0x01, 0x02, 0x03, 0x04, 0x0A, 0x00, 0x0B, 0x00];
Packets.Packet packet = Packets.Parse(bytes);
// Bitfields decode to their declared type; the offsets and shifts were computed at build time.
Equal((byte)3, packet.Head.Version);
Equal((byte)5, packet.Head.Priority);
Equal((byte)1, packet.Head.Urgent);
// A union reads every member from the same bytes and keeps the raw storage for writing back.
Equal(0x04030201u, packet.Body.Word);
SequenceEqual([1, 2, 3, 4], packet.Body.Octets);
SequenceEqual([1, 2, 3, 4], packet.Body.RawStorage!);
// An inline struct becomes a nested class named after its parent and member.
Equal((ushort)10, packet.Position.X);
Equal((ushort)11, packet.Position.Y);
Equal(0, Packets.Offsets.Head.Priority);
Equal(5, Packets.Offsets.Position.X);
// Writing a union: choose the member to write, or leave SelectedMember null to write RawStorage back.
packet.Body.SelectedMember = "word";
packet.Body.Word = 0xAABBCCDD;
byte[] written = Packets.Serialize(packet);
SequenceEqual([0xDD, 0xCC, 0xBB, 0xAA], written[1..5]);
}
Bitfields
uint8 version : 4; uint8 priority : 3; uint8 urgent : 1; share one byte. The compiler decides, at build time,
which storage unit each field lives in, at which bit offset, and whether bits are counted from the low or the high
end (BitfieldPacking and BitfieldAllocation on the attribute). The generated reader takes the unit, extracts the
field's bits with the shared Codec.ExtractBits, and stores them in the declared type - a byte here. In the
view, each bitfield accessor is a single masked read; in Offsets, a bitfield reports its storage unit's first
byte.
Byte 0b1_101_0011 gives version = 3, priority = 5, urgent = 1 with the default low-bit-first allocation.
Unions
A union is a class with every member plus two extras: RawStorage, the union's bytes as read, and
SelectedMember, which is null after a read. Reading fills every member from the same bytes - Word and
Octets overlap - because the data does not say which one is meant.
Writing has to choose. With SelectedMember set to a member's layout name, that member is written and the rest of
the union's extent is zero; with SelectedMember left null, RawStorage is written back unchanged (so a parsed
union round-trips byte for byte). A union whose members have different sizes is written into its full size, the
widest member's, exactly as the runtime writes a UnionValue.
Inline and promoted structs
struct { uint16 x; uint16 y; } position; declares a struct in place and names the member. The generator has no
tag to name the class after, so it uses the parent and the member: Packets.PacketPosition, reached through
packet.Position. Its offsets are known, so Offsets.Position.X is a constant too.
A struct declared in place without a member name (struct { uint16 ea_size; uint16 reserved; };) is
promoted: its members belong to the parent, as in C. The generated class splices them in - packet.EaSize, not
packet.Something.EaSize - and the reader reads them at their promoted offsets, the same way the runtime's
StructValue shows them.
Check yourself
- After
Packets.Parse, what ispacket.Body.SelectedMember? - Which member is written when
SelectedMemberisnull? - Why is the inline struct's class called
PacketPosition?
Answers
null: the bytes do not say which member is meant; every member andRawStorageare filled.- None of the members:
RawStorageis written, which must have the union's size. - The struct has no tag, so the generator names it after its parent (
packet) and the member (position).
Exercise
Set packet.Body.SelectedMember = "octets" and packet.Body.Octets = [9, 8, 7, 6], serialize, and read the
result back. What is Word?
Solution
The union's four bytes are 09 08 07 06; reading them back as the little-endian uint32 word gives 0x06070809.
Both members always describe the same storage.