Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,19 @@ Names begin with an ASCII letter or underscore. Array lengths are decimal values

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:

```text
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)
```

## Exit codes

- `0`: command succeeded, or `check` found no struct over the padding limit.
Expand Down
2 changes: 2 additions & 0 deletions src/structpad/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ def alignment(self, abi: str) -> int:
("i64", 8, 8, 8, 4),
("f64", 8, 8, 8, 4),
("ptr", 8, 8, 4, 4),
("usize", 8, 8, 4, 4),
("isize", 8, 8, 4, 4),
)
}

Expand Down
115 changes: 115 additions & 0 deletions tests/test_pointer_ints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""Tests for pointer-width integer primitives."""

import importlib
import json
import sys
from io import BytesIO
from pathlib import Path

_SOURCE_DIRECTORY = str(Path(__file__).resolve().parents[1] / "src")
if _SOURCE_DIRECTORY not in sys.path:
sys.path.insert(0, _SOURCE_DIRECTORY)

from structpad.types import get_primitive

run = importlib.import_module("structpad.cli").run

_BUF = b"struct Buf {\n u8 tag;\n usize len;\n isize off;\n u16 flags;\n}\n"


def _run(args: list[str], source: bytes = _BUF) -> tuple[int, bytes, bytes]:
stdin = BytesIO(source)
stdout = BytesIO()
stderr = BytesIO()
status = run(args, stdin, stdout, stderr)
return status, stdout.getvalue(), stderr.getvalue()


def test_usize_and_isize_have_the_size_and_alignment_of_ptr() -> None:
pointer = get_primitive("ptr")
assert pointer is not None

for name in ("usize", "isize"):
primitive = get_primitive(name)
assert primitive is not None
for abi, expected in (("lp64", (8, 8)), ("ilp32", (4, 4))):
expected_size, expected_alignment = expected
assert primitive.size(abi) == expected_size
assert primitive.alignment(abi) == expected_alignment
assert primitive.size(abi) == pointer.size(abi)
assert primitive.alignment(abi) == pointer.alignment(abi)


def test_layout_under_lp64() -> None:
status, stdout, stderr = _run(["layout", "-"])

assert status == 0
assert stdout == (
b"struct Buf: size 32, align 8, padding 13\n"
b"offset size field\n"
b" 0 1 tag: u8\n"
b" 1 7 (padding)\n"
b" 8 8 len: usize\n"
b" 16 8 off: isize\n"
b" 24 2 flags: u16\n"
b" 26 6 (padding)\n"
)
assert stderr == b""


def test_layout_under_ilp32() -> None:
status, stdout, stderr = _run(["layout", "--abi", "ilp32", "-"])

assert status == 0
assert stdout == (
b"struct Buf: size 16, align 4, padding 5\n"
b"offset size field\n"
b" 0 1 tag: u8\n"
b" 1 3 (padding)\n"
b" 4 4 len: usize\n"
b" 8 4 off: isize\n"
b" 12 2 flags: u16\n"
b" 14 2 (padding)\n"
)
assert stderr == b""


def test_suggest_and_check_use_the_pointer_width() -> None:
status, stdout, stderr = _run(["suggest", "-"])

assert status == 0
assert stdout == (
b"struct Buf: size 32 -> 24 (saves 8)\n"
b" usize len;\n"
b" isize off;\n"
b" u16 flags;\n"
b" u8 tag;\n"
)
assert stderr == b""

status, stdout, stderr = _run(["check", "--max-padding", "8", "-"])

assert status == 1
assert stdout == (
b"Buf: 13 bytes of padding (limit 8)\n1 of 1 structs over the padding limit\n"
)
assert stderr == b""


def test_json_layout_names_the_type_and_other_names_stay_unknown() -> None:
status, stdout, stderr = _run(["layout", "--format", "json", "-"])

assert status == 0
layout = json.loads(stdout)
len_field = next(field for field in layout[0]["fields"] if field["name"] == "len")
assert len_field["type"] == "usize"
assert len_field["offset"] == 8
assert len_field["size"] == 8
assert stderr == b""

invalid_source = b"struct Buf {\n size_t n;\n}\n"
status, stdout, stderr = _run(["layout", "-"], invalid_source)

assert status == 2
assert stdout == b""
assert stderr == b'structpad: <stdin>:2: unknown type "size_t"\n'
Loading