From 90f78bbb11247db54d56772c806b631c1a3bee7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 3 Oct 2024 19:13:35 +0200 Subject: [PATCH 01/10] Convert `_curses` into a PEP-489 module. --- Modules/_cursesmodule.c | 97 +++++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 37 deletions(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 61b65675375547..599fb4e29c85a0 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -165,25 +165,26 @@ typedef struct _cursesmodule_state { PyTypeObject *window_type; // PyCursesWindow_Type } _cursesmodule_state; -// For now, we keep a global state variable to prepare for PEP 489. -static _cursesmodule_state curses_global_state; - static inline _cursesmodule_state * -get_cursesmodule_state(PyObject *Py_UNUSED(module)) +get_cursesmodule_state(PyObject *module) { - return &curses_global_state; + void *state = PyModule_GetState(module); + assert(state != NULL); + return (_cursesmodule_state *)state; } static inline _cursesmodule_state * -get_cursesmodule_state_by_cls(PyTypeObject *Py_UNUSED(cls)) +get_cursesmodule_state_by_cls(PyTypeObject *cls) { - return &curses_global_state; + void *state = PyType_GetModuleState(cls); + assert(state != NULL); + return (_cursesmodule_state *)state; } static inline _cursesmodule_state * -get_cursesmodule_state_by_win(PyCursesWindowObject *Py_UNUSED(win)) +get_cursesmodule_state_by_win(PyCursesWindowObject *win) { - return &curses_global_state; + return get_cursesmodule_state_by_cls(Py_TYPE(win)); } /*[clinic input] @@ -211,8 +212,8 @@ static const char *curses_screen_encoding = NULL; * set and this returns 0. Otherwise, this returns 1. * * Since this function can be called in functions that do not - * have a direct access to the module's state, the exception - * type is directly taken from the global state for now. + * have a direct access to the module's state, '_curses.error' + * is imported on demand. */ static inline int _PyCursesCheckFunction(int called, const char *funcname) @@ -220,7 +221,12 @@ _PyCursesCheckFunction(int called, const char *funcname) if (called == TRUE) { return 1; } - PyErr_Format(curses_global_state.error, "must call %s() first", funcname); + PyObject *exc = _PyImport_GetModuleAttrString("_curses", "error"); + if (exc != NULL) { + PyErr_Format(exc, "must call %s() first", funcname); + Py_DECREF(exc); + } + // assert(PyErr_Occurred()); return 0; } @@ -4763,7 +4769,7 @@ _curses_has_extended_color_support_impl(PyObject *module) /* List of functions defined in the module */ -static PyMethodDef PyCurses_methods[] = { +static PyMethodDef _cursesmodule_methods[] = { _CURSES_BAUDRATE_METHODDEF _CURSES_BEEP_METHODDEF _CURSES_CAN_CHANGE_COLOR_METHODDEF @@ -4942,10 +4948,34 @@ curses_capi_capsule_new(void *capi) return capsule; } -/* Module initialization */ +/* Module initialization and cleanup functions */ + +static int +_cursesmodule_traverse(PyObject *mod, visitproc visit, void *arg) +{ + _cursesmodule_state *state = get_cursesmodule_state(mod); + Py_VISIT(state->error); + Py_VISIT(state->window_type); + return 0; +} + +static int +_cursesmodule_clear(PyObject *mod) +{ + _cursesmodule_state *state = get_cursesmodule_state(mod); + Py_CLEAR(state->error); + Py_CLEAR(state->window_type); + return 0; +} + +static void +_cursesmodule_free(void *mod) +{ + (void)_cursesmodule_clear((PyObject *)mod); +} static int -cursesmodule_exec(PyObject *module) +_cursesmodule_exec(PyObject *module) { _cursesmodule_state *state = get_cursesmodule_state(module); /* Initialize object type */ @@ -4987,7 +5017,6 @@ cursesmodule_exec(PyObject *module) return -1; } rc = PyDict_SetItemString(module_dict, "error", state->error); - Py_DECREF(state->error); if (rc < 0) { return -1; } @@ -5181,33 +5210,27 @@ cursesmodule_exec(PyObject *module) /* Initialization function for the module */ +static PyModuleDef_Slot _cursesmodule_slots[] = { + {Py_mod_exec, _cursesmodule_exec}, + {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED}, + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, + {0, NULL} +}; + static struct PyModuleDef _cursesmodule = { PyModuleDef_HEAD_INIT, .m_name = "_curses", - .m_size = -1, - .m_methods = PyCurses_methods, + .m_doc = NULL, + .m_size = sizeof(_cursesmodule_state), + .m_methods = _cursesmodule_methods, + .m_slots = _cursesmodule_slots, + .m_traverse = _cursesmodule_traverse, + .m_clear = _cursesmodule_clear, + .m_free = _cursesmodule_free }; PyMODINIT_FUNC PyInit__curses(void) { - // create the module - PyObject *mod = PyModule_Create(&_cursesmodule); - if (mod == NULL) { - goto error; - } -#ifdef Py_GIL_DISABLED - if (PyUnstable_Module_SetGIL(mod, Py_MOD_GIL_NOT_USED) < 0) { - goto error; - } -#endif - // populate the module - if (cursesmodule_exec(mod) < 0) { - goto error; - } - return mod; - -error: - Py_XDECREF(mod); - return NULL; + return PyModuleDef_Init(&_cursesmodule); } From a0fdf54d4ef69b0b7aeb2e32cb4c68e2c12297ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 3 Oct 2024 19:16:42 +0200 Subject: [PATCH 02/10] blurb --- .../next/Library/2024-10-03-19-16-38.gh-issue-123961.ik1Dgs.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-10-03-19-16-38.gh-issue-123961.ik1Dgs.rst diff --git a/Misc/NEWS.d/next/Library/2024-10-03-19-16-38.gh-issue-123961.ik1Dgs.rst b/Misc/NEWS.d/next/Library/2024-10-03-19-16-38.gh-issue-123961.ik1Dgs.rst new file mode 100644 index 00000000000000..b637b895d0b803 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-10-03-19-16-38.gh-issue-123961.ik1Dgs.rst @@ -0,0 +1,2 @@ +Convert :mod:`curses` to multi-phase initialization (:pep:`489`), thereby +fixing reference leaks at interpreter shutdown. Patch by Bénédikt Tran. From 4b19c4cc1b549e9b7cc22b5b60c236d988eb1ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Thu, 3 Oct 2024 19:17:45 +0200 Subject: [PATCH 03/10] remove global variables suppression --- Tools/c-analyzer/cpython/globals-to-fix.tsv | 3 --- 1 file changed, 3 deletions(-) diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index a0be2a0a203f8c..a52b652940c833 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -358,7 +358,6 @@ Modules/_testclinic.c - TestClass - ##----------------------- ## static types -Modules/_cursesmodule.c - PyCursesWindow_Type - Modules/_datetimemodule.c - PyDateTime_DateTimeType - Modules/_datetimemodule.c - PyDateTime_DateType - Modules/_datetimemodule.c - PyDateTime_DeltaType - @@ -383,7 +382,6 @@ Modules/_tkinter.c - Tktt_Type - Modules/xxlimited_35.c - Xxo_Type - ## exception types -Modules/_cursesmodule.c - PyCursesError - Modules/_tkinter.c - Tkinter_TclError - Modules/xxlimited_35.c - ErrorObject - Modules/xxmodule.c - ErrorObject - @@ -423,7 +421,6 @@ Modules/readline.c - libedit_history_start - Modules/_ctypes/cfield.c - formattable - Modules/_ctypes/malloc_closure.c - free_list - -Modules/_cursesmodule.c - curses_global_state - Modules/_curses_panel.c - lop - Modules/_ssl/debughelpers.c _PySSL_keylog_callback lock - Modules/_tkinter.c - quitMainLoop - From 52396b208855fe0d02e40968836009ba8d263bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 4 Oct 2024 12:22:31 +0200 Subject: [PATCH 04/10] Change `_cursesmodule` prefix into `cursesmodule`. --- Modules/_cursesmodule.c | 114 ++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 599fb4e29c85a0..42c1c81e9859a1 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -163,25 +163,25 @@ typedef chtype attr_t; /* No attr_t type is available */ typedef struct _cursesmodule_state { PyObject *error; // PyCursesError PyTypeObject *window_type; // PyCursesWindow_Type -} _cursesmodule_state; +} cursesmodule_state; -static inline _cursesmodule_state * +static inline cursesmodule_state * get_cursesmodule_state(PyObject *module) { void *state = PyModule_GetState(module); assert(state != NULL); - return (_cursesmodule_state *)state; + return (cursesmodule_state *)state; } -static inline _cursesmodule_state * +static inline cursesmodule_state * get_cursesmodule_state_by_cls(PyTypeObject *cls) { void *state = PyType_GetModuleState(cls); assert(state != NULL); - return (_cursesmodule_state *)state; + return (cursesmodule_state *)state; } -static inline _cursesmodule_state * +static inline cursesmodule_state * get_cursesmodule_state_by_win(PyCursesWindowObject *win) { return get_cursesmodule_state_by_cls(Py_TYPE(win)); @@ -243,7 +243,7 @@ _PyCursesStatefulCheckFunction(PyObject *module, int called, const char *funcnam if (called == TRUE) { return 1; } - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_Format(state->error, "must call %s() first", funcname); return 0; } @@ -281,7 +281,7 @@ _PyCursesStatefulCheckFunction(PyObject *module, int called, const char *funcnam /* Utility Functions */ static inline void -_PyCursesSetError(_cursesmodule_state *state, const char *funcname) +_PyCursesSetError(cursesmodule_state *state, const char *funcname) { if (funcname == NULL) { PyErr_SetString(state->error, catchall_ERR); @@ -302,7 +302,7 @@ PyCursesCheckERR(PyObject *module, int code, const char *fname) if (code != ERR) { Py_RETURN_NONE; } else { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); _PyCursesSetError(state, fname); return NULL; } @@ -314,7 +314,7 @@ PyCursesCheckERR_ForWin(PyCursesWindowObject *win, int code, const char *fname) if (code != ERR) { Py_RETURN_NONE; } else { - _cursesmodule_state *state = get_cursesmodule_state_by_win(win); + cursesmodule_state *state = get_cursesmodule_state_by_win(win); _PyCursesSetError(state, fname); return NULL; } @@ -752,7 +752,7 @@ Window_TwoArgNoReturnFunction(wresize, int, "ii;lines,columns") /* Allocation and deallocation of Window Objects */ static PyObject * -PyCursesWindow_New(_cursesmodule_state *state, +PyCursesWindow_New(cursesmodule_state *state, WINDOW *win, const char *encoding) { if (encoding == NULL) { @@ -1411,12 +1411,12 @@ _curses_window_derwin_impl(PyCursesWindowObject *self, int group_left_1, win = derwin(self->win,nlines,ncols,begin_y,begin_x); if (win == NULL) { - _cursesmodule_state *state = get_cursesmodule_state_by_win(self); + cursesmodule_state *state = get_cursesmodule_state_by_win(self); PyErr_SetString(state->error, catchall_NULL); return NULL; } - _cursesmodule_state *state = get_cursesmodule_state_by_win(self); + cursesmodule_state *state = get_cursesmodule_state_by_win(self); return PyCursesWindow_New(state, win, NULL); } @@ -1565,7 +1565,7 @@ _curses_window_getkey_impl(PyCursesWindowObject *self, int group_right_1, /* getch() returns ERR in nodelay mode */ PyErr_CheckSignals(); if (!PyErr_Occurred()) { - _cursesmodule_state *state = get_cursesmodule_state_by_win(self); + cursesmodule_state *state = get_cursesmodule_state_by_win(self); PyErr_SetString(state->error, "no input"); } return NULL; @@ -1625,7 +1625,7 @@ _curses_window_get_wch_impl(PyCursesWindowObject *self, int group_right_1, return NULL; /* get_wch() returns ERR in nodelay mode */ - _cursesmodule_state *state = get_cursesmodule_state_by_win(self); + cursesmodule_state *state = get_cursesmodule_state_by_win(self); PyErr_SetString(state->error, "no input"); return NULL; } @@ -2139,7 +2139,7 @@ _curses_window_noutrefresh_impl(PyCursesWindowObject *self) #ifdef py_is_pad if (py_is_pad(self->win)) { if (!group_right_1) { - _cursesmodule_state *state = get_cursesmodule_state_by_win(self); + cursesmodule_state *state = get_cursesmodule_state_by_win(self); PyErr_SetString(state->error, "noutrefresh() called for a pad " "requires 6 arguments"); @@ -2364,7 +2364,7 @@ _curses_window_refresh_impl(PyCursesWindowObject *self, int group_right_1, #ifdef py_is_pad if (py_is_pad(self->win)) { if (!group_right_1) { - _cursesmodule_state *state = get_cursesmodule_state_by_win(self); + cursesmodule_state *state = get_cursesmodule_state_by_win(self); PyErr_SetString(state->error, "refresh() for a pad requires 6 arguments"); return NULL; @@ -2447,12 +2447,12 @@ _curses_window_subwin_impl(PyCursesWindowObject *self, int group_left_1, win = subwin(self->win, nlines, ncols, begin_y, begin_x); if (win == NULL) { - _cursesmodule_state *state = get_cursesmodule_state_by_win(self); + cursesmodule_state *state = get_cursesmodule_state_by_win(self); PyErr_SetString(state->error, catchall_NULL); return NULL; } - _cursesmodule_state *state = get_cursesmodule_state_by_win(self); + cursesmodule_state *state = get_cursesmodule_state_by_win(self); return PyCursesWindow_New(state, win, self->encoding); } @@ -2852,7 +2852,7 @@ _curses_color_content_impl(PyObject *module, int color_number) PyCursesStatefulInitialisedColor(module); if (_COLOR_CONTENT_FUNC(color_number, &r, &g, &b) == ERR) { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_Format(state->error, "%s() returned ERR", Py_STRINGIFY(_COLOR_CONTENT_FUNC)); return NULL; @@ -3092,7 +3092,7 @@ _curses_getmouse_impl(PyObject *module) rtn = getmouse( &event ); if (rtn == ERR) { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_SetString(state->error, "getmouse() returned ERR"); return NULL; } @@ -3187,11 +3187,11 @@ _curses_getwin(PyObject *module, PyObject *file) fseek(fp, 0, 0); win = getwin(fp); if (win == NULL) { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_SetString(state->error, catchall_NULL); goto error; } - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); res = PyCursesWindow_New(state, win, NULL); error: @@ -3338,7 +3338,7 @@ _curses_init_pair_impl(PyObject *module, int pair_number, int fg, int bg) COLOR_PAIRS - 1); } else { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_Format(state->error, "%s() returned ERR", Py_STRINGIFY(_CURSES_INIT_PAIR_FUNC)); } @@ -3364,14 +3364,14 @@ _curses_initscr_impl(PyObject *module) if (curses_initscr_called) { wrefresh(stdscr); - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); return PyCursesWindow_New(state, stdscr, NULL); } win = initscr(); if (win == NULL) { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_SetString(state->error, catchall_NULL); return NULL; } @@ -3468,7 +3468,7 @@ _curses_initscr_impl(PyObject *module) SetDictInt("COLS", COLS); #undef SetDictInt - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyObject *winobj = PyCursesWindow_New(state, win, NULL); if (winobj == NULL) { return NULL; @@ -3502,7 +3502,7 @@ _curses_setupterm_impl(PyObject *module, const char *term, int fd) sys_stdout = PySys_GetObject("stdout"); if (sys_stdout == NULL || sys_stdout == Py_None) { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_SetString(state->error, "lost sys.stdout"); return NULL; } @@ -3523,7 +3523,7 @@ _curses_setupterm_impl(PyObject *module, const char *term, int fd) s = "setupterm: could not find terminfo database"; } - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_SetString(state->error, s); return NULL; } @@ -3841,12 +3841,12 @@ _curses_newpad_impl(PyObject *module, int nlines, int ncols) win = newpad(nlines, ncols); if (win == NULL) { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_SetString(state->error, catchall_NULL); return NULL; } - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); return PyCursesWindow_New(state, win, NULL); } @@ -3882,12 +3882,12 @@ _curses_newwin_impl(PyObject *module, int nlines, int ncols, win = newwin(nlines,ncols,begin_y,begin_x); if (win == NULL) { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_SetString(state->error, catchall_NULL); return NULL; } - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); return PyCursesWindow_New(state, win, NULL); } @@ -4002,7 +4002,7 @@ _curses_pair_content_impl(PyObject *module, int pair_number) COLOR_PAIRS - 1); } else { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_Format(state->error, "%s() returned ERR", Py_STRINGIFY(_CURSES_PAIR_CONTENT_FUNC)); } @@ -4333,7 +4333,7 @@ _curses_start_color_impl(PyObject *module) PyCursesStatefulInitialised(module); if (start_color() == ERR) { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_SetString(state->error, "start_color() returned ERR"); return NULL; } @@ -4486,7 +4486,7 @@ _curses_tparm_impl(PyObject *module, const char *str, int i1, int i2, int i3, result = tparm((char *)str,i1,i2,i3,i4,i5,i6,i7,i8,i9); if (!result) { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_SetString(state->error, "tparm() returned NULL"); return NULL; } @@ -4688,7 +4688,7 @@ _curses_use_default_colors_impl(PyObject *module) if (code != ERR) { Py_RETURN_NONE; } else { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); PyErr_SetString(state->error, "use_default_colors() returned ERR"); return NULL; } @@ -4769,7 +4769,7 @@ _curses_has_extended_color_support_impl(PyObject *module) /* List of functions defined in the module */ -static PyMethodDef _cursesmodule_methods[] = { +static PyMethodDef cursesmodule_methods[] = { _CURSES_BAUDRATE_METHODDEF _CURSES_BEEP_METHODDEF _CURSES_CAN_CHANGE_COLOR_METHODDEF @@ -4878,7 +4878,7 @@ curses_capi_start_color_called(void) } static void * -curses_capi_new(_cursesmodule_state *state) +curses_capi_new(cursesmodule_state *state) { assert(state->window_type != NULL); void **capi = (void **)PyMem_Calloc(PyCurses_API_pointers, sizeof(void *)); @@ -4951,33 +4951,33 @@ curses_capi_capsule_new(void *capi) /* Module initialization and cleanup functions */ static int -_cursesmodule_traverse(PyObject *mod, visitproc visit, void *arg) +cursesmodule_traverse(PyObject *mod, visitproc visit, void *arg) { - _cursesmodule_state *state = get_cursesmodule_state(mod); + cursesmodule_state *state = get_cursesmodule_state(mod); Py_VISIT(state->error); Py_VISIT(state->window_type); return 0; } static int -_cursesmodule_clear(PyObject *mod) +cursesmodule_clear(PyObject *mod) { - _cursesmodule_state *state = get_cursesmodule_state(mod); + cursesmodule_state *state = get_cursesmodule_state(mod); Py_CLEAR(state->error); Py_CLEAR(state->window_type); return 0; } static void -_cursesmodule_free(void *mod) +cursesmodule_free(void *mod) { - (void)_cursesmodule_clear((PyObject *)mod); + (void)cursesmodule_clear((PyObject *)mod); } static int -_cursesmodule_exec(PyObject *module) +cursesmodule_exec(PyObject *module) { - _cursesmodule_state *state = get_cursesmodule_state(module); + cursesmodule_state *state = get_cursesmodule_state(module); /* Initialize object type */ state->window_type = (PyTypeObject *)PyType_FromModuleAndSpec( module, &PyCursesWindow_Type_spec, NULL); @@ -5210,27 +5210,27 @@ _cursesmodule_exec(PyObject *module) /* Initialization function for the module */ -static PyModuleDef_Slot _cursesmodule_slots[] = { - {Py_mod_exec, _cursesmodule_exec}, +static PyModuleDef_Slot cursesmodule_slots[] = { + {Py_mod_exec, cursesmodule_exec}, {Py_mod_multiple_interpreters, Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, {0, NULL} }; -static struct PyModuleDef _cursesmodule = { +static struct PyModuleDef cursesmodule = { PyModuleDef_HEAD_INIT, .m_name = "_curses", .m_doc = NULL, - .m_size = sizeof(_cursesmodule_state), - .m_methods = _cursesmodule_methods, - .m_slots = _cursesmodule_slots, - .m_traverse = _cursesmodule_traverse, - .m_clear = _cursesmodule_clear, - .m_free = _cursesmodule_free + .m_size = sizeof(cursesmodule_state), + .m_methods = cursesmodule_methods, + .m_slots = cursesmodule_slots, + .m_traverse = cursesmodule_traverse, + .m_clear = cursesmodule_clear, + .m_free = cursesmodule_free }; PyMODINIT_FUNC PyInit__curses(void) { - return PyModuleDef_Init(&_cursesmodule); + return PyModuleDef_Init(&cursesmodule); } From b7337a7b4adba2d90d34f193cf6663aca7bd5000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 4 Oct 2024 12:26:55 +0200 Subject: [PATCH 05/10] use an anonymous structure --- Modules/_cursesmodule.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 42c1c81e9859a1..ce3de9a7e92378 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -160,9 +160,9 @@ typedef chtype attr_t; /* No attr_t type is available */ #define _CURSES_PAIR_CONTENT_FUNC pair_content #endif /* _NCURSES_EXTENDED_COLOR_FUNCS */ -typedef struct _cursesmodule_state { - PyObject *error; // PyCursesError - PyTypeObject *window_type; // PyCursesWindow_Type +typedef struct { + PyObject *error; // curses exception type + PyTypeObject *window_type; // exposed by PyCursesWindow_Type } cursesmodule_state; static inline cursesmodule_state * From 9e0dd702401a1e580c74179f2e5595f52ea6d2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:28:24 +0200 Subject: [PATCH 06/10] fix free-threaded builds --- Modules/_cursesmodule.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index ce3de9a7e92378..d5afbcb2dd18a0 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -4898,8 +4898,9 @@ curses_capi_free(void *capi) { assert(capi != NULL); void **capi_ptr = (void **)capi; - assert(capi_ptr[0] != NULL); - Py_DECREF(capi_ptr[0]); // decref curses window type + // In free-threaded builds, capi_ptr[0] may have been already cleared + // by curses_capi_capsule_destructor(), hence the use of Py_XDECREF(). + Py_XDECREF(capi_ptr[0]); // decref curses window type PyMem_Free(capi_ptr); } From 72a89dff39c80dafa41c564c018fa9ded9d64724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:52:06 +0200 Subject: [PATCH 07/10] un-comment `assert` --- Modules/_cursesmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index d5afbcb2dd18a0..051d141b929e22 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -226,7 +226,7 @@ _PyCursesCheckFunction(int called, const char *funcname) PyErr_Format(exc, "must call %s() first", funcname); Py_DECREF(exc); } - // assert(PyErr_Occurred()); + assert(PyErr_Occurred()); return 0; } From 8a376734340eabce9f19ba1836f039f76c0d1f9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:10:43 +0200 Subject: [PATCH 08/10] disallow the `curses` to be loaded more than once per process --- Modules/_cursesmodule.c | 10 ++++++++++ Tools/c-analyzer/cpython/globals-to-fix.tsv | 1 + 2 files changed, 11 insertions(+) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 051d141b929e22..11f7b88d3bf08c 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -4975,9 +4975,19 @@ cursesmodule_free(void *mod) (void)cursesmodule_clear((PyObject *)mod); } +/* Indicate whether the module has already been loaded or not. */ +static int curses_module_loaded = 0; + static int cursesmodule_exec(PyObject *module) { + if (curses_module_loaded) { + PyErr_SetString(PyExc_ImportError, + "module 'curses' can only be loaded once per process"); + return -1; + } + curses_module_loaded = 1; + cursesmodule_state *state = get_cursesmodule_state(module); /* Initialize object type */ state->window_type = (PyTypeObject *)PyType_FromModuleAndSpec( diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv index a52b652940c833..badd7b79102310 100644 --- a/Tools/c-analyzer/cpython/globals-to-fix.tsv +++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv @@ -407,6 +407,7 @@ Modules/_tkinter.c - trbInCmd - Include/datetime.h - PyDateTimeAPI - Modules/_ctypes/cfield.c _ctypes_get_fielddesc initialized - Modules/_ctypes/malloc_closure.c - _pagesize - +Modules/_cursesmodule.c - curses_module_loaded - Modules/_cursesmodule.c - curses_initscr_called - Modules/_cursesmodule.c - curses_setupterm_called - Modules/_cursesmodule.c - curses_start_color_called - From 077b195caedfd1a8152a1c67aea829e7737fa48a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 8 Oct 2024 11:18:46 +0200 Subject: [PATCH 09/10] remove un-necessary member initialization --- Modules/_cursesmodule.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 11f7b88d3bf08c..28e9429b9b3cb1 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -193,6 +193,9 @@ class _curses.window "PyCursesWindowObject *" "clinic_state()->window_type" [clinic start generated code]*/ /*[clinic end generated code: output=da39a3ee5e6b4b0d input=ae6cb623018f2cbc]*/ +/* Indicate whether the module has already been loaded or not. */ +static int curses_module_loaded = 0; + /* Tells whether setupterm() has been called to initialise terminfo. */ static int curses_setupterm_called = FALSE; @@ -4975,9 +4978,6 @@ cursesmodule_free(void *mod) (void)cursesmodule_clear((PyObject *)mod); } -/* Indicate whether the module has already been loaded or not. */ -static int curses_module_loaded = 0; - static int cursesmodule_exec(PyObject *module) { @@ -5231,7 +5231,6 @@ static PyModuleDef_Slot cursesmodule_slots[] = { static struct PyModuleDef cursesmodule = { PyModuleDef_HEAD_INIT, .m_name = "_curses", - .m_doc = NULL, .m_size = sizeof(cursesmodule_state), .m_methods = cursesmodule_methods, .m_slots = cursesmodule_slots, From c3263ab40917af9cfa31bb2ed8994883306801d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:55:48 +0200 Subject: [PATCH 10/10] allow reloading if garbage collected --- Modules/_cursesmodule.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index 28e9429b9b3cb1..27d5df08de933e 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -4976,6 +4976,7 @@ static void cursesmodule_free(void *mod) { (void)cursesmodule_clear((PyObject *)mod); + curses_module_loaded = 0; // allow reloading once garbage-collected } static int