Skip to content

Commit da93e1f

Browse files
r41k0uclaude
andcommitted
Docs: Integer semantics user-guide page
C's rules over the ctypes types: what a declaration means, literal rank, source-driven widening with the destination typing the stored value, the usual arithmetic conversions applied per operation, which operators and predicates the sign selects, and the list of deliberate divergences from Python (/ and // truncate, % takes the dividend's sign, fixed width, unsigned types exist). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSDVsZH5NtoASyxB8FCtGU
1 parent a5cbff3 commit da93e1f

3 files changed

Lines changed: 129 additions & 0 deletions

File tree

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ user-guide/maps
6969
user-guide/structs
7070
user-guide/compilation
7171
user-guide/helpers
72+
user-guide/integers
7273
```
7374

7475
```{toctree}

docs/user-guide/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ PythonBPF uses Python's `ctypes` module for type definitions:
4444
* `c_void_p` - Void pointers
4545
* `str(N)` - Fixed-length strings (e.g., `str(16)` for 16-byte string)
4646

47+
Integers follow C's rules for width, sign, conversion and arithmetic; see
48+
{doc}`integers` for the details and the places where this differs from Python.
49+
4750
## Example Structure
4851

4952
A typical PythonBPF program follows this structure:

docs/user-guide/integers.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Integer Semantics
2+
3+
PythonBPF programs are Python syntax, but the integers in them behave as C integers: the
4+
program runs in the kernel as BPF bytecode, where every value is a fixed-width machine
5+
word. This page describes the rules the compiler applies. They are C's rules, applied to
6+
the `ctypes` types you declare, so a program's arithmetic matches what the equivalent C
7+
program compiled with clang would compute.
8+
9+
```{note}
10+
This is one of the few places where PythonBPF deliberately differs from Python. Python
11+
integers have arbitrary precision and no unsigned types; BPF has neither. The
12+
[divergences from Python](#divergences-from-python) are listed at the end of this page.
13+
```
14+
15+
## Types
16+
17+
An integer's type is the `ctypes` type it was declared with, and the type carries both
18+
a width and a sign:
19+
20+
| Signed | Unsigned | Width |
21+
|---|---|---|
22+
| `c_int8` | `c_uint8` | 8 |
23+
| `c_int16` | `c_uint16` | 16 |
24+
| `c_int32` | `c_uint32` | 32 |
25+
| `c_int64` | `c_uint64` | 64 |
26+
27+
Every declaration site uses these types: local variables initialised with a constructor
28+
call, `@bpfglobal` variables, `@struct` fields, map keys and values, and fields read from
29+
`vmlinux` structures. Helper functions return the type of the kernel's signature, so
30+
`pid()` and `ktime()` are unsigned while `probe_read`-style helpers return a signed
31+
`long`.
32+
33+
```python
34+
count = c_uint32(0) # a 32-bit unsigned local
35+
delta = c_int64(-1) # a 64-bit signed local
36+
```
37+
38+
A local assigned without a constructor takes its type from the expression:
39+
40+
```python
41+
now = ktime() # c_uint64, the helper's return type
42+
total = count + 1 # the type of the addition (see below), held in a 64-bit slot
43+
```
44+
45+
Undeclared locals are always 64 bits wide; the inferred type only decides their sign.
46+
Declare the local with a constructor when a narrower width matters.
47+
48+
### Literals
49+
50+
A literal has the type a C compiler gives it: `int` (32-bit signed) if the value fits,
51+
`long long` (64-bit signed) otherwise. This matters for mixed arithmetic: in
52+
`count / -2` with `count` a `c_uint32`, the literal `-2` is a 32-bit `int`, so the
53+
division happens in `c_uint32` exactly as it would in C.
54+
55+
## Assignment and conversion
56+
57+
Assigning a value to a variable of a different integer type converts it, and the
58+
variable's declared type is what the stored value *is* afterwards:
59+
60+
* **Widening preserves the value.** The conversion looks at the *source*'s sign: an
61+
unsigned source is zero-extended, a signed source is sign-extended. So a `c_uint32`
62+
holding `0xFFFFFFFF` stored into a `c_int64` gives `4294967295`, and a `c_int32`
63+
holding `-1` stored into a `c_uint64` gives `0xFFFFFFFFFFFFFFFF`. This is what C and
64+
`ctypes` both do.
65+
* **Narrowing truncates.** Only the low bits survive.
66+
* **Same width reinterprets.** A `c_uint32` `0xFFFFFFFF` stored into a `c_int32` reads
67+
as `-1`.
68+
69+
The same rules apply to explicit conversions written as constructor calls
70+
(`c_int64(count)`), to struct field stores and to `return`.
71+
72+
## Arithmetic
73+
74+
Each binary operation is typed on its own, from its two operands, following C's usual
75+
arithmetic conversions:
76+
77+
1. Operands narrower than 32 bits are promoted to `c_int32`.
78+
2. If both operands have the same sign, the result has the wider width and that sign.
79+
3. If the signs differ, the unsigned type wins when it is at least as wide as the signed
80+
one; otherwise the signed type wins.
81+
82+
The operation is then performed in that type, and its result has that type. The variable
83+
receiving the result plays no part until the final store. Two consequences worth knowing:
84+
85+
* **Intermediate results wrap at their own width.** `c_uint32(0x80000000) * c_uint32(2)`
86+
is a `c_uint32` multiplication, so it wraps to `0` before being stored, even if the
87+
destination is a `c_uint64`. Widen an operand first if you want a 64-bit product.
88+
* **Mixed signs go unsigned.** `c_uint32(10) / c_int32(-2)` is an unsigned division by
89+
`0xFFFFFFFE`, giving `0`, not `-5`.
90+
91+
The sign of the operation's type selects the instruction for the operations where it
92+
matters:
93+
94+
| Operator | Signed type | Unsigned type |
95+
|---|---|---|
96+
| `/`, `//` | truncating signed division | unsigned division |
97+
| `%` | remainder with the dividend's sign | unsigned remainder |
98+
| `>>` | arithmetic shift (sign bit shifts in) | logical shift (zeros shift in) |
99+
| `<`, `<=`, `>`, `>=` | signed comparison | unsigned comparison |
100+
101+
`+`, `-`, `*`, `<<`, `&`, `|`, `^`, `==` and `!=` produce the same bits for either sign.
102+
103+
Unary minus on an unsigned value follows C too: `-x` is `2^N - x` in the value's type.
104+
105+
## Comparisons
106+
107+
A comparison converts both operands with the same usual arithmetic conversions and then
108+
compares in the resulting type. `c_uint64(10) > c_int64(-1)` is therefore an unsigned
109+
comparison in which `-1` is the largest possible value, and the result is false. The
110+
result of a comparison is `1` or `0`, as in C.
111+
112+
## Divergences from Python
113+
114+
Because the semantics are C's, some Python behaviour does not carry over:
115+
116+
* `/` is integer division; there is no floating-point result.
117+
* `//` and `/` are the same operation, and both truncate toward zero: `-7 // 2` is `-3`,
118+
where Python gives `-4`.
119+
* `%` takes the sign of the dividend: `-7 % 2` is `-1`, where Python gives `1`.
120+
* Integers have a fixed width and wrap on overflow; there is no arbitrary precision.
121+
* Unsigned types exist, and mixing them with signed values follows C's conversions
122+
rather than Python's mathematical integers.
123+
124+
The test programs under `tests/passing_tests/signedness/` show each rule with its
125+
expected value, and `tests/c-form/signedness.bpf.c` is the equivalent C program.

0 commit comments

Comments
 (0)