From be7654f89cd7fb3e573c2721c26a68c1a778b324 Mon Sep 17 00:00:00 2001 From: Brad Barnett <127794626+bdbarnett@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:49:20 -0500 Subject: [PATCH] CPython: declare the module GIL-free on free-threaded builds PyInit_lvgl calls PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED) after PyModule_Create, guarded by Py_GIL_DISABLED, so importing lvgl on 3.13t and later no longer turns the GIL back on. LVGL isn't thread-safe: this holds because every lvgl call, and every lvgl object's release, happens on one thread. --- binding/emit_cpython_glue.py | 10 +++++++++ generated/lvgl_python.c | 6 ++++++ tests/test_cpython_gil_declaration.py | 29 +++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 tests/test_cpython_gil_declaration.py diff --git a/binding/emit_cpython_glue.py b/binding/emit_cpython_glue.py index 41f1f96..ff65323 100644 --- a/binding/emit_cpython_glue.py +++ b/binding/emit_cpython_glue.py @@ -278,6 +278,16 @@ def finish_py_module(max_phase): print(" if (m == NULL) {") print(" return NULL;") print(" }") + # Free-threaded builds re-enable the GIL on import unless the module + # says it doesn't need it. LVGL itself isn't thread-safe; the rule that + # makes this safe is one thread for every LVGL call and every LVGL + # object's release, which lvgl-python documents. + print("#ifdef Py_GIL_DISABLED") + print(" if (PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED) < 0) {") + print(" Py_DECREF(m);") + print(" return NULL;") + print(" }") + print("#endif") if registration.int_constants or registration.generated_globals: for int_constant in registration.int_constants: diff --git a/generated/lvgl_python.c b/generated/lvgl_python.c index 528c3b3..fac281f 100644 --- a/generated/lvgl_python.c +++ b/generated/lvgl_python.c @@ -93462,6 +93462,12 @@ PyMODINIT_FUNC PyInit_lvgl(void) if (m == NULL) { return NULL; } +#ifdef Py_GIL_DISABLED + if (PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED) < 0) { + Py_DECREF(m); + return NULL; + } +#endif if (PyModule_AddIntConstant(m, "DPI_DEF", ENUM_LV_DPI_DEF) < 0) return NULL; if (PyModule_AddIntConstant(m, "DRAW_BUF_STRIDE_ALIGN", ENUM_LV_DRAW_BUF_STRIDE_ALIGN) < 0) return NULL; if (PyModule_AddIntConstant(m, "DRAW_BUF_ALIGN", ENUM_LV_DRAW_BUF_ALIGN) < 0) return NULL; diff --git a/tests/test_cpython_gil_declaration.py b/tests/test_cpython_gil_declaration.py new file mode 100644 index 0000000..832c043 --- /dev/null +++ b/tests/test_cpython_gil_declaration.py @@ -0,0 +1,29 @@ +"""The generated CPython module declares that it doesn't need the GIL. + +On a free-threaded build (3.13t and later) importing an extension that +doesn't declare this turns the GIL back on for the whole process. The +binding declares it right after ``PyModule_Create``, guarded by +``Py_GIL_DISABLED`` so other builds compile the same source. LVGL isn't +thread-safe: the declaration holds because every LVGL call, and every +LVGL object's release, happens on one thread (documented in lvgl-python). +""" +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +CPYTHON_C = REPO_ROOT / "generated" / "lvgl_python.c" + + +def test_module_init_declares_gil_not_used(): + text = CPYTHON_C.read_text(encoding="utf-8") + init = text[text.index("PyMODINIT_FUNC PyInit_lvgl(void)"):] + match = re.search( + r"PyObject \*m = PyModule_Create\(&lvgl_module_def\);\n" + r" if \(m == NULL\) \{\n return NULL;\n \}\n" + r"#ifdef Py_GIL_DISABLED\n" + r" if \(PyUnstable_Module_SetGIL\(m, Py_MOD_GIL_NOT_USED\) < 0\) \{", + init, + ) + assert match, "PyInit_lvgl no longer declares Py_MOD_GIL_NOT_USED after PyModule_Create"