-
-
Notifications
You must be signed in to change notification settings - Fork 30.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gh-123290: curses: fix reference leaks in error-branches and cleanup module's initialization #123910
Conversation
Modules/_cursesmodule.c
Outdated
if (wo->encoding != NULL) | ||
if (wo->win != stdscr && wo->win != NULL) { | ||
delwin(wo->win); | ||
wo->win = NULL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that it's useful since we free() the memory a few lines below. Same remark for wo->encoding = NULL;
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't really know whether delwin(wo->win)
checks if its argument is NULL or not so I wanted to be sure (I'll check the manual more in detail tomorrow).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FTR, from the man page:
X/Open defines no error conditions. In this implementation
delwin
returns an error if the window pointer is null, or if the window is the parent of another window.
This implementation also maintains a list of windows, and checks that the pointer passed to delwin is one that it created, returning an error if it was not.
I added a comment saying that we silently ignore any error in delwin
.
delwin(wo->win); | ||
wo->win = NULL; | ||
} | ||
if (wo->encoding != NULL) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check is not needed, PyMem_Free(NULL) is well defined: it does nothing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see why I thought it wasn't. Actually, the online docs tell me that it's a no-op but the implementation does not tell me directly. I'll make the modifications tomorrow as well (actually I'll do everything tomorrow).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PyMem_Free():
If p is NULL, no operation is performed.
Modules/_cursesmodule.c
Outdated
return NULL; | ||
} | ||
PyObject *result = PyCursesCheckERR(resize_term(nlines, ncols), "resize_term"); | ||
CHECK_NOT_NULL_OR_ERROR(result); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dislike CHECK_NOT_NULL_OR_ERROR() macro, it makes the code less readable. I prefer the explicit existing if (!result) return NULL;
code.
Modules/_cursesmodule.c
Outdated
} | ||
PyObject *result = PyCursesCheckERR(resize_term(nlines, ncols), "resize_term"); | ||
CHECK_NOT_NULL_OR_ERROR(result); | ||
CHECK_RET_FLAG_OR_ERROR(update_lines_cols(module)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this macro. I would prefer:
if (!update_lines_cols(module)) goto error;
CHECK_RET_FLAG_OR_ERROR
+ goto makes the code less straightforward.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe my naming sense is terrible but it was something I found in the code sometimes (e.g., CHECK_INT
in _decimal.c
). But I'll change the macros since it's probably better to reduce the diff rather than reducing the overall number of lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EDIT: I think I know why the macro was confusing (probably because of the FLAG part; should have been CHECK_RET_BOOL but it's not really a good idea either). I'll remove it and use an explicit if (!(...))
.
|
||
static char *screen_encoding = NULL; | ||
static char *CURSES_SCREEN_ENCODING = NULL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these 4 variables moved to the module state later? If yes, these changes look redundant. If no, why not? :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these 4 variables moved to the module state later?
No. Because I totally forgot about them in the other PR. But here, when I was isolating the components to select, I was like "hum, but those are actually global variables" and it confused me to have them in lowercase as if they were local variables :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that i'm ok with renaming these variables, but I'm less comfortable with uppercase names. Uppercase is somehow reserved to macros in C (in CPython at least). I suggest to rename these variables to lowercase.
Modules/_cursesmodule.c
Outdated
} | ||
CHECK_NOT_NULL_OR_ERROR(o); | ||
CHECK_RET_CODE_OR_ERROR(PyObject_SetAttrString(exposed_module, "LINES", o)); | ||
CHECK_RET_CODE_OR_ERROR(PyObject_SetAttrString(private_module, "LINES", o)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect a macro getting a module, a key and a value. Rather than a macro taking a function call.
Modules/_cursesmodule.c
Outdated
} \ | ||
} while (0) | ||
|
||
#define DICT_ADD_INT_VALUE_OR_ERROR(DICT_OBJECT, STRING, VALUE) \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to rename the macro to DICT_ADD_INT_VALUE
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about it but wondered whether it was necessary to indicate in the name the fact that we jump to the error
label. If you think a comment is sufficient, I can rename it.
Modules/_cursesmodule.c
Outdated
if (!(STATUS)) { \ | ||
goto error; \ | ||
} \ | ||
} while (0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that these 3 macros are useful. They hide the goto which makes it to follow code using them (IMO).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a matter of personal taste but I can also understand your stand on that matter. Personally, what I find hard to read is when you have all those if(...) { goto error; }
spread on 3 lines. I would find them much easier to read if PEP 7 allowed me to write if (!(STATUS)) goto error;
. On the other hand, with those macros, you may be also introduce errors where you need to free resources before jumping to error
so they should be used carefully...
I had alternatives where you would just put whatever cleanup statements you want when invoking the macro but I felt it would be even less readable...
Thanks for the review Victor. I'll address your comments tomorrow! |
31cbba0
to
384716d
Compare
32996a1
to
356b00c
Compare
@vstinner I eventually removed the I also added a local macro for adding the LINES and COLS constants. Please tell me if there is anything else to change! (again, thanks a lot for your feedback). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have mixed feelings about this PR. IMO it changes too many different things at once, so it's hard to review. For example, it adds a module argument to update_lines_cols(): the rationale for this change is unclear to me. It also adds ;
to PyCursesInitialised
which is unrelated. It also renames some variables. And the main part: fixing reference leaks.
Would it be possible to try to write a minimum change only to fix refleaks? Without the large refactoring?
#define CHECK_STATE_FLAG(FUNC_TO_CALL, GLOBAL_FLAG) \ | ||
do { \ | ||
if ((GLOBAL_FLAG) != TRUE) { \ | ||
assert(PyCursesError != NULL); \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would suggest to remove this assertion. It sounds unlikely that the exception is NULL when this macro is used. If there is a risk, I would suggest to handle the test in the caller instead.
Py_DECREF(m); | ||
#define MODULE_ADD_INT_CONSTANT(MODULE, NAME, VALUE) \ | ||
do { \ | ||
if (PyModule_AddIntConstant((MODULE), (NAME), (long)(VALUE)) < 0) { \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this (long)
cast really needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the compiler won't complain but I'm not entirely sure since we use macros. If an implementation uses typed global variables, we might want to force the cast (I've seen both usages of explicit casting and sometimes not but I don't think the cast hurts)
Py_DECREF(c); | ||
if (start_color() != ERR) { | ||
CURSES_INITIALISED_COLORS = TRUE; | ||
PyObject *module_dict = PyModule_GetDict(module); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PyObject *module_dict = PyModule_GetDict(module); | |
PyObject *module_dict = PyModule_GetDict(module); // borrowed ref |
return NULL; | ||
if (PyDict_SetItemString(ModDict, "COLORS", c) < 0) { | ||
Py_DECREF(c); | ||
if (start_color() != ERR) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you exit early? Start with if (start_color() == ERR) { ... return ... }
so you can remove one indentation level.
|
||
static char *screen_encoding = NULL; | ||
static char *CURSES_SCREEN_ENCODING = NULL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that i'm ok with renaming these variables, but I'm less comfortable with uppercase names. Uppercase is somehow reserved to macros in C (in CPython at least). I suggest to rename these variables to lowercase.
@@ -3967,49 +4010,32 @@ _curses_qiflush_impl(PyObject *module, int flag) | |||
/* Internal helper used for updating curses.LINES, curses.COLS, _curses.LINES | |||
* and _curses.COLS */ | |||
#if defined(HAVE_CURSES_RESIZETERM) || defined(HAVE_CURSES_RESIZE_TERM) | |||
static int | |||
update_lines_cols(void) | |||
static int /* 1 on success, 0 on failure */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you move this comment in the comment 1 line above?
|
||
#undef DICT_ADD_INT_VALUE | ||
#undef CHECK_RET_CODE | ||
#undef CHECK_STATE_FLAG |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think that it's useful to undefine macros at the end of the file. It's ok to have macros defined for a whole C file.
I discussed with Victor and I'll split the work (again) into smaller PRs:
(Hence, I'm closing this one, again). |
In this PR, we prepare the
cursesmodule
for #123630 and clean-up some bits. To that end, we:do { ... } while (0)
constructions.There are some un-necessary
PyObject *
casts that will be removed if I were to move from a static type to a heap type, so I won't do it now.Now I have an important question. There is a global variable named
ModDict
which is set to the module's dict upon initialization. It is then used ininitscr()
to add other constants. However, AFACIT, this variable is only used as a convenience to avoid re-querying the module's dict, right? Or is there some deeper meaning where I must use a global variable to store tha module's dict?cc @vstinner @encukou
curses
error-branches #123290