Table of Contents

Differences from C

Portable borrows familiar declaration syntax, but a layout is not an ISO C translation unit. CStructSharp does not run a preprocessor, import headers, or ask a compiler how to place native objects.

The forms below are representative rejected fixtures stored in portable-v1.json. Tests construct each layout on .NET 8 and .NET 10 and require CStructLayoutException with code InvalidLayout.

Unsupported C forms

For a small translation, a C header might describe struct packet { unsigned short kind; unsigned int length; };. Before translating, confirm the format specification says those fields are 16 and 32 bits and stored without padding. Then use:

struct packet {
    uint16 kind;
    uint32 length;
};

With packed placement, kind occupies offsets 0–1 and length occupies offsets 2–5. With Portable aligned placement, length starts at offset 4 and the total size is 8. Choose the rule stated by the format, not by the host computer. Since the header vocabulary (unsigned short, DWORD, __u32, ...) is built in, the translation is usually the original text with its #include lines left in place (they are recorded, not resolved) and its #pragma pack lines honored as composite alignment clamps. This example establishes one format's widths; it does not prove equivalence with every native compiler.

Fixture id Representative form Why it is not accepted Portable approach
include-without-delimiters #include stdint.h An include path must be quoted or angle-bracketed to be recorded Write #include <stdint.h>; the path is recorded on CStruct.Includes and never read
unknown-preprocessor-directive #error stop Only #define, #undef, #include, #pragma, #ifdef, #ifndef, #else, and #endif are recognized Remove the line
unterminated-conditional #ifdef X with no #endif Every conditional must be closed in the same source Add the matching #endif
text-constant-in-expression #define MAGIC "CD001" then value[MAGIC] A text, byte, bare, or macro constant is published on CStruct.Constants but has no integer value Use an integer #define in expressions
tag-kind-mismatch struct root { union child value; }; where child is a struct A tag keyword is checked against the referenced declaration's actual kind Use the matching keyword, or omit it and write child value;
tag-alias-kind-mismatch typedef union tag alias; where tag is a struct The same kind check applies to a tag alias Use the matching keyword
duplicate-typedef-tag typedef struct shared {...} a; typedef struct shared {...} b; A typedef's tag is a global type, as in C, and cannot be declared twice Give each body its own tag, or use the anonymous form
pointer-to-typedef-array typedef uint16 pair[2]; pair *value; The language has no pointer-to-array storage Point at a struct that wraps the array
pointer-alias-to-typedef-array typedef uint8 pair[2]; typedef pair *middle; typedef middle link; An intermediate or unused alias cannot hide pointer-to-array storage Point at a struct that wraps the array
unsized-typedef-array typedef uint16 open[]; An array typedef needs a count in every dimension Give the alias a count, or declare the unsized array on a char/wchar field
negative-unused-typedef-array typedef uint8 invalid[-1]; Array counts must be nonnegative, even when an alias is unused Use zero for an empty array or a positive count for stored elements
dynamic-union-member union choice { uint8 count; uint8 values[count]; }; Every union member must have fixed storage, so the union's extent is known before any member is read Read the count outside the union, or wrap the runtime-sized member in a struct that is parsed on its own
runtime-sized-multidimensional-array uint8 values[count][3]; Every dimension of a multidimensional array must be a compile-time-fixed count; a fixed value[2][3] is supported (see Arrays and strings) Make every dimension a compile-time-fixed count, or flatten the runtime-sized dimension into a single-dimension array
data-sized-array-of-dynamic-element entry entries[] where entry is runtime-sized A data-sized array ([], [EOF]) needs one fixed element size to step by Give the element a fixed size, or count the elements with an earlier field
sizeof-of-dynamic-type sizeof(entry) where entry is runtime-sized sizeof folds at construction, so the type must be complete and fixed-size Name a fixed type, or compute the size from earlier fields
sizeof-of-multiplication sizeof(h*o) The argument must be a type name, not a value expression Name the intended storage type with sizeof(type); place arithmetic outside the call
unsupported-expression-call strlen(root) Only sizeof(type) and offsetof(type, field) are accepted calls; nothing ever invokes user code Use an integer expression
anonymous-flag-expression-member flag { A = N, B }; An anonymous flag's omitted member needs the previous values as literals to find the next bit Give every member of an anonymous flag a literal value, or name the flag
qualifier-not-in-closed-set _Atomic uint8 value; Only const/volatile/restrict are recognized and discarded Remove the unrecognized qualifier before construction
trailing-qualifier-position uint8 value const; Accepted qualifiers appear before the type or after a pointer star, not after the declarator name Move the qualifier to an accepted position
void-value-field void value; void has no storage; only void * (an opaque address) is a field Declare a pointer, or a real element type
unrecognized-integer-spelling intmax_t value; The accepted alias table is curated (see alias spellings); intmax_t has no single portable width Use a documented spelling, or add typedef int64 intmax_t;
floating-point-field long double value; No single portable width exists to standardize on (80-bit extended, 128-bit quad, or 64-bit, depending on compiler/target) Use float32/float64 (or the float/double aliases) when 64 bits of precision is enough
enum-value-outside-storage enum kind : uint8 { BIG = 256 }; A member must fit the declared (or defaulted) backing type; a compiler would widen the enum or reject it, dissect keeps the value and fails on write Declare a wider backing type, or omit it for the 32-bit compiler default
indexed-nested-reference uint8 v[items[0].n]; The head of a dotted reference is a scalar struct field; an array element's field is reached through its bare name after the element is read Count with n after items, or copy the value into a field of the enclosing struct
zero-width-bitfield uint8 reserved : 0; A zero width has no storage, so it cannot carry a name; C rejects it too Write the separator unnamed: uint8 : 0;
non-power-of-two-alignment uint8 value @align(3); An explicit alignment override must be a positive power of two, matching every native ABI's own alignment rule Use a power-of-two value, e.g. @align(4)
non-power-of-two-composite-alignment struct root @align(3) { uint8 value; }; A composite's own explicit alignment override must also be a positive power of two Use a power-of-two value, e.g. @align(4)
non-power-of-two-pack-pragma #pragma pack(3) A pack value is a composite alignment override and follows the same power-of-two rule Use #pragma pack(1), (2), (4), ...
offset-assertion-mismatch struct root { uint8 a; uint8 value @5; }; where value naturally lands at offset 1 An offset assertion is checked against the field's actual computed offset Correct the asserted value, or omit it if the field's placement is expected to vary
offset-assertion-on-bitfield uint8 flag : 1 @2; An offset assertion is not supported on a bitfield declarator Assert the offset of a non-bitfield sibling, or omit the assertion

Broader unsupported families include other floating types, textual macro expansion, and named compiler modes. One fixture may represent several equivalent spellings. Forms that were rejected in earlier releases and are now accepted: #include (recorded), #pragma pack (a composite alignment clamp), forward declarations (struct node;), function pointers (an opaque address, like void *), T values[] on a non-character type (zero-terminated) and T values[EOF] (read to the end), typedef T name[N];, typedef struct tag alias;, typedef declarator lists (typedef struct _X {...} X, *PX;), typedef struct NAME {...}; with no alias, top-level struct { ... } name; and struct X { ... } variable;, and the Windows/kernel/IDA/C99 alias spellings.

No host ABI inference

ABI means application binary interface: the compiler/target rules for native widths, alignment, calling convention, and related details. Portable uses explicit binary-format rules instead:

Native concept Portable behavior
C long / unsigned long width long / ulong are 64-bit unless the layout is compiled with CStructCompilationOptions.CLongWidth = 32; Windows LONG/ULONG are always 32-bit
size_t / intptr_t width As wide as the layout's configured pointer size
Plain char signedness char is one unsigned raw code unit
_Bool/C++ bool width and representation bool/_Bool is always 1 byte, canonical 0x00/0x01 write output
wchar_t width/locale wchar is one 16-bit UTF-16 code unit
Pointer width Constructor value 1, 2, 4, or 8
Enum backing Supported explicit integral type; omitted means the configured 32-bit default (or an explicit DefaultEnumStorage option)
Struct/union padding Constructor chooses packed or Portable aligned placement
Bitfield placement BitfieldPacking.SysV (GCC/Clang rule, the default) or Msvc; low-bit-first numbering by default, high-bit-first by option
Native byte order Constructor order plus optional field suffix

The core does not inspect OS, CPU, process bitness, current culture, installed compiler, system headers, target triple, or native data model. Compiler-comparison files show selected observations only; they do not add LLP64 or LP64 modes or infer a compiler from the host.

Portable versus real compilers

The table below is generated (node tools/quality/compiler-fixture.mjs table) from the observations recorded by tools/compiler-fixtures/portable-host-facts.c. Each row is one C declaration compiled with the values in contracts/quality/compiler-fixtures/shapes.json; each compiler column shows the object it produced; the Portable column names the BitfieldPacking mode(s) in which the library, given the equivalent Portable declaration (natural placement, or packed placement for the #pragma pack(1) rows), produces the same bytes. long rows use CLongWidth matching the compiler's long. Signed bitfields match in bytes; the Portable value is the unsigned slice.

Shape C declaration Portable GCC 15.2.0 (Linux x64, SysV ABI)
bits-u8-u16 struct { uint8_t a:4; uint16_t b:4; } SysV, Msvc (modelled, no msvc baseline yet) size 2, align 2: AF00
bits-u8-u8-u16 struct { uint8_t a:3; uint8_t b:5; uint16_t c; } SysV, Msvc (modelled, no msvc baseline yet) size 4, align 2: FF00CDAB
bits-u32-3-29-1 struct { uint32_t a:3; uint32_t b:29; uint32_t c:1; } SysV, Msvc (modelled, no msvc baseline yet) size 8, align 4: FFFFFFFF01000000
bits-u16-15-u8-2 struct { uint16_t a:15; uint8_t b:2; } SysV, Msvc (modelled, no msvc baseline yet) size 4, align 2: FF7F0300
bits-zero-width struct { uint8_t a:3; uint8_t :0; uint8_t b:3; } SysV, Msvc (modelled, no msvc baseline yet) size 2, align 1: 0707
bits-zero-width-u32 struct { uint8_t a:3; uint32_t :0; uint8_t b:3; } SysV, Msvc (modelled, no msvc baseline yet) size 5, align 1: 0700000007
bits-signed struct { int8_t a:3; uint8_t b:5; } /* a = -1 */ SysV, Msvc (modelled, no msvc baseline yet) size 1, align 1: FF
bits-u8-6-6 struct { uint8_t a:6; uint8_t b:6; } SysV, Msvc (modelled, no msvc baseline yet) size 2, align 1: 3F3F
bits-u64-u8 struct { uint64_t a:4; uint8_t b:4; } SysV, Msvc (modelled, no msvc baseline yet) size 8, align 8: FF00000000000000
bits-after-byte struct { uint8_t x; uint32_t a:4; uint8_t b:4; } SysV, Msvc (modelled, no msvc baseline yet) size 4, align 4: AAFF0000
u64-after-u8 struct { uint8_t a; uint64_t b; } SysV, Msvc (modelled, no msvc baseline yet) size 16, align 8: 11000000000000001122334455667788
double-after-u8 struct { uint8_t a; double b; } SysV, Msvc (modelled, no msvc baseline yet) size 16, align 8: 1100000000000000000000000000F83F
long struct { uint8_t a; long b; } SysV, Msvc (modelled, no msvc baseline yet) size 16, align 8: 11000000000000007856341200000000
enum-large struct { uint8_t a; enum { BIG = 0x7FFFFFFF } b; } SysV, Msvc (modelled, no msvc baseline yet) size 8, align 4: 11000000FFFFFF7F
bool struct { uint8_t a; _Bool b; uint16_t c; } SysV, Msvc (modelled, no msvc baseline yet) size 4, align 2: 11013322
pack2-array #pragma pack(2) struct { uint8_t a; uint32_t b[2]; uint8_t c; } SysV, Msvc (modelled, no msvc baseline yet) size 12, align 2: 11005544332299887766AA00
nested-align struct inner { uint8_t a; uint32_t b; }; struct { uint8_t x; struct inner in; uint8_t y; } SysV, Msvc (modelled, no msvc baseline yet) size 16, align 4: 11000000220000006655443377000000
union-size union { uint8_t a; uint32_t b; uint16_t c[3]; } /* c set */ SysV, Msvc (modelled, no msvc baseline yet) size 8, align 4: 2211443366550000
packed-bits-u8-u16 #pragma pack(1) struct { uint8_t a:4; uint16_t b:4; } SysV, Msvc (modelled, no msvc baseline yet) size 1, align 1: AF
packed-bits-u8-6-6 #pragma pack(1) struct { uint8_t a:6; uint8_t b:6; } SysV, Msvc (modelled, no msvc baseline yet) size 2, align 1: FF0F
packed-bits-u16-15-u8-2 #pragma pack(1) struct { uint16_t a:15; uint8_t b:2; } SysV, Msvc (modelled, no msvc baseline yet) size 3, align 1: FFFF01
packed-bits-after-byte #pragma pack(1) struct { uint8_t x; uint32_t a:4; uint8_t b:4; } SysV, Msvc (modelled, no msvc baseline yet) size 2, align 1: AAFF

Baselines: GCC 15.2.0 on Linux x64 (x86_64-linux-gnu). The Portable column names the BitfieldPacking mode(s) in which the library reproduces the compiler of the same ABI family byte for byte; CompilerDifferentialFixtureTests verifies every claim against every baseline. Compilers not recorded here (MSVC, clang-cl, macOS clang, 32-bit targets) are recorded by the compiler-fixtures workflow when it runs.

The limited preprocessor

#define NAME expression binds an integer expression. It is not textual macro expansion: a name is never substituted into later source text, there are no parameters, defined, header search, compiler built-ins, casts, sizeof, or target macros. #define NAME "text", #define NAME b"bytes", a bare #define NAME, and a function-like #define NAME(args) ... are accepted so a pasted header keeps its magic strings and guards; they are published on CStruct.Constants and take no part in layout expressions. #ifdef/#ifndef/#else/#endif select declarations by the names defined so far (plus CStructCompilationOptions.Defined), #undef removes a constant for the rest of the source, #include is recorded on CStruct.Includes, #pragma pack(...) clamps the alignment of the composites that follow it, and any other #pragma is ignored. A backslash before a line end joins the lines, as in C.

One operation may supply an integer variable that overrides a matching definition. See Expressions, defines, and runtime variables.

Other invalid layouts

Portable also rejects duplicate names, unknown types, circular aliases/definitions, recursive by-value composites, invalid enum backing/ranges, bit widths outside storage, negative/overflowing array counts, and layout dependencies that cannot produce finite storage.

Recursive structures are possible through pointers to real named declarations and remain subject to traversal limits. Use the grammar, primitive table, and feature table for accepted forms.