Указатели и исключения функций Cython - PullRequest
0 голосов
/ 29 декабря 2018

Я пытаюсь обернуть существующую библиотеку C, используя Cython.Библиотека использует обратные вызовы, которые я хотел бы перенаправить для выполнения кода Python.Допустим, соответствующая строка в заголовке выглядит следующим образом:

typedef RETCODE (*FUNC_EVAL)(int a, int b, void* func_data);

, где код возврата используется для сигнализации об ошибке.API для создания соответствующей структуры C выглядит следующим образом:

RETCODE func_create(Func** fstar,
                    FUNC_EVAL func_eval,
                    void* func_data);

Я добавил заголовок / файлы реализации Cython.Заголовок содержит typedef:

  ctypedef RETCODE (*FUNC_EVAL)(int a,
                                int b, 
                                void* func_data)

Реализация содержит функцию-оболочку:

cdef RETCODE func_eval(int a,
                       int b,
                       void* func_data):
  (<object> func_data).func_eval(a, b)
  return OKAY;

Я могу передать эту функцию в оболочку func_create cython просто отлично.

Однако я хочу убедиться, что исключения из кода Python возвращаются в библиотеку C, возвращая значение ERROR в качестве кода возврата.Поэтому я добавил следующее:

cdef RETCODE func_eval(int a,
                       int b,
                       void* func_data) except ERROR:
  (<object> func_data).func_eval(a, b)
  return OKAY;

Однако теперь Cython завершается со следующим сообщением об ошибке:

  Cannot assign type 'RETCODE (*)(int, int, void *) except ERROR' to 'FUNC_EVAL'

Я неправильно использую оператор except ...?

1 Ответ

0 голосов
/ 30 декабря 2018

То есть Cython пытается помешать вам сделать тонкие ошибки.

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

Один разв функции обнаружен сбой, должно произойти следующее:

  • , если эта функция «знает», как обрабатывать эту ошибку (например, «кроме» - предложение), поэтому она должна очистить глобальную ошибкусостояние перед продолжением.
  • если эта функция не «знает», как обрабатывать свою ошибку, она должна прервать и вернуть сигнал сбоя.

Важная вещь: еслифункция не сообщает о произошедшей ошибке, она должна очистить состояние ошибки, в противном случае интерпретатор python находится в незавершенном состоянии, и могут возникнуть незначительные ошибки: например, Cython cdef -функции с except? зависят от правильного состояния ошибки(как работают различные except -заказы Cython, см., например, этот SO-ответ ).

Теперь вернемся к вашей cdef -функции.

  • Если он объявлен без except, Cython заботится о глобальном состоянии: если происходит ошибка, состояние очищается (и предупреждение записывается в стандартную ошибку) перед возвратом функциизначение по умолчанию.
  • Если функция объявлена ​​с except 1, вызывающая функция должна позаботиться об очистке состояния ошибки.

Поэтому вопрос таков:Вызывает ли вызывающий FUNC_EVAL -функционер состояние ошибки Python в случае ошибки?

  • , если да, оберните тип указателя функции как ctypedef... (*FUNC_EVAL)(...) except 1, чтобы было ясно, что Cython сможет обработать ошибку.
  • , если нет (более вероятно)вам нужно будет позаботиться о состоянии ошибки Python в функции cdef.

В случае "Нет" самый простой способ - использовать try: ... except: ... вcdef -функция, т.е.

cdef RETCODE func_eval(int a,
                       int b,
                       void* func_data):
  try:
    (<object> func_data).func_eval(a, b)
  except Exception:
     return ERROR
  return OKAY

Кто-то может быть обеспокоен тем, что использование try... except... увеличит накладные расходы даже для случая, когда не возникает исключение.И это правда.Тем не менее, вы уже вызываете некоторые функции Python, поэтому дополнительные издержки не приведут к снижению производительности.

Мои быстрые эксперименты показали, что вы можете потерять до 30%, если в вызываемом коде вообще нет вычисленийФункциональность Python (см. Эксперименты в приложении к ответу).Но вышеприведенное является крайним случаем, обычно вы теряете намного меньше, поэтому я бы не стал его оптимизировать, если только профилировщик не покажет, что это действительно проблема.

Если вы определите ERROR=0 и 'OKAY= 1 , so you can use the implementation detail, that Cython sets the result to 0`, когда очищает ошибку.Тем не менее, кажется, что это скользкая дорога.


Измерение накладных расходов:

%%cython -a
cdef extern from *:
    """
    typedef int (*FUN)(void);
    void call(FUN f){
       f();
    }
    """
    ctypedef int (*FUN)()
    void call(FUN f)

def dummy():
    pass

cdef int cython_handling():
    dummy()
    return 1

cdef int manual_handling():
    try:
        dummy()
    except Exception:
        return 0
    return 1

def check_cython():
    cdef int i
    for i in range(1000):
        call(cython_handling)

def check_manually():
    cdef int i
    for i in range(1000):
        call(manual_handling)

А теперь:

%timeit check_cython()
# 21.6 µs ± 164 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
%timeit check_manually()
# 27 µs ± 493 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
...