Skip to content

Commit e0e7541

Browse files
committed
docs: adapt Cython gotchas to this repo's actual idioms
Trim and reshape the section added in the previous commit to match python-zeroconf's reality rather than aioesphomeapi's: - Drop the `noexcept` cdef bullet and the `except *` / `except? -N` bullet. The authored .pxd files in this repo don't use either — warning about traps the codebase doesn't reach is noise. - Fix the `unsigned int` -> `cdef int` bullet. DNS RR length and name compression pointer fields are 16-bit and 14-bit, so they don't cross the bit-31 signed boundary. Cite TTL instead — it is 32-bit (RFC 1035 §3.2.1) and the only field where the trap applies — and frame the rule as "stay with `unsigned int` end- to-end" since that is the convention `_protocol/incoming.pxd`, `_cache.pxd`, and `_handlers/*.pxd` already follow. - Ground the `cdef`-typed module constants bullet in an actual example: `_ANSWER_STRATEGY_POINTER` in `_handlers/query_handler.{py,pxd}`. - Ground the PyLong-conversion bullet in actual examples: `_TYPE_PTR`, `_DNS_PTR_MIN_TTL`, `_MIN_SCHEDULED_RECORD_EXPIRATION` already use the `cdef unsigned int` alias pattern. - Drop "(things that have bitten us)" from the heading — most of the surviving bullets are preventative for this repo, not historical incidents. Docs only.
1 parent bdde674 commit e0e7541

1 file changed

Lines changed: 45 additions & 55 deletions

File tree

CLAUDE.md

Lines changed: 45 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -157,61 +157,51 @@ mutated from multiple threads without locks; no
157157
but the test matrix exercises 3.14t, so any new Cython module
158158
needs to keep working there.
159159

160-
## Cython gotchas (things that have bitten us)
160+
## Cython gotchas
161161

162-
These are non-obvious traps in the `.py` + `.pxd` setup that work
163-
fine in pure-Python mode but break or silently misbehave in the
164-
shipped Cython wheels. Tests pass locally with `SKIP_CYTHON=1`
165-
fallback paths, then CI on `use_cython` builds catches the issue —
166-
or worse, the issue ships and only manifests in production wheels.
162+
Non-obvious traps in the `.py` + `.pxd` setup that work fine in
163+
pure-Python mode but break or silently misbehave in the shipped
164+
Cython wheels. Distilled from the patterns that already exist in
165+
this repo's `.pxd` files and from incidents in sibling Cython-
166+
accelerated projects.
167167

168168
- **`cdef`-typed module constants are not Python-importable.**
169-
Declaring `cdef int _MAX_X` in `.pxd` makes Cython treat
170-
`_MAX_X = 5` in the `.py` as a C int assignment; the Python
171-
module dict never gets the binding. `from module import _MAX_X`
172-
succeeds in pure-Python but raises `ImportError` under Cython.
173-
**Pattern**: define both names — `MAX_X = 5` (Python-
174-
importable) and `_MAX_X = MAX_X` (cdef-typed for hot-path
175-
comparisons). Tests import the public name; production code
176-
uses either.
177-
178-
- **`noexcept` cdef paths must be pure C.** Calling a Python
179-
method that can raise from inside a `cdef ... noexcept`
180-
function is undefined / lossy — Cython prints the exception
181-
via `WriteUnraisable` and silently continues. Keep `noexcept`
182-
paths to sentinel returns and let the caller handle Python-
183-
level work.
184-
185-
- **`unsigned int` result returned through `cdef int` can flip
186-
sign.** A length / offset decoded into an `unsigned int` and
187-
returned via `cdef int` will come back negative for any value
188-
with bit 31 set. If the caller does `if x < 0: return`, an
189-
attacker-controlled large value silently hits the "incomplete"
190-
/ "stop processing" branch instead of being rejected. Either
191-
cap the input range so decoded values stay in signed-int
192-
range, or check explicit sentinel values (`x == _SENTINEL_A`)
193-
instead of generic `< 0`. The on-the-wire parsers in
194-
`_protocol/incoming` are exactly the kind of code where this
195-
bites — name pointers and RR length fields cross the 16-bit /
196-
32-bit signed boundary.
197-
198-
- **`except *` / `except? -N` adds per-call exception checks.**
199-
Switching a hot-path `cdef ... noexcept` to `except *` or
200-
`except? -3` adds a `PyErr_Occurred()` check after every call.
201-
Negligible for cold paths, measurable on hot paths — CodSpeed
202-
has caught double-digit regressions from this on
203-
packet-parsing benchmarks in sibling Cython projects. Prefer
204-
`noexcept` for hot paths and route error handling through the
205-
caller.
206-
207-
- **Module-level Python int constants force `PyLong` conversion
208-
in hot-path comparisons.** `if length > _MAX_FRAME_SIZE`
169+
Declaring `cdef unsigned int _ANSWER_STRATEGY_POINTER` in
170+
`query_handler.pxd` makes Cython treat
171+
`_ANSWER_STRATEGY_POINTER = 1` in `query_handler.py` as a C int
172+
assignment; the Python module dict never gets the binding.
173+
`from zeroconf._handlers.query_handler import
174+
_ANSWER_STRATEGY_POINTER` succeeds in pure-Python but raises
175+
`ImportError` under Cython. If you need the value visible from
176+
Python (e.g. a test wants to assert on it), define both names —
177+
a public `ANSWER_STRATEGY_POINTER = 1` Python binding plus a
178+
`cdef`-typed `_ANSWER_STRATEGY_POINTER = ANSWER_STRATEGY_POINTER`
179+
alias for hot-path comparisons.
180+
181+
- **Match the existing `unsigned int` convention for length, TTL,
182+
type/class, and offset fields.** `_protocol/incoming.pxd`,
183+
`_cache.pxd`, and `_handlers/*.pxd` already declare these as
184+
`unsigned int` end-to-end. Introducing a `cdef int` return that
185+
carries a value originally decoded into `unsigned int` flips
186+
sign for any value with bit 31 set — TTL is a 32-bit DNS field
187+
(RFC 1035 §3.2.1, interpreted as unsigned), so a large TTL
188+
passed back through a `cdef int` boundary becomes negative and
189+
trips `< 0` sentinel branches. Stay with `unsigned int` across
190+
the whole call chain; if you need a real sentinel, return an
191+
explicit value (`UINT_MAX`, a dedicated constant) and check for
192+
it by equality.
193+
194+
- **Module-level Python int constants force `PyLong_AsLong` on
195+
every hot-path comparison.** `if record.type == _TYPE_PTR`
209196
compiles to a Python attribute lookup + `PyLong_AsLong` per
210-
call. Adding `cdef int _MAX_FRAME_SIZE` to the `.pxd` makes it
211-
a native C comparison. Timing / size constants from `const.py`
212-
used inside `cdef` hot paths in `_protocol/`, `_cache`,
213-
`_handlers/`, or `_listener` should have a `cdef`-typed alias
214-
declared in the appropriate `.pxd`.
197+
call when `_TYPE_PTR` is just a `.py`-level binding. The repo
198+
already follows the right pattern — `_cache.pxd` /
199+
`record_manager.pxd` declare `cdef unsigned int _TYPE_PTR`,
200+
`_DNS_PTR_MIN_TTL`, `_MIN_SCHEDULED_RECORD_EXPIRATION`, etc.
201+
When adding a new size / TTL / type constant from `const.py`
202+
to a `cdef` hot path in `_protocol/`, `_cache`, `_handlers/`,
203+
or `_listener`, add the `cdef`-typed alias to the corresponding
204+
`.pxd` at the same time.
215205

216206
- **Sign-compare warnings in generated C are real.** `gcc`/
217207
`clang` warns when comparing `unsigned int` with `int` because
@@ -220,11 +210,11 @@ or worse, the issue ships and only manifests in production wheels.
220210
signedness of compared operands in the `.pxd` (e.g. if the
221211
local is `unsigned int`, declare the constant as
222212
`cdef unsigned int`; if the local is `int`, declare it
223-
`cdef int`). The warning predicts a class of overflow bug like
224-
the unsigned->signed trap above.
213+
`cdef int`). The warning predicts the unsigned -> signed
214+
overflow class of bug.
225215

226-
- **CodSpeed regressions only show in the Cython build.** Pure-
227-
Python (`SKIP_CYTHON=1`) tests can pass while production
216+
- **CodSpeed regressions only show up in the Cython build.**
217+
Pure-Python (`SKIP_CYTHON=1`) tests can pass while production
228218
wire-format hot paths regress. Trust the CodSpeed check on PRs
229219
that touch any file in `TO_CYTHONIZE`; rebuild in place with
230220
`REQUIRE_CYTHON=1 poetry install --only=main,dev` before

0 commit comments

Comments
 (0)