Кто должен называть PyErr_Fetch? - PullRequest
0 голосов
/ 23 сентября 2018

Многие функции в C API для Python небезопасны для использования, если может быть установлен индикатор ошибки.В частности, PyFloat_AsDouble и подобные функции неоднозначны в том смысле, что у них нет возвращаемого значения, зарезервированного для указания ошибки: если они успешны (но возвращают значение, использованное для ошибок), клиент, который вызывает PyErr_Occurred будет полагать, что они потерпели неудачу, если индикатор ошибки был просто уже установлен.(Обратите внимание, что это более или менее гарантированно произойдет с PyIter_Next.) В более общем случае любая функция, которая может дать сбой, перезаписывает индикатор ошибки, если это происходит, что может или не может быть желательным.

К сожалению,Возможность вызова таких функций с установленным индикатором ошибки весьма вероятна: обычная реакция на ошибку - Py_DECREF локальные переменные, и (если не известны типы всех объектов, которые могут быть (косвенно) освобождены ею)который может выполнить произвольный код.(Это хороший пример опасности наличия кода очистки с возможностью сбоя.) Интерпретатор перехватывает исключения , повышенные в таких деструкторах, но не предотвращает утечку исключений в их.

На любом конце мы можем использовать PyErr_Fetch и PyErr_Restore для предотвращения этих проблем.Обращаясь к вызову неоднозначной функции, они позволяют надежно определить, удалось ли это;при значении Py_DECREF они предотвращают установку индикатора ошибки во время выполнения любого восприимчивого кода в первую очередь.(Их также можно использовать даже вокруг кода очистки, вызываемого напрямую, который может дать сбой, чтобы можно было выбирать, какое исключение распространять. В этом случае не возникает вопросов: в любом случае код очистки не может выбирать между несколькими исключениями.)

Любой выбор размещения значительно увеличивает сложность кода и время выполнения: много вызовов неоднозначных функций и много Py_DECREF s на путях обработки ошибок.Хотя принцип защитного программирования предполагает использование его в обоих местах, гораздо более приятный код будет получен в результате (осторожного программирования) универсального соглашения (для покрытия выполняемого произвольного кода).

Сам C имеет такое соглашение: errno должно быть сохранено вызывающей стороной произвольного кода, даже если (как и подавленные исключения в деструкторах Python) этот код не должен устанавливать errno для чего-либо.Основная причина в том, что он может быть сброшен (но никогда не равен 0) многими успешными библиотечными вызовами (чтобы они могли обрабатывать ошибки внутри), еще больше сужая набор операций, которые можно безопасно выполнять, в то время как errno содержит некоторыезначительная ценность.(Это также предотвращает проблему, которая возникает, когда PyErr_Occurred сообщает о ранее существовавшей ошибке: программисты на Си должны установить errno в 0, прежде чем вызывать неоднозначную функцию.) Другая причина заключается в том, что «вызов произвольного кода без сообщений об ошибках» не являетсяобычная операция в большинстве программ на C, поэтому обременять другой код ради нее бессмысленно.

Существует ли такое соглашение (даже если есть ошибочный код, который не следует ему в самом CPython)?В противном случае, есть ли техническая причина для выбора одного из них?Или, может быть, это инженерная проблема, основанная на слишком буквальном чтении «произвольного»: должен ли CPython сохранять и восстанавливать сам индикатор ошибки, пока он обрабатывает исключения деструктора?

1 Ответ

0 голосов
/ 23 сентября 2018

Если ваша очистка - всего лишь куча Py_DECREF, вам не нужно звонить PyErr_Fetch.Py_DECREF предназначен для безопасного вызова с набором исключений.Если код внутри Py_DECREF должен делать что-то, что небезопасно делать с набором исключений, он будет нести ответственность за сохранение и восстановление состояния исключения.(Если ваша очистка включает в себя больше, чем просто Py_DECREF, вам, возможно, придется самостоятельно что-то делать.)

Например, tp_finalize, один из этапов уничтожения объекта, наиболее вероятный для вызова произвольного кода Python, несет полную ответственность за сохранение и восстановление активного исключения :

tp_finalize не должно изменять текущее состояние исключения;поэтому, рекомендуемый способ написать нетривиальный финализатор:

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);
}

Для __del__ методов, написанных на Python, вы можете увидеть соответствующую обработку в 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);

Система слабых ссылок также берет на себя ответственность за сохранение состояния исключения перед вызовом обратных вызовов со слабыми ссылками:

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_Fetch и PyErr_Restore состояние исключения, когда вы закончите.Если что-то вызывает другое исключение во время очистки, вы можете либо связать его в цепочку ( неловко, но возможно на уровне C), либо вывести короткое предупреждение в stderr с помощью PyErr_WriteUnraisable изатем подавите новое исключение, PyErr_Clear -ing его или PyErr_Restore -ing исходное состояние исключения поверх него.

...