Skip to content
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

Closed
wants to merge 12 commits into from

Conversation

picnixz
Copy link
Contributor

@picnixz picnixz commented Sep 10, 2024

In this PR, we prepare the cursesmodule for #123630 and clean-up some bits. To that end, we:

  • ensure that error branches correctly free resources (even if the error is usually "critical", in the sense that the interpreter would exit after reporting it).
  • protect macros against dangling else using do { ... } while (0) constructions.
  • use upper-case names for global variables; this helps the readability of the module.

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 in initscr() 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

@picnixz picnixz changed the title gh-123630: curses: fix reference leaks in error-branches and cleanup module's initialization gh-123290: curses: fix reference leaks in error-branches and cleanup module's initialization Sep 10, 2024
if (wo->encoding != NULL)
if (wo->win != stdscr && wo->win != NULL) {
delwin(wo->win);
wo->win = NULL;
Copy link
Member

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

Copy link
Contributor Author

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

Copy link
Contributor Author

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) {
Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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.

https://docs.python.org/dev/c-api/memory.html#c.PyMem_Free

return NULL;
}
PyObject *result = PyCursesCheckERR(resize_term(nlines, ncols), "resize_term");
CHECK_NOT_NULL_OR_ERROR(result);
Copy link
Member

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.

}
PyObject *result = PyCursesCheckERR(resize_term(nlines, ncols), "resize_term");
CHECK_NOT_NULL_OR_ERROR(result);
CHECK_RET_FLAG_OR_ERROR(update_lines_cols(module));
Copy link
Member

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.

Copy link
Contributor Author

@picnixz picnixz Sep 10, 2024

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.

Copy link
Contributor Author

@picnixz picnixz Sep 10, 2024

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;
Copy link
Member

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? :-)

Copy link
Contributor Author

@picnixz picnixz Sep 10, 2024

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 :(

Copy link
Member

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.

}
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));
Copy link
Member

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.

} \
} while (0)

#define DICT_ADD_INT_VALUE_OR_ERROR(DICT_OBJECT, STRING, VALUE) \
Copy link
Member

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.

Copy link
Contributor Author

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.

if (!(STATUS)) { \
goto error; \
} \
} while (0)
Copy link
Member

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

Copy link
Contributor Author

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

@picnixz
Copy link
Contributor Author

picnixz commented Sep 10, 2024

Thanks for the review Victor. I'll address your comments tomorrow!

@picnixz picnixz force-pushed the curses/consistent-cleanup-123630 branch from 31cbba0 to 384716d Compare September 11, 2024 10:54
@picnixz picnixz force-pushed the curses/consistent-cleanup-123630 branch from 32996a1 to 356b00c Compare September 11, 2024 11:07
@picnixz
Copy link
Contributor Author

picnixz commented Sep 11, 2024

@vstinner I eventually removed the if (x == NULL) goto error and the if (!x) goto error macros. I kept the CHECK_RET_CODE_OR_ERROR and the macro for adding an int to a dict but removed their _OR_ERROR suffixes and wrote a comment instead.

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

Copy link
Member

@vstinner vstinner left a 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); \
Copy link
Member

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) { \
Copy link
Member

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?

Copy link
Contributor Author

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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) {
Copy link
Member

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;
Copy link
Member

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 */
Copy link
Member

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
Copy link
Member

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.

@picnixz
Copy link
Contributor Author

picnixz commented Sep 11, 2024

I discussed with Victor and I'll split the work (again) into smaller PRs:

  • PR fixing ref-leaks in error branches
  • PR protecting existing macros using do { ... } while (0) constructions
  • PR renaming global variables and removing the ModDict global variable as well.

(Hence, I'm closing this one, again).

@picnixz picnixz closed this Sep 11, 2024
@picnixz picnixz deleted the curses/consistent-cleanup-123630 branch September 11, 2024 14:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants