Skip to content

CPython: callback trampolines hold their callback for the call and release their arguments - #35

Merged
bdbarnett merged 1 commit into
mainfrom
ostinato/trampoline-strong-ref
Oct 15, 2026
Merged

bdbarnett merged 1 commit into
mainfrom
ostinato/trampoline-strong-ref

Conversation

@bdbarnett

Copy link
Copy Markdown
Contributor

Two reference bugs in every generated CPython callback trampoline (binding/emit_cpython_native.py), on every Python build.

The callback could be freed while it ran. The trampoline looked the Python callback up in the callback dict and used that borrowed reference for the whole call. A callback that replaces its own registration while it runs, such as timer.set_cb(other) from inside the timer callback, dropped the dict's reference, and with it the only one. The trampoline now takes its own reference before the call and lets go of it right after, before LVGL's lock is taken back.

Every call leaked its arguments. Each argument converter returns a new reference and PyTuple_Pack takes another, but the trampoline never released the converter's. A widget passed to a tree_walk callback gained a reference per call and was never freed. The trampoline now releases them, and a failed conversion clears its error and returns instead of handing PyTuple_Pack a NULL.

generated/lvgl_python.c is regenerated (./regenerate_all.sh --target cpython; --check matches). tests/test_cpython_callback_references.py reads every trampoline in the generated C and checks both; with the template change reverted on this branch (a planted fault, since undone) both tests fail, and the full generator suite passes with it (106 tests).

Built into lvgl-python, a behavioural test (in lvgl-python's sync PR) fails on the current bindings, with the callback dead after replacing itself and a widget's count up by 10 after 10 tree walks, and passes with this change. lvgl-python's suite and tools/test_lvgl_smoke.py pass on that build. The sync should land with or after lvgl-python#35, whose runtime fix makes every converter return a new reference.

…leases its arguments

The trampoline looked the callback up as a borrowed reference from the
callback dict, so a callback that replaced its own registration while it
ran (timer.set_cb(other) inside the timer callback) was freed mid-call. It
now takes its own reference for the call.

It also never released the converters' references to the arguments after
packing them, so every call leaked one reference to each argument. A
failed conversion now clears its error instead of passing NULL on.
@bdbarnett
bdbarnett merged commit d0f6f4f into main Oct 15, 2026
4 checks passed
@bdbarnett
bdbarnett deleted the ostinato/trampoline-strong-ref branch October 15, 2026 20:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant