Skip to content

Commit d6994c9

Browse files
committed
Add multi-callback regression test and release event callback dicts
Register PRESSED/RELEASED/CLICKED on one object in test_lvgl_cpython to guard against add_event_cb clobbering regressions. Document fast incremental rebuilds via setup.py build_ext --inplace. Release per-registration callback dicts on event removal and object DELETE in lvpy_runtime, with peek helpers for remove_event_cb wrappers.
1 parent 44828aa commit d6994c9

4 files changed

Lines changed: 159 additions & 8 deletions

File tree

‎README.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ Phases 1–7 are enabled in the generator today. Smoke tests in `test_lvgl_cpyth
212212
- **Display OO API**: no methods on `lv_display_t` wrappers; use `lv.display_*` module functions.
213213
- **Prototype aliasing**: some widget `tp_methods` entries may point at the wrong C function when LVGL reuses prototypes; prefer module-level names when in doubt.
214214
- **`C_Pointer` helper struct**: emitted late; a runtime stub is used until helper emission is complete.
215-
- **Callback lifetime**: Python wrappers clear `lv_obj->user_data` on dealloc; long-running apps should keep references to objects that register callbacks.
215+
- **Callback lifetime**: Per-registration callback dicts (`add_event_cb` with `user_data=None`) are released when the event is removed or the object is deleted. Python wrappers clear `lv_obj->user_data` on dealloc; long-running apps should still keep references to objects that register callbacks.
216216

217217
## Development
218218

@@ -224,6 +224,14 @@ cd ../lv_cpython_mod && .venv/bin/pip install -e .
224224
.venv/bin/python test_lvgl_cpython.py
225225
```
226226

227+
After the first full build, incremental rebuilds are much faster (only changed sources such as `generated/lvpy.c` are recompiled). From `lv_cpython_mod`, with `setuptools` and `wheel` installed in the target venv:
228+
229+
```bash
230+
.venv/bin/python setup.py build_ext --inplace
231+
```
232+
233+
With an editable install (`pip install -e .`), the updated `.so` / `.pyd` beside this repo is picked up immediately — no reinstall step needed.
234+
227235
Generator work lives in the [`lv_bindings`](https://github.com/PyDevices/lv_bindings) repository (`binding/emit_py_native.py`, `binding/emit_py_cp.py`, `binding/emit_cpython.py`). Runtime fixes and CPython-specific behavior belong here in `lvpy_runtime.c`.
228236

229237
## Related projects

‎lvpy_runtime.c‎

Lines changed: 108 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -511,13 +511,7 @@ static PyObject *py_C_Pointer_new(PyTypeObject *type, PyObject *args, PyObject *
511511
return make_new_lv_struct(type, args, kwds, 0);
512512
}
513513

514-
static void py_lv_delete_cb(lv_event_t *e)
515-
{
516-
lv_obj_t *lv_obj = lv_event_get_target(e);
517-
if (!lv_obj) return;
518-
py_lv_obj_t *self = (py_lv_obj_t *)lv_obj->user_data;
519-
if (self) self->lv_obj = NULL;
520-
}
514+
static void py_lv_delete_cb(lv_event_t *e);
521515

522516
PyTypeObject *py_get_base_obj_type(void)
523517
{
@@ -621,6 +615,113 @@ PyObject *get_callback_dict_from_user_data(void *user_data)
621615
return mp_get_callbacks(obj);
622616
}
623617

618+
int lvpy_is_per_registration_callback_dict(void *user_data)
619+
{
620+
if (!user_data) return 0;
621+
PyObject *obj = (PyObject *)user_data;
622+
if (!PyDict_Check(obj)) return 0;
623+
PyTypeObject *base = py_get_base_obj_type();
624+
if (base && PyObject_TypeCheck(obj, base)) return 0;
625+
return 1;
626+
}
627+
628+
void lvpy_release_callback_user_data(void *user_data)
629+
{
630+
if (lvpy_is_per_registration_callback_dict(user_data)) {
631+
Py_DECREF((PyObject *)user_data);
632+
}
633+
}
634+
635+
static void *lvpy_peek_event_dsc_list_cb_user_data(
636+
uint32_t count,
637+
lv_event_dsc_t *(*get_dsc)(uint32_t index, void *ctx),
638+
void *ctx,
639+
lv_event_cb_t cb,
640+
void *specific_user_data)
641+
{
642+
for (uint32_t i = 0; i < count; i++) {
643+
lv_event_dsc_t *dsc = get_dsc(i, ctx);
644+
if (!dsc) continue;
645+
if (lv_event_dsc_get_cb(dsc) != cb) continue;
646+
void *ud = lv_event_dsc_get_user_data(dsc);
647+
if (specific_user_data != NULL && ud != specific_user_data) continue;
648+
return ud;
649+
}
650+
return NULL;
651+
}
652+
653+
typedef struct {
654+
lv_obj_t *obj;
655+
} lvpy_obj_peek_ctx;
656+
657+
static lv_event_dsc_t *lvpy_obj_peek_get_dsc(uint32_t index, void *ctx)
658+
{
659+
lvpy_obj_peek_ctx *c = (lvpy_obj_peek_ctx *)ctx;
660+
return lv_obj_get_event_dsc(c->obj, index);
661+
}
662+
663+
void *lvpy_peek_obj_event_cb_user_data(lv_obj_t *obj, lv_event_cb_t cb, void *specific_user_data)
664+
{
665+
if (!obj) return NULL;
666+
lvpy_obj_peek_ctx ctx = {obj};
667+
return lvpy_peek_event_dsc_list_cb_user_data(
668+
lv_obj_get_event_count(obj), lvpy_obj_peek_get_dsc, &ctx, cb, specific_user_data);
669+
}
670+
671+
typedef struct {
672+
lv_display_t *disp;
673+
} lvpy_display_peek_ctx;
674+
675+
static lv_event_dsc_t *lvpy_display_peek_get_dsc(uint32_t index, void *ctx)
676+
{
677+
lvpy_display_peek_ctx *c = (lvpy_display_peek_ctx *)ctx;
678+
return lv_display_get_event_dsc(c->disp, index);
679+
}
680+
681+
void *lvpy_peek_display_event_cb_user_data(lv_display_t *disp, lv_event_cb_t cb, void *specific_user_data)
682+
{
683+
if (!disp) return NULL;
684+
lvpy_display_peek_ctx ctx = {disp};
685+
return lvpy_peek_event_dsc_list_cb_user_data(
686+
lv_display_get_event_count(disp), lvpy_display_peek_get_dsc, &ctx, cb, specific_user_data);
687+
}
688+
689+
typedef struct {
690+
lv_indev_t *indev;
691+
} lvpy_indev_peek_ctx;
692+
693+
static lv_event_dsc_t *lvpy_indev_peek_get_dsc(uint32_t index, void *ctx)
694+
{
695+
lvpy_indev_peek_ctx *c = (lvpy_indev_peek_ctx *)ctx;
696+
return lv_indev_get_event_dsc(c->indev, index);
697+
}
698+
699+
void *lvpy_peek_indev_event_cb_user_data(lv_indev_t *indev, lv_event_cb_t cb, void *specific_user_data)
700+
{
701+
if (!indev) return NULL;
702+
lvpy_indev_peek_ctx ctx = {indev};
703+
return lvpy_peek_event_dsc_list_cb_user_data(
704+
lv_indev_get_event_count(indev), lvpy_indev_peek_get_dsc, &ctx, cb, specific_user_data);
705+
}
706+
707+
static void py_lv_delete_cb(lv_event_t *e)
708+
{
709+
PyGILState_STATE gstate = PyGILState_Ensure();
710+
lv_obj_t *lv_obj = lv_event_get_target(e);
711+
if (lv_obj) {
712+
uint32_t count = lv_obj_get_event_count(lv_obj);
713+
for (uint32_t i = 0; i < count; i++) {
714+
lv_event_dsc_t *dsc = lv_obj_get_event_dsc(lv_obj, i);
715+
if (dsc) {
716+
lvpy_release_callback_user_data(lv_event_dsc_get_user_data(dsc));
717+
}
718+
}
719+
py_lv_obj_t *self = (py_lv_obj_t *)lv_obj->user_data;
720+
if (self) self->lv_obj = NULL;
721+
}
722+
PyGILState_Release(gstate);
723+
}
724+
624725
void *mp_lv_callback(PyObject *py_callback, void *lv_callback, const char *callback_name,
625726
void **user_data_ptr, void *containing_struct,
626727
void *(*get_user_data)(void *), void (*set_user_data)(void *, void *))

‎lvpy_runtime.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ PyObject *mp_get_callbacks(PyObject *obj);
9595
void lvpy_dealloc_obj(py_lv_obj_t *self);
9696
PyObject *get_callback_dict_from_user_data(void *user_data);
9797

98+
/* Per-registration callback dicts (add_event_cb with user_data=None) are Py_INCREF'd in mp_lv_callback. */
99+
int lvpy_is_per_registration_callback_dict(void *user_data);
100+
void lvpy_release_callback_user_data(void *user_data);
101+
void *lvpy_peek_obj_event_cb_user_data(lv_obj_t *obj, lv_event_cb_t cb, void *specific_user_data);
102+
void *lvpy_peek_display_event_cb_user_data(lv_display_t *disp, lv_event_cb_t cb, void *specific_user_data);
103+
void *lvpy_peek_indev_event_cb_user_data(lv_indev_t *indev, lv_event_cb_t cb, void *specific_user_data);
104+
98105
void *mp_lv_callback(PyObject *py_callback, void *lv_callback, const char *callback_name,
99106
void **user_data_ptr, void *containing_struct,
100107
void *(*get_user_data)(void *), void (*set_user_data)(void *, void *));

‎test_lvgl_cpython.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,40 @@ def on_button_clicked(event):
130130
print("OK: screen and button event callbacks")
131131

132132

133+
def test_multi_callbacks(lv):
134+
"""Multiple add_event_cb registrations on one object must not clobber each other."""
135+
scr = lv.screen_active()
136+
btn = lv.button(scr)
137+
btn.set_size(80, 40)
138+
fired = []
139+
140+
def mk(name):
141+
def cb(event):
142+
fired.append((name, event.get_code()))
143+
144+
return cb
145+
146+
for name, code in (
147+
("PRESSED", lv.EVENT.PRESSED),
148+
("RELEASED", lv.EVENT.RELEASED),
149+
("CLICKED", lv.EVENT.CLICKED),
150+
):
151+
btn.add_event_cb(mk(name), code, None)
152+
153+
btn.send_event(lv.EVENT.PRESSED, None)
154+
btn.send_event(lv.EVENT.CLICKED, None)
155+
btn.send_event(lv.EVENT.RELEASED, None)
156+
157+
expected = [
158+
("PRESSED", lv.EVENT.PRESSED),
159+
("CLICKED", lv.EVENT.CLICKED),
160+
("RELEASED", lv.EVENT.RELEASED),
161+
]
162+
if fired != expected:
163+
_fail(f"multi-callback dispatch mismatch: got {fired!r}, expected {expected!r}")
164+
print("OK: multiple filtered callbacks on one object")
165+
166+
133167
def test_event_callback(lv):
134168
test_callbacks(lv)
135169

@@ -181,6 +215,7 @@ def main():
181215
test_blob_dereference(lv)
182216
test_widget(lv)
183217
test_callbacks(lv)
218+
test_multi_callbacks(lv)
184219
finally:
185220
lv.deinit()
186221

0 commit comments

Comments
 (0)