Table of Contents

Structs, unions, enums, and typedefs

These declarations combine primitive values into reusable shapes. Names are case-sensitive, every referenced type must exist, and by-value storage must have a finite size or a runtime size the operation can determine safely.

Named structs

A struct places fields in declaration order:

struct point {
    int16 x;
    int16 y;
};

struct record {
    uint8 tag;
    point location;
};

In packed mode, each field starts where the previous one ended. In aligned mode, the start is rounded to the field's alignment and the final struct size is rounded to the largest field alignment. A nested struct uses its complete size, including tail padding.

For packed bytes A1 FE FF 05 00:

Path Offset Value
record.tag 0 161
record.location.x 1 -2
record.location.y 3 5

The nested-structs fixture checks size 2 for a one-byte child/root case, exact offsets and values, and exact output bytes on both frameworks.

A field's type reference may optionally carry a leading struct, union, or enum keyword, matching how C itself refers to a tagged type: record { struct point location; }; compiles to the identical field as record { point location; };. The keyword is checked against the referenced declaration's actual kind, so union point location; is rejected when point is declared as a struct - see the tag-keywords fixture.

Duplicate top-level names, duplicate fields in one struct, unknown types, and recursive by-value fields produce InvalidLayout. A recursive pointer can be valid because the pointer itself has finite width; following it is limited during reading.

Inline structs

An inline struct gives one field a nested shape without creating a reusable global type:

struct root {
    struct {
        uint8 kind;
        uint16 value;
    } item;
};

Its paths are root.item.kind and root.item.value. There is no separate type name for the inner declaration - that is already true of every inline struct, named or not.

Inline structs may nest, and a struct may nest an inline union the same way (union { ... } choice;), which reads as a UnionValue exactly like a field of a named union type. A union body may in turn hold inline structs and unions. Apart from name reuse, an inline composite follows the same placement, read, write, and update rules as a named child. An inline composite is always a single member; declare an array of a named type for repeated ones.

A tagged inline body (struct gen { ... } gen; or union version_information { ... }; inside another body) declares its tag as a global type, exactly as C and dissect do: the member-name form is then an ordinary field of that type (with every declarator shape: } gen[2];, } *p;), and the form without a member name promotes the body (the reading MSVC's anonymous-member extension and dissect give it) while still declaring the tag.

Padding fields

A field named _ is unnamed padding, the way an anonymous bitfield is: it is read and skipped, written as zeroes without a caller value, absent from every result, addressable by nobody, and free to repeat in one body. It needs fixed-size storage with a non-composite target: for example, uint32 _;, char _[3];, or an enum with fixed-width backing storage. A pointer to a non-composite type (uint8 *_;) reserves the configured pointer width and writes the null address. Struct or union targets, including pointers to them, and runtime-sized _ fields are rejected.

Writing into an existing buffer also replaces explicit _ padding with zeroes. Fixed-size custom-codec padding uses its declared storage size without invoking the codec's value encoder. These bytes count toward write limits. This differs from implicit alignment gaps between fields, which a write into existing storage leaves unchanged. Read array-element limits also apply to unnamed padding arrays, even when a fixed-layout read skips constructing their values. Debug byte ranges can still describe padding, and a union retains these bytes in its raw storage without adding an unnamed member view.

Anonymous promoted members

The member declarator itself - not the inline struct's own type, which is already always unnamed - may also be omitted. When it is, the inline struct's own fields are promoted directly into the containing struct's own namespace instead of nesting under a name of their own:

struct root {
    uint8 a;
    struct {
        uint8 x;
        uint8 y;
    };
    uint8 b;
};

Here x and y are addressable directly as root.x and root.y, not root.<something>.x. Placement, size, and alignment are completely unaffected - promotion changes only which path/mapped-class/JSON name resolves to a field, not where it lives in the stream. Parsing, serializing, writing, and updating all treat x and y as if they were declared directly on root.

Promotion is transitive: an anonymous member's own anonymous members promote all the way up to the nearest named container. A promoted member's own field names must not collide with the containing struct's own names, a sibling promoted member's names, or a transitively deeper promoted member's names - any collision anywhere in that flattened namespace is a construction-time error, naming the struct the collision becomes visible in. A named nested struct keeps its own independent namespace, unaffected by any of this - reusing a name already used by a sibling promoted member is not a collision.

The same promotion applies to an anonymous inline union, the shape Windows headers use everywhere:

struct file_name {
    uint32 attributes;
    union {
        struct { uint16 ea_size; uint16 reserved; };
        uint32 reparse_tag;
    };
    uint8 name_length;
};

ea_size, reserved, and reparse_tag are all addressable directly on file_name; every one of them is decoded from the same four bytes, because they are the union's overlapping views, and an anonymous struct inside the union promotes its members through the union. Reading gives each promoted member its decoded value (the union's raw storage is not published under a name of its own). Writing a promoted union chooses the member to encode from the members the data supplies - the widest one first, so a value that came from a parse reproduces the whole storage, and a new value needs only one member (reparse_tag, or ea_size with reserved) - and clears the rest of the union extent, exactly as UnionValue.FromMember does; supplying none of them is a write error. Updating a promoted member changes only that member's bytes.

The anonymous-promoted-member fixture checks a=1, x=2, y=3, b=4, size 4, and bytes 01020304 on both frameworks; the inline-unions fixture checks the union shape above.

Unions

A union overlays every member at one address:

union choice {
    uint8 small;
    uint32 large;
};

Its storage is large enough for the largest complete member. Its alignment is the largest member alignment; aligned mode rounds the final union size to that value.

aligned union choice, bytes A5 00 00 00
offset   0    1    2    3
storage A5   00   00   00
small   A5
large   A5   00   00   00  → 165 (little-endian)

Reading returns UnionValue with a copy of the complete raw storage and every decoded member. CStructSharp cannot infer an active member because an untagged union does not store one.

Writing an unchanged parsed union reproduces its raw storage. For new output, choose one member with UnionValue.FromMember or WithSelectedMember. The writer clears the complete union first, then writes that member, so bytes outside a smaller member become zero.

Arrays of unions use the complete union stride. A struct member inside a union begins at the union address, then its own fields proceed normally. Debug and address operations observe the same overlapping region. See the union guide.

Enums

An enum stores an integer and associates names with selected values:

enum mode : uint16 {
    Unknown = 0,
    Read = 1,
    Write = Read << 1
};

struct root {
    mode value;
};

The optional backing type must resolve to a supported fixed integral type. Without one, storage follows the rule C compilers apply: 32 bits, uint32 unless a member is written as a negative number, which selects int32. That is GCC's choice for an enum without negative members and the uint32 dissect.cstruct assumes; CStructCompilationOptions.DefaultEnumStorage names any other spelling ("byte" for the smallest storage, "int32" for MSVC's fixed int).

Members are evaluated in order. The first omitted value is zero; each later omitted value is the previous value plus one. The comma between members is optional, because a value can never be followed by a name: members separated by line breaks alone (the Windows-header habit) are unambiguous, and a trailing comma is allowed as in C. A member name may start with, or consist of, digits (32BIT_MACHINE, or 0 = 0x30 in an enum of character codes). Duplicate names, values outside the backing range, circular/unknown expression dependencies, and unsupported backing types produce InvalidLayout.

Reading returns EnumValueResult:

Property Meaning
Enum Declared enum name
Name First matching member, or null when the number is not declared
Value Exact signed or unsigned mathematical value
RawBits Backing bits represented as an unsigned number
StorageType / BitWidth / IsSigned Backing identity, width, and signedness

Unknown numbers remain valid and can be written back without narrowing. Writers accept a compatible EnumValueResult, a declared member name, or an in-range numeric value. See the enum guide.

A member is referred to from an expression by its qualified name (mode.Write); the bare name stays local to the enum. An enum or flag may be bitfield storage (mode kind : 2;): its bits live in the backing type's storage unit, which it shares with adjacent bitfields of that type or of other enums on it, and the slice reads back as an EnumValueResult. An enum with no name (enum { A = 3, B };) declares no type: its members become integer constants, counting up from zero, exactly as C treats them.

Flags

A flag is a bitmask enum. It is declared like an enum, but an omitted member value is the next unused bit rather than the previous value plus one (dissect.cstruct's rule), and reading one decomposes the stored value:

flag access : uint16 {
    READ,
    WRITE,
    EXEC,
    HIDDEN = 0x100
};

Reading returns FlagValueResult, an EnumValueResult with Names (every member whose bits are all set, in declaration order, computed on first use), Remainder (the bits no member accounts for), and Has(name); Name is still the single member equal to the whole value, when there is one. Writers additionally accept "READ|EXEC", a sequence of member names, and a [Flags] CLR enum; typed reads map to a [Flags] enum by value. An anonymous flag { ... }; declares constants like an anonymous enum. A struct holding a flag field reads through the general interpreter rather than the span fast path. The flags fixture checks the declaration above.

Typedefs

A typedef gives another name to one existing type, an optional pointer depth, and optionally a fixed array shape:

typedef uint16 word;
typedef int24< little_delta;
typedef fixed16_16> network_revision;
typedef word *word_pointer;
typedef unsigned long long ticks_t;
typedef uint8 byte_t, *pbyte_t;
typedef uint16 pair[2];
typedef struct packet {
    uint8 kind;
    word value;
}; packet_alias;

Following aliases does not change width, byte order, alignment, or pointer addressing. A finite chain reaches its final type without losing array dimensions, regardless of the number of alias declarations. Alias cycles are rejected. A field declared with an array typedef (pair p;) has the typedef's dimensions as its innermost ones (pair rows[3] is uint16 rows[3][2]), and the typedef itself can be a root (Parse(bytes, "pair") reads two values). A pointer to an array typedef and an unsized array typedef are rejected. The pointer restriction also applies through intermediate aliases, even if no field uses them: typedef pair *pair_pointer; cannot hide the array target. Wrap the array in a named struct when you need a pointer to the complete group. Every array-typedef dimension must be a nonnegative 32-bit integer count. Construction checks literals as well as expressions, even if no field uses the alias. Zero is a valid count and produces an empty array.

The typedef-struct form declares a struct and one or more aliases. The named-tag form declares the tag as a global type as well, exactly as C does, so _X, X, and PX below all resolve, and a second typedef struct _X is a duplicate. The trailing declarator list may carry pointer stars; a body with no alias at all (typedef struct NAME { ... };) declares just the tag; and typedef struct tag alias; aliases a tag declared elsewhere in the same layout (its struct/union keyword is checked against the tag's kind):

typedef struct _X { uint8 a; } X, *PX;
typedef struct { uint8 x; uint8 y; } point_t;
typedef union { uint8 small; uint16 large; } choice_t;
typedef union tagged_choice { uint8 small; uint16 large; } tagged_choice_t;
typedef struct NAME { uint8 a; };
typedef struct _X x_alias;
typedef enum _KIND : uint8 { NONE, CODE } KIND, *PKIND;
typedef flag { READ, WRITE } access_t;
typedef enum _KIND kind_alias;

The enum and flag spellings follow the same forms: a tagged body declares the tag and aliases it, an anonymous body is the alias's own enum, an alias equal to the tag never collides with itself, and typedef enum Tag Alias; aliases an enum declared elsewhere.

The typedefs fixture checks that word value reads 34 12 as 4660 and that a packed root followed by one byte has size/alignment 3/2.

Top-level declaration forms

Two more spellings appear in copied headers and dissect definitions. A body whose only name follows the closing brace declares that name as the type (it is a typedef without the keyword), and a named body may be followed by an object name, which declares nothing:

struct { uint32 tv_sec; uint32 tv_usec; } timeval;
struct exit_status { uint16 termination; uint16 exit; } status;

timeval and exit_status are types; status is not a declaration and does not reserve its name. A forward declaration (struct node;) is accepted and declares nothing: a self-referential pointer (node *next; inside struct node) never needed one, and using a forward-declared tag by value before its body is still an unknown type.

Compare the declarations

Declaration Storage Direct result Reusable name Mistake to avoid
Named struct Sequential Dynamic object or mapped class Yes Assuming host padding
Inline struct (named member) Sequential, nested Nested dynamic object No Assuming member promotion
Inline struct (anonymous member) Sequential, promoted Spliced into the parent object No Assuming a nested container still exists
Union Overlapping UnionValue Yes Guessing an active member
Enum One backing integer EnumValueResult Yes Assuming a compiler's signedness for values at bit 31
Typedef Same as its target Same as its target Alias Expecting a new ABI/layout

See Names and scopes, Layout and padding, and the complete grammar.