From 063dae8c573a393e3494c997227aaa21e562eb45 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 22 Aug 2017 14:01:02 -0700 Subject: [PATCH] Don't abort on FT2Font weakref. Currently, import weakref, matplotlib.ft2font weakref.ref(matplotlib.ft2font.FT2Font("/usr/share/fonts/TTF/DejaVuSans.ttf")) aborts Py3 with terminate called after throwing an instance of 'char const*' Fatal Python error: Aborted whereas on Py2 a normal `TypeError: cannot create weak reference to 'matplotlib.ft2font.FT2Font' object` is raised. This is because the following happens: - Py3 sets the TypeError for failure to create a weakref. - The FT2Font object gets GC'd as nothing is referring to it (yes, creating a weakref to a temporary is a bit silly). - The FT2Font destructor calls close_file_callback, which calls mpl_PyFile_DupClose, which calls PyObject_AsFileDescriptor... which, on Py3, calls the fileno() method on the file object. - PyObject_AsFileDescriptor fails because the originally set TypeError has not been cleared, so mpl_PyFile_DupClose likewise fails, causing close_file_callback to throw a C++ exception. Instead, carefully stash and restore the current exception state, both in mpl_PyFile_DupClose, and likewise below, in mpl_PyFile_CloseFile (the latter is needed because otherwise PyObject_CallMethod will clear the exception, cf comment in CPython's Objects/abstract.c: /* PyObject_Call() must not be called with an exception set, because it may clear it (directly or indirectly) and so the caller loses its exception */ Note that if an exception *actually* gets raised by mpl_PyFile_DupClose or mpl_PyFile_CloseFile, it will overwrite the TypeError. Strictly speaking, on Py3, it may be preferrable to chain the exceptions, but this seems a bit overkill (mostly, I just want to avoid aborting the whole Python process). --- src/file_compat.h | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/file_compat.h b/src/file_compat.h index 42fdd162288c..84340655bedc 100644 --- a/src/file_compat.h +++ b/src/file_compat.h @@ -126,6 +126,9 @@ static NPY_INLINE FILE *mpl_PyFile_Dup(PyObject *file, char *mode, mpl_off_t *or */ static NPY_INLINE int mpl_PyFile_DupClose(PyObject *file, FILE *handle, mpl_off_t orig_pos) { + PyObject *exc_type = NULL, *exc_value = NULL, *exc_tb = NULL; + PyErr_Fetch(&exc_type, &exc_value, &exc_tb); + int fd; PyObject *ret; mpl_off_t position; @@ -136,25 +139,33 @@ static NPY_INLINE int mpl_PyFile_DupClose(PyObject *file, FILE *handle, mpl_off_ fclose(handle); /* Restore original file handle position, in order to not confuse - Python-side data structures */ + Python-side data structures. Note that this would fail if an exception + is currently set, which can happen as this function is called in cleanup + code, so we need to carefully fetch and restore the exception state. */ fd = PyObject_AsFileDescriptor(file); if (fd == -1) { - return -1; + goto fail; } if (mpl_lseek(fd, orig_pos, SEEK_SET) != -1) { if (position == -1) { PyErr_SetString(PyExc_IOError, "obtaining file position failed"); - return -1; + goto fail; } /* Seek Python-side handle to the FILE* handle position */ ret = PyObject_CallMethod(file, (char *)"seek", (char *)(MPL_OFF_T_PYFMT "i"), position, 0); if (ret == NULL) { - return -1; + goto fail; } Py_DECREF(ret); } + PyErr_Restore(exc_type, exc_value, exc_tb); return 0; +fail: + Py_XDECREF(exc_type); + Py_XDECREF(exc_value); + Py_XDECREF(exc_tb); + return -1; } static NPY_INLINE int mpl_PyFile_Check(PyObject *file) @@ -200,14 +211,23 @@ static NPY_INLINE PyObject *mpl_PyFile_OpenFile(PyObject *filename, const char * static NPY_INLINE int mpl_PyFile_CloseFile(PyObject *file) { + PyObject *type, *value, *tb; + PyErr_Fetch(&type, &value, &tb); + PyObject *ret; ret = PyObject_CallMethod(file, (char *)"close", NULL); if (ret == NULL) { - return -1; + goto fail; } Py_DECREF(ret); + PyErr_Restore(type, value, tb); return 0; +fail: + Py_XDECREF(type); + Py_XDECREF(value); + Py_XDECREF(tb); + return -1; } #ifdef __cplusplus