structpad is an offline command-line tool for people who design binary records, network packets, and FFI structures. It parses a small struct description language, reports field offsets and padding for the selected ABI, suggests a lower-padding field order, and can fail a CI job when padding exceeds a limit. It reads input without changing it and uses only the Python standard library at runtime.
Install the package with Python 3.11 or newer:
pip install .You can run the command as structpad or use the module form:
python -m structpad --helpSave this example as net.sp:
// wire structs
struct Packet {
u8 tag;
u32 length;
u16 flags;
char name[10];
ptr next;
}
packed struct Wire {
u8 kind;
u32 value;
}
struct Frame {
u8 version;
struct Packet head;
struct Wire tail[2];
bool ok;
}
Show each struct's field offsets, sizes, alignment, and padding:
structpad layout net.spFor the example above, the layout is:
struct Packet: size 32, align 8, padding 7
offset size field
0 1 tag: u8
1 3 (padding)
4 4 length: u32
8 2 flags: u16
10 10 name: char[10]
20 4 (padding)
24 8 next: ptr
struct Wire: size 5, align 1, padding 0, packed
offset size field
0 1 kind: u8
1 4 value: u32
struct Frame: size 56, align 8, padding 12
offset size field
0 1 version: u8
1 7 (padding)
8 32 head: struct Packet
40 10 tail: struct Wire[2]
50 1 ok: bool
51 5 (padding)
Suggest an order with less padding:
structpad suggest net.spstruct Packet: size 32, already compact
struct Wire: packed, size 5, nothing to reorder
struct Frame: size 56 -> 48 (saves 8)
struct Packet head;
u8 version;
struct Wire tail[2];
bool ok;
Check structs against a padding limit; the default limit is zero bytes:
structpad check --max-padding 8 net.spFrame: 12 bytes of padding (limit 8)
1 of 3 structs over the padding limit
The check exits with status 1 when any struct exceeds the limit.
--ignore NAME is available only with check and may be repeated; --ignore=NAME is also supported. Ignored structs are omitted from the padding check but remain included in the summary's total. The summary ends with the number of distinct ignored names, so duplicates do not increase that count.
structpad check --ignore Packet net.spFrame: 12 bytes of padding (limit 0)
1 of 3 structs over the padding limit (1 ignored)
Ignoring both Packet and Frame prints ok, 3 structs (2 ignored) and exits with status 0. Invalid names such as 1x produce invalid --ignore "1x"; a name not defined in the file produces unknown struct "Nope" in --ignore. Both errors exit with status 2.
--struct NAME is available only with layout and may be repeated; --struct=NAME is also supported. Only the named structs are printed, once each and in file order, with the same numbers as in the full listing, because nested structs are still resolved from the whole file.
structpad layout --struct Frame --struct Wire net.spstruct Wire: size 5, align 1, padding 0, packed
offset size field
0 1 kind: u8
1 4 value: u32
struct Frame: size 56, align 8, padding 12
offset size field
0 1 version: u8
1 7 (padding)
8 32 head: struct Packet
40 10 tail: struct Wire[2]
50 1 ok: bool
51 5 (padding)
An invalid name such as 1x produces invalid --struct "1x", a name not defined in the file produces unknown struct "Nope" in --struct, and --struct with suggest or check produces suggest does not take --struct or check does not take --struct. All of these exit with status 2.
--format json is available only with layout; --format=json is also supported and --format text is the default. It prints the same structs as the text listing (also with --struct) as one JSON array, indented by two spaces, with the keys in the order shown below. fields lists the fields with the offset and size of each one and gaps lists every run of padding bytes, including the tail, so the sizes in gaps add up to padding. For struct Pair { u8 a; u32 b; }:
structpad layout --format json pair.sp[
{
"name": "Pair",
"size": 8,
"align": 4,
"padding": 3,
"packed": false,
"fields": [
{
"name": "a",
"type": "u8",
"offset": 0,
"size": 1
},
{
"name": "b",
"type": "u32",
"offset": 4,
"size": 4
}
],
"gaps": [
{
"offset": 1,
"size": 3
}
]
}
]
An invalid value such as xml produces invalid --format "xml" (use text or json), and --format with suggest or check produces suggest does not take --format or check does not take --format. Both exit with status 2.
Select the ABI with --abi lp64 (the default) or --abi ilp32; for example:
structpad layout --abi ilp32 net.spUse - as the file argument to read from standard input. The display name in diagnostics is <stdin>.
A file contains one or more struct definitions. Each field is a primitive type, an array, or a previously defined struct:
struct Header {
u8 kind;
u32 length;
char name[10];
}
Names begin with an ASCII letter or underscore. Array lengths are decimal values from 1 through 65536. Use packed struct Name { to give all fields byte alignment. Blank lines, comments beginning with //, and a UTF-8 byte-order mark are accepted. A struct must contain at least one field, and nested structs must be defined earlier in the file.
The lp64 ABI uses 8-byte pointers; ilp32 uses 4-byte pointers. Both ABIs align 64-bit integers and floats to 8 bytes under lp64; under ilp32 they keep their 8-byte size but use 4-byte alignment. Each field starts at an offset aligned to its type, and the final struct size is rounded up to the struct alignment. Padding is the space between fields and at the end.
The pointer-width integers usize and isize have the size and alignment of ptr: 8 and 8 under lp64, 4 and 4 under ilp32. They are the types to use for lengths and offsets that follow the platform, such as size_t in C or usize in Rust. For struct Buf { u8 tag; usize len; isize off; u16 flags; }, structpad layout --abi ilp32 prints:
struct Buf: size 16, align 4, padding 5
offset size field
0 1 tag: u8
1 3 (padding)
4 4 len: usize
8 4 off: isize
12 2 flags: u16
14 2 (padding)
0: command succeeded, orcheckfound no struct over the padding limit.1:checkfound at least one struct over the padding limit.2: usage or input error.
Run the test suite and linter from the project root:
python -m pytest -q
python -m ruff check .MIT, see LICENSE.
Built with BAKED, an open-source coding agent.