You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Oct 30, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Oct 30, 2026. It is now read-only.
CPython: should add_event_cb callbacks outlive their deleted widget? #33
On CPython, a callback passed to add_event_cb stays alive after its widget is deleted and the widget's Python wrapper is dropped. MicroPython and CircuitPython free it. Is that intended?
The CPython runtime keeps a widget's callbacks in a dict on its Python wrapper, and LVGL's side only points at the wrapper. lvgl-python's guide describes the contract as: keep a Python reference to any widget that owns callbacks. From that, you'd expect a callback to go when the wrapper does. It doesn't, apparently because mp_lv_callback in src/lvpy_runtime.c adds one more reference to the callback after storing it in that dict:
PyDict_SetItemString already holds its own reference, and nothing releases the extra one. The line has been there since the runtime was first added, with no comment, and no test checks callback lifetime. It could be deliberate, keeping a callback alive for code that drops its widget reference, or it could be a leftover. The per-registration dict path a few lines up (PyDict_New followed by Py_INCREF) raises the same question.
To reproduce, with pydevices-lvgl 9.5.50 on CPython 3.12:
Where it shows up: an app that builds a screen, deletes it and builds another keeps every callback it ever registered, and everything those callbacks reference. On MicroPython and CircuitPython, the same cycle run 50 times, with each callback holding a 20 KB buffer, loses under 2 KB of heap.
changed the title [-]CPython: add_event_cb callbacks outlive their deleted widget[/-][+]CPython: should a deleted widget release its event callbacks?[/+]on Oct 12, 2026
changed the title [-]CPython: should a deleted widget release its event callbacks?[/-][+]CPython: should add_event_cb callbacks outlive their deleted widget?[/+]on Oct 12, 2026
Not intended: it was a leak, and it's fixed in lvgl-python#30, now on main. It reaches PyPI with the next pydevices-lvgl release.
What was going on: with user_data=None, add_event_cb stores the handler in a small dict of its own (not on the widget's wrapper) and hands LVGL a pointer to that dict. The runtime releases one reference to the dict when the event is removed or the object deleted, which is the right design, but mp_lv_callback created the dict with two references and took a third one on the handler. Nothing matched those, so the dict, the handler and whatever the handler held outlived the widget. Your reproduction prints callback alive: True on 9.5.50 and False with the fix; 50 build-and-delete cycles with 20 KB per handler grew the heap by 10.4 MB before and by under 10 KB after.
Just deleting the extra increfs isn't enough, which may be why the line survived. LVGL sends LV_EVENT_DELETE to the runtime's own hook before a DELETE callback you registered on the same object, so an immediate release freed the dict that your callback was about to read (a segfault under MALLOC_PERTURB_). The reference is now dropped once no LVGL call and no Python callback is in flight.
The contract, as the README now says: a callback lives as long as its registration. It is released when the widget is deleted or the event removed. Dropping your Python reference to the widget while it exists neither stops the callback nor leaks it, and a handler may delete its own widget. tests/test_callback_lifetime.py in lvgl-python pins each of those cases.
Activity
[-]CPython: add_event_cb callbacks outlive their deleted widget[/-][+]CPython: should a deleted widget release its event callbacks?[/+][-]CPython: should a deleted widget release its event callbacks?[/-][+]CPython: should add_event_cb callbacks outlive their deleted widget?[/+]Release add_event_cb callbacks with their registration (#30)
bdbarnett commentedon Oct 12, 2026
Not intended: it was a leak, and it's fixed in lvgl-python#30, now on
main. It reaches PyPI with the nextpydevices-lvglrelease.What was going on: with
user_data=None,add_event_cbstores the handler in a small dict of its own (not on the widget's wrapper) and hands LVGL a pointer to that dict. The runtime releases one reference to the dict when the event is removed or the object deleted, which is the right design, butmp_lv_callbackcreated the dict with two references and took a third one on the handler. Nothing matched those, so the dict, the handler and whatever the handler held outlived the widget. Your reproduction printscallback alive: Trueon 9.5.50 andFalsewith the fix; 50 build-and-delete cycles with 20 KB per handler grew the heap by 10.4 MB before and by under 10 KB after.Just deleting the extra increfs isn't enough, which may be why the line survived. LVGL sends
LV_EVENT_DELETEto the runtime's own hook before aDELETEcallback you registered on the same object, so an immediate release freed the dict that your callback was about to read (a segfault underMALLOC_PERTURB_). The reference is now dropped once no LVGL call and no Python callback is in flight.The contract, as the README now says: a callback lives as long as its registration. It is released when the widget is deleted or the event removed. Dropping your Python reference to the widget while it exists neither stops the callback nor leaks it, and a handler may delete its own widget.
tests/test_callback_lifetime.pyin lvgl-python pins each of those cases.