@@ -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