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
non-integer-field-in-expression char tag[4] then value[tag] Text, arrays, structs, unions, floating-point, fixed-point and UUID fields have no single integer value Read the field as an integer or an enum, or use a numeric field
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
count-on-non-pointer uint8 value @count(2); @count(N) counts the elements a pointer targets, so it needs a pointer Declare the array uint8 value[2];, or make the field a pointer
count-on-pointer-array uint8 *values[2] @count(2); One count cannot describe the targets of several pointers Point at a struct that holds the pointer and its count, and declare an array of that struct
count-on-void-pointer void *raw @count(2); A void * has no element type to count Point at the element type, such as uint8 *raw @count(2);
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
offset-assertion-names-a-field struct root { uint8 n; uint8 value @n; }; An offset assertion is a constant, like @align(N): it can use numbers and #defines, but not fields or a caller's variables Assert a constant offset, or use a #define

Broader unsupported families include other floating types, textual macro expansion, and named compiler modes. One fixture may represent several equivalent spellings.

Some C forms are accepted with a narrower meaning than a compiler gives them: #include is recorded, not resolved; #pragma pack clamps composite alignment; a function pointer is an opaque address, like void *; and T values[] on a non-character type reads up to an all-zero element, while T values[EOF] reads to the end of the input. These are accepted as in C: forward declarations (struct node;), 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 and 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. SysV on x86 is SysV placement with four-byte pointers, compared with a 32-bit x86 compiler: the i386 System V ABI aligns eight-byte scalars (uint64_t, double) to four bytes inside a struct, while Portable natural placement aligns them to eight, so the rows with such a member do not match there.

Shape C declaration Portable Clang 18.1.3 (Linux x64, SysV ABI) GCC 13.3.0 (Linux x64, SysV ABI) GCC 13.3.0 (Linux x86, SysV ABI) Clang 21.0.0 (macOS arm64, SysV ABI) Clang 20.1.8 (Windows x64, MSVC ABI) MSVC 19.51.36257 (Windows x64, MSVC ABI)
bits-u8-u16 struct { uint8_t a:4; uint16_t b:4; } SysV, SysV on x86, Msvc size 2, align 2: AF00 size 2, align 2: AF00 size 2, align 2: AF00 size 2, align 2: AF00 size 4, align 2: 0F000A00 size 4, align 2: 0F000A00
bits-u8-u8-u16 struct { uint8_t a:3; uint8_t b:5; uint16_t c; } SysV, SysV on x86, Msvc size 4, align 2: FF00CDAB size 4, align 2: FF00CDAB size 4, align 2: FF00CDAB size 4, align 2: FF00CDAB size 4, align 2: FF00CDAB size 4, align 2: FF00CDAB
bits-u32-3-29-1 struct { uint32_t a:3; uint32_t b:29; uint32_t c:1; } SysV, SysV on x86, Msvc size 8, align 4: FFFFFFFF01000000 size 8, align 4: FFFFFFFF01000000 size 8, align 4: FFFFFFFF01000000 size 8, align 4: FFFFFFFF01000000 size 8, align 4: FFFFFFFF01000000 size 8, align 4: FFFFFFFF01000000
bits-u16-15-u8-2 struct { uint16_t a:15; uint8_t b:2; } SysV, SysV on x86, Msvc size 4, align 2: FF7F0300 size 4, align 2: FF7F0300 size 4, align 2: FF7F0300 size 4, align 2: FF7F0300 size 4, align 2: FF7F0300 size 4, align 2: FF7F0300
bits-zero-width struct { uint8_t a:3; uint8_t :0; uint8_t b:3; } SysV, SysV on x86, Msvc size 2, align 1: 0707 size 2, align 1: 0707 size 2, align 1: 0707 size 2, align 1: 0707 size 2, align 1: 0707 size 2, align 1: 0707
bits-zero-width-u32 struct { uint8_t a:3; uint32_t :0; uint8_t b:3; } SysV, SysV on x86, Msvc size 5, align 1: 0700000007 size 5, align 1: 0700000007 size 5, align 1: 0700000007 size 5, align 1: 0700000007 size 8, align 4: 0700000007000000 size 8, align 4: 0700000007000000
bits-signed struct { int8_t a:3; uint8_t b:5; } /* a = -1 */ SysV, SysV on x86, Msvc size 1, align 1: FF size 1, align 1: FF size 1, align 1: FF size 1, align 1: FF size 1, align 1: FF size 1, align 1: FF
bits-u8-6-6 struct { uint8_t a:6; uint8_t b:6; } SysV, SysV on x86, Msvc size 2, align 1: 3F3F size 2, align 1: 3F3F size 2, align 1: 3F3F size 2, align 1: 3F3F size 2, align 1: 3F3F size 2, align 1: 3F3F
bits-u64-u8 struct { uint64_t a:4; uint8_t b:4; } SysV, Msvc size 8, align 8: FF00000000000000 size 8, align 8: FF00000000000000 size 4, align 4: FF000000 size 8, align 8: FF00000000000000 size 16, align 8: 0F000000000000000F00000000000000 size 16, align 8: 0F000000000000000F00000000000000
bits-after-byte struct { uint8_t x; uint32_t a:4; uint8_t b:4; } SysV, SysV on x86, Msvc size 4, align 4: AAFF0000 size 4, align 4: AAFF0000 size 4, align 4: AAFF0000 size 4, align 4: AAFF0000 size 12, align 4: AA0000000F0000000F000000 size 12, align 4: AA0000000F0000000F000000
u64-after-u8 struct { uint8_t a; uint64_t b; } SysV, Msvc size 16, align 8: 11000000000000001122334455667788 size 16, align 8: 11000000000000001122334455667788 size 12, align 4: 110000001122334455667788 size 16, align 8: 11000000000000001122334455667788 size 16, align 8: 11000000000000001122334455667788 size 16, align 8: 11000000000000001122334455667788
double-after-u8 struct { uint8_t a; double b; } SysV, Msvc size 16, align 8: 1100000000000000000000000000F83F size 16, align 8: 1100000000000000000000000000F83F size 12, align 4: 11000000000000000000F83F size 16, align 8: 1100000000000000000000000000F83F size 16, align 8: 1100000000000000000000000000F83F size 16, align 8: 1100000000000000000000000000F83F
long struct { uint8_t a; long b; } SysV, SysV on x86, Msvc size 16, align 8: 11000000000000007856341200000000 size 16, align 8: 11000000000000007856341200000000 size 8, align 4: 1100000078563412 size 16, align 8: 11000000000000007856341200000000 size 8, align 4: 1100000078563412 size 8, align 4: 1100000078563412
enum-large struct { uint8_t a; enum { BIG = 0x7FFFFFFF } b; } SysV, SysV on x86, Msvc size 8, align 4: 11000000FFFFFF7F size 8, align 4: 11000000FFFFFF7F size 8, align 4: 11000000FFFFFF7F size 8, align 4: 11000000FFFFFF7F size 8, align 4: 11000000FFFFFF7F size 8, align 4: 11000000FFFFFF7F
bool struct { uint8_t a; _Bool b; uint16_t c; } SysV, SysV on x86, Msvc size 4, align 2: 11013322 size 4, align 2: 11013322 size 4, align 2: 11013322 size 4, align 2: 11013322 size 4, align 2: 11013322 size 4, align 2: 11013322
pack2-array #pragma pack(2) struct { uint8_t a; uint32_t b[2]; uint8_t c; } SysV, SysV on x86, Msvc size 12, align 2: 11005544332299887766AA00 size 12, align 2: 11005544332299887766AA00 size 12, align 2: 11005544332299887766AA00 size 12, align 2: 11005544332299887766AA00 size 12, align 2: 11005544332299887766AA00 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, SysV on x86, Msvc size 16, align 4: 11000000220000006655443377000000 size 16, align 4: 11000000220000006655443377000000 size 16, align 4: 11000000220000006655443377000000 size 16, align 4: 11000000220000006655443377000000 size 16, align 4: 11000000220000006655443377000000 size 16, align 4: 11000000220000006655443377000000
union-size union { uint8_t a; uint32_t b; uint16_t c[3]; } /* c set */ SysV, SysV on x86, Msvc size 8, align 4: 2211443366550000 size 8, align 4: 2211443366550000 size 8, align 4: 2211443366550000 size 8, align 4: 2211443366550000 size 8, align 4: 2211443366550000 size 8, align 4: 2211443366550000
packed-bits-u8-u16 #pragma pack(1) struct { uint8_t a:4; uint16_t b:4; } SysV, SysV on x86, Msvc size 1, align 1: AF size 1, align 1: AF size 1, align 1: AF size 1, align 1: AF size 3, align 1: 0F0A00 size 3, align 1: 0F0A00
packed-bits-u8-6-6 #pragma pack(1) struct { uint8_t a:6; uint8_t b:6; } SysV, SysV on x86, Msvc size 2, align 1: FF0F size 2, align 1: FF0F size 2, align 1: FF0F size 2, align 1: FF0F size 2, align 1: 3F3F size 2, align 1: 3F3F
packed-bits-u16-15-u8-2 #pragma pack(1) struct { uint16_t a:15; uint8_t b:2; } SysV, SysV on x86, Msvc size 3, align 1: FFFF01 size 3, align 1: FFFF01 size 3, align 1: FFFF01 size 3, align 1: FFFF01 size 3, align 1: FF7F03 size 3, align 1: FF7F03
packed-bits-after-byte #pragma pack(1) struct { uint8_t x; uint32_t a:4; uint8_t b:4; } SysV, SysV on x86, Msvc size 2, align 1: AAFF size 2, align 1: AAFF size 2, align 1: AAFF size 2, align 1: AAFF size 6, align 1: AA0F0000000F size 6, align 1: AA0F0000000F

Baselines: Clang 18.1.3 on Linux x64 (x86_64-pc-linux-gnu); GCC 13.3.0 on Linux x64 (x86_64-linux-gnu); GCC 13.3.0 on Linux x86 (i686-linux-gnu); Clang 21.0.0 on macOS arm64 (arm64-apple-darwin25.6.0); Clang 20.1.8 on Windows x64 (x86_64-pc-windows-msvc); MSVC 19.51.36257 on Windows x64 (x64-pc-windows-msvc). 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. The manually started compiler-fixtures workflow records the baselines again.

Integer arithmetic in expressions

In C, every integer expression has a type, and the type decides the arithmetic. A constant's type depends on its value and spelling: with a 32-bit int, 0xFFFFFFFF does not fit int, so it is an unsigned int (ISO C11 §6.4.4.1). Before an operator runs, the usual arithmetic conversions promote small types to int and then convert both operands to one common type, which may be unsigned (§6.3.1.1, §6.3.1.8). Unsigned arithmetic wraps around, and signed overflow is undefined behavior (§6.5). These rules follow the machine word, which is what made early C fast, but they can surprise a reader: in C, -1 < 0xFFFFFFFF is false, because -1 is converted to the unsigned value 4294967295 before the comparison.

CStructSharp expressions have no types. Every expression is evaluated in one signed 128-bit range, which holds every value of every field up to 64 bits exactly, and a result outside that range is an error rather than a wraparound (see Expressions use exact 128-bit integers). So -1 < 0xFFFFFFFF is 1 (true), and 0xFFFFFFFFFFFFFFFF + 1 is 18446744073709551616. A header expression that relies on unsigned wraparound needs its intended value written out: C's ~0u is 4294967295 with a 32-bit unsigned, but ~0 here is -1, so write the mask 0xFFFFFFFF instead.

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.