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"