谁应该打电话给PyErr_Fetch?

Who should call PyErr_Fetch?

C API 中用于 Python 的许多函数在可能设置错误指示符的情况下使用不安全。特别是,PyFloat_AsDouble 和类似的函数 不明确 因为它们没有 return 值保留用于指示错误:如果它们成功(但碰巧 return 用于错误的值),如果错误指示器已经设置,则调用 PyErr_Occurred 的客户端将认为它们失败了。 (请注意,PyIter_Next 或多或少会保证发生这种情况。)更一般地说,任何可能失败的函数都会覆盖错误指示器,如果失败的话,这可能是可取的,也可能不是可取的。

不幸的是,使用错误指示符集调用此类函数的可能性一点也不小:对错误的常见反应是 Py_DECREF 局部变量,并且(除非所有对象的类型可能被(间接)释放是已知的)可以执行任意代码。 (这是一个很好的例子,说明清理代码可能会失败。)解释器在此类析构函数 中捕获异常 raised,但它不会阻止异常从泄漏 他们.

任一端,我们可以使用PyErr_FetchPyErr_Restore来防止这些问题。围绕一个模糊函数的调用,它们允许可靠​​地确定它是否成功;放在 Py_DECREF 附近,它们首先防止在执行任何易受影响的代码期间设置错误指示器。 (它们甚至可以在可能失败的直接调用的清理代码周围使用,以便允许选择传播哪个异常。在这种情况下,将它放在哪里是毫无疑问的:清理代码无论如何不能在多个异常之间进行选择.)

无论选择哪种放置方式,都会显着增加代码的复杂性和执行时间:有很多调用不明确的函数,错误处理路径上有很多 Py_DECREF。虽然防御性编程的原则建议在两个 地方使用它,但更好的代码将来自(仔细编程)通用 约定(以覆盖正在执行的任意代码)。

C 本身有这样一个约定:errno 必须由任意代码的调用者保存,即使(如 Python 析构函数中的抑制异常)该代码不应设置 errno 到任何东西。主要原因是它可以被许多 成功的 库调用重置(但永远不会为 0)(让它们在内部处理错误),进一步缩小可以安全执行的操作集 errno 具有一定的重要价值。 (这也防止了当 PyErr_Occurred 报告预先存在的错误时出现的问题:C 程序员必须在调用不明确的函数之前将 errno 设置为 0。)另一个原因是“调用一些任意代码而没有错误报告”在大多数 C 程序中不是常见操作,因此为了它而给其他代码增加负担是没有意义的。

是否有这样的约定(即使 CPython 本身存在不遵循它的错误代码)?如果做不到这一点,是否有技术原因来指导选择一个建立?或者这可能是一个工程问题,基于对“任意”的过于直白的解读:CPython 是否应该在处理析构函数异常时保存和恢复错误指示器本身?

如果您的清理只是一堆 Py_DECREF,则不需要调用 PyErr_FetchPy_DECREF 旨在安全调用异常集。如果 Py_DECREF 中的代码需要做一些对异常集不安全的事情,它将负责保存和恢复异常状态。 (如果您的清理涉及的不仅仅是 Py_DECREF,您可能需要自己处理事情。)

例如,tp_finalize,最有可能调用任意Python代码的对象销毁步骤之一,is explicitly responsible for saving and restoring an active exception

tp_finalize should not mutate the current exception status; therefore, a recommended way to write a non-trivial finalizer is:

static void
local_finalize(PyObject *self)
{
    PyObject *error_type, *error_value, *error_traceback;

    /* Save the current exception, if any. */
    PyErr_Fetch(&error_type, &error_value, &error_traceback);

    /* ... */

    /* Restore the saved exception. */
    PyErr_Restore(error_type, error_value, error_traceback);
}

对于Python中写的__del__方法,可以在slot_tp_finalize中查看相关处理:

/* Save the current exception, if any. */
PyErr_Fetch(&error_type, &error_value, &error_traceback);

/* Execute __del__ method, if any. */
del = lookup_maybe_method(self, &PyId___del__, &unbound);
if (del != NULL) {
    res = call_unbound_noarg(unbound, del, self);
    if (res == NULL)
        PyErr_WriteUnraisable(del);
    else
        Py_DECREF(res);
    Py_DECREF(del);
}

/* Restore the saved exception. */
PyErr_Restore(error_type, error_value, error_traceback);

弱引用系统还takes responsibility用于在调用弱引用回调之前保存异常状态:

if (*list != NULL) {
    PyWeakReference *current = *list;
    Py_ssize_t count = _PyWeakref_GetWeakrefCount(current);
    PyObject *err_type, *err_value, *err_tb;

    PyErr_Fetch(&err_type, &err_value, &err_tb);
    if (count == 1) {
        PyObject *callback = current->wr_callback;

        current->wr_callback = NULL;
        clear_weakref(current);
        if (callback != NULL) {
            if (((PyObject *)current)->ob_refcnt > 0)
                handle_callback(current, callback);
            Py_DECREF(callback);
        }
    }
    else {
        ...

所以在设置异常时调用 Py_DECREF 是可怕的,你考虑它是件好事,但只要对象销毁代码运行正常,它应该没问题。


那么,如果除了清除引用之外,您还需要做更多的清理工作怎么办?在那种情况下,如果您的清理对于异常集不安全,您应该在完成时调用 PyErr_FetchPyErr_Restore 异常状态。如果在清理时出现另一个异常,您可以将它链接起来( at C level), or dump a short warning to stderr with PyErr_WriteUnraisable 然后通过 PyErr_Clear-ing 或 PyErr_Restore-ing 原始异常来抑制新异常陈述它。