Skip to content

Commit 0c9b2ef

Browse files
committed
Merge main; sync bindings 22f8383 (lvgl-bindings#36 as merged)
The generated files, lv_conf.h and fs_driver.py are byte-identical to lvgl-bindings at 22f8383, and the lvgl submodule pin is unchanged; only LVGL_BINDINGS_COMMIT moves from the pre-merge be7654f.
2 parents 145a994 + 2ff7e8e commit 0c9b2ef

2 files changed

Lines changed: 120 additions & 1 deletion

File tree

‎LVGL_BINDINGS_COMMIT‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
be7654f89cd7fb3e573c2721c26a68c1a778b324
1+
22f8383eedc785a6c87c57c7ed46c95c454cebe3

‎tests/test_callback_references.py‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# SPDX-License-Identifier: MIT
2+
"""References a callback trampoline holds while LVGL calls into Python.
3+
4+
- A callback that replaces its own registration while it runs
5+
(``timer.set_cb(other)`` from inside the timer callback) stays alive until
6+
it returns. Before, the only reference was the callback dict's, so the
7+
replacement freed the callback mid-call.
8+
- Arguments passed to a callback are released after the call. Before, every
9+
call leaked one reference to each argument, so a widget handed to a
10+
``tree_walk`` callback was never freed.
11+
12+
The checks run in one subprocess under MALLOC_PERTURB_ so a use-after-free
13+
shows up as a crash rather than luck.
14+
"""
15+
16+
import json
17+
import os
18+
import subprocess
19+
import sys
20+
import unittest
21+
22+
23+
_SCRIPT = r"""
24+
import gc
25+
import json
26+
import sys
27+
import weakref
28+
29+
import lvgl as lv
30+
31+
lv.init()
32+
disp = lv.display_create(64, 64)
33+
disp.set_color_format(lv.COLOR_FORMAT.RGB565)
34+
buf = lv.draw_buf_create(64, 64, lv.COLOR_FORMAT.RGB565, 0)
35+
disp.set_draw_buffers(buf, None)
36+
disp.set_render_mode(lv.DISPLAY_RENDER_MODE.PARTIAL)
37+
scr = lv.screen_active()
38+
out = {}
39+
40+
41+
def pump(ms):
42+
for _ in range(ms // 5):
43+
lv.tick_inc(5)
44+
lv.timer_handler()
45+
46+
47+
# A callback that replaces itself stays alive until it returns.
48+
timer = lv.timer_create_basic()
49+
timer.set_period(10)
50+
51+
52+
class Replaces:
53+
def __call__(self, t):
54+
ref = weakref.ref(self)
55+
del self
56+
timer.set_cb(lambda t: None)
57+
gc.collect()
58+
out["alive_after_replacing_itself"] = ref() is not None
59+
60+
61+
timer.set_cb(Replaces())
62+
pump(50)
63+
timer.delete()
64+
65+
66+
# Callback arguments are released after the call.
67+
child = lv.button(scr)
68+
69+
def walk(obj, user_data):
70+
return lv.obj.TREE_WALK.NEXT
71+
72+
before = sys.getrefcount(child)
73+
for _ in range(10):
74+
scr.tree_walk(walk, None)
75+
out["tree_walk_arg_refcount_delta"] = sys.getrefcount(child) - before
76+
77+
child.delete()
78+
lv.deinit()
79+
print("RESULT " + json.dumps(out))
80+
"""
81+
82+
83+
def _run():
84+
env = dict(os.environ)
85+
env["MALLOC_PERTURB_"] = "165"
86+
proc = subprocess.run(
87+
[sys.executable, "-c", _SCRIPT],
88+
capture_output=True,
89+
text=True,
90+
timeout=120,
91+
env=env,
92+
)
93+
for line in proc.stdout.splitlines():
94+
if line.startswith("RESULT "):
95+
return proc, json.loads(line[len("RESULT "):])
96+
return proc, None
97+
98+
99+
class CallbackReferenceTest(unittest.TestCase):
100+
@classmethod
101+
def setUpClass(cls):
102+
cls.proc, cls.out = _run()
103+
104+
def setUp(self):
105+
if self.out is None:
106+
self.fail(
107+
"script did not finish (exit %s)\nstdout:\n%s\nstderr:\n%s"
108+
% (self.proc.returncode, self.proc.stdout, self.proc.stderr)
109+
)
110+
111+
def test_callback_outlives_replacing_itself(self):
112+
self.assertIs(self.out.get("alive_after_replacing_itself"), True)
113+
114+
def test_callback_arguments_are_released(self):
115+
self.assertEqual(self.out["tree_walk_arg_refcount_delta"], 0)
116+
117+
118+
if __name__ == "__main__":
119+
unittest.main()

0 commit comments

Comments
 (0)