gh-123961: Convert _curses to a multi-phase init module (PEP-489) (#124965)

This commit is contained in:
Bénédikt Tran 2024-10-08 13:42:44 +02:00 committed by GitHub
parent ba14dfafd9
commit e4292c0410
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 116 additions and 82 deletions

View file

@ -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.

View file

@ -160,30 +160,31 @@ 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
} _cursesmodule_state;
typedef struct {
PyObject *error; // curses exception type
PyTypeObject *window_type; // exposed by 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))
static inline cursesmodule_state *
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))
static inline cursesmodule_state *
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))
static inline cursesmodule_state *
get_cursesmodule_state_by_win(PyCursesWindowObject *win)
{
return &curses_global_state;
return get_cursesmodule_state_by_cls(Py_TYPE(win));
}
/*[clinic input]
@ -192,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;
@ -211,8 +215,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 +224,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;
}
@ -237,7 +246,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;
}
@ -275,7 +284,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);
@ -296,7 +305,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;
}
@ -308,7 +317,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;
}
@ -746,7 +755,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) {
@ -1405,12 +1414,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);
}
@ -1559,7 +1568,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;
@ -1619,7 +1628,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;
}
@ -2133,7 +2142,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");
@ -2358,7 +2367,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;
@ -2441,12 +2450,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);
}
@ -2846,7 +2855,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;
@ -3086,7 +3095,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;
}
@ -3181,11 +3190,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:
@ -3332,7 +3341,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));
}
@ -3358,14 +3367,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;
}
@ -3462,7 +3471,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;
@ -3496,7 +3505,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;
}
@ -3517,7 +3526,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;
}
@ -3835,12 +3844,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);
}
@ -3876,12 +3885,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);
}
@ -3996,7 +4005,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));
}
@ -4327,7 +4336,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;
}
@ -4480,7 +4489,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;
}
@ -4682,7 +4691,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;
}
@ -4763,7 +4772,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
@ -4872,7 +4881,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 *));
@ -4892,8 +4901,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);
}
@ -4942,12 +4952,44 @@ 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);
curses_module_loaded = 0; // allow reloading once garbage-collected
}
static int
cursesmodule_exec(PyObject *module)
{
_cursesmodule_state *state = get_cursesmodule_state(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(
module, &PyCursesWindow_Type_spec, NULL);
@ -4987,7 +5029,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 +5222,26 @@ cursesmodule_exec(PyObject *module)
/* Initialization function for the module */
static struct PyModuleDef _cursesmodule = {
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_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);
}

View file

@ -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 -
@ -409,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 -
@ -423,7 +422,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 -

Can't render this file because it has a wrong number of fields in line 4.