Создание оболочки Cython для функции Python

У меня есть функция C, подпись которой выглядит так:

typedef double (*func_t)(double*, int)
int some_f(func_t myFunc);

Я хотел бы передать функцию Python (не обязательно явно) в качестве аргумента для some_f. К сожалению, я не могу позволить себе изменить объявление some_f, вот и все: я не должен изменять код C.

Одна очевидная вещь, которую я попытался сделать, - это создать базовую функцию обертывания, подобную этой:

cdef double wraping_f(double *d, int i /*?, object f */):
     /*do stuff*/
     return <double>f(d_t)

Однако я не могу придумать способ на самом деле «поместить» его в тело wrapping_f.

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

Взгляните на вторую половину этого ответа: stackoverflow.com/a/34900829/4657412. Сложность заключается в том, что вы хотите, чтобы к указателю функции было прикреплено какое-то состояние (чтобы он знал, какой объект python вызывать), а указатели функций C не могут сохранять состояние. Поэтому я считаю, что это действительно невозможно в стандарте C. Единственный способ, который я нашел для этого, - использовать ctypes или cffi, которым удается сделать это с помощью некоторых (скрытых) нестандартных хаков (генерация кода во время выполнения)

DavidW 26.06.2018 18:45
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
4
1
654
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Этот ответ больше в стиле «сделай сам», и, хотя он не лишен интереса, вам следует обратиться к моему другому ответу для краткого рецепта.


Этот ответ - хитрость и немного чрезмерный, он работает только для Linux64 и, вероятно, не должен быть рекомендован - но я просто не могу удержаться от его публикации.

На самом деле существует четыре версии:

  • насколько легкой могла бы быть жизнь, если бы API учитывал возможность закрытия
  • использование глобального состояния для создания единственного закрытия [также рассматривается вами]
  • использование нескольких глобальных состояний для одновременного создания нескольких замыканий [также рассматривается вами]
  • использование jit-скомпилированных функций для создания произвольного количества замыканий одновременно

Для простоты я выбрал более простую подпись func_t - int (*func_t)(void).

Я знаю, вы не можете изменить API. Тем не менее, я не могу отправиться в болезненное путешествие, не упомянув, насколько это может быть просто ... Существует довольно распространенный трюк с подделкой замыканий с помощью указателей на функции - просто добавьте дополнительный параметр в свой API (обычно void *), то есть:

#version 1: Life could be so easy
# needs Cython >= 0.28 because of verbatim C-code feature
%%cython 
cdef extern from *: #fill some_t with life
    """
    typedef int (*func_t)(void *);
    static int some_f(func_t fun, void *params){
        return fun(params);
    }
    """
    ctypedef int (*func_t)(void *)
    int some_f(func_t myFunc, void *params)

cdef int fun(void *obj):
    print(<object>obj)
    return len(<object>obj)

def doit(s):
    cdef void *params = <void*>s
    print(some_f(&fun, params))

Мы в основном используем void *params для передачи внутреннего состояния закрытия в fun, поэтому результат fun может зависеть от этого состояния.

Поведение такое, как ожидалось:

>>> doit('A')
A
1

Но увы, API такой, какой есть. Мы могли бы использовать глобальный указатель и оболочку для передачи информации:

#version 2: Use global variable for information exchange
# needs Cython >= 0.28 because of verbatim C-code feature
%%cython 
cdef extern from *:
    """
    typedef int (*func_t)();
    static int some_f(func_t fun){
        return fun();
    }
    static void *obj_a=NULL;
    """
    ctypedef int (*func_t)()
    int some_f(func_t myFunc)
    void *obj_a

cdef int fun(void *obj):
    print(<object>obj)
    return len(<object>obj)

cdef int wrap_fun():
    global obj_a
    return fun(obj_a)

cdef func_t create_fun(obj):
    global obj_a
    obj_a=<void *>obj
    return &wrap_fun


def doit(s):
    cdef func_t fun = create_fun(s)
    print(some_f(fun))

С ожидаемым поведением:

>>> doit('A')
A
1

create_fun - это просто удобство, которое устанавливает глобальный объект и возвращает соответствующую оболочку исходной функции fun.

NB: было бы безопаснее сделать obj_a Python-объектом, потому что void * может стать висящим, но чтобы код был ближе к версиям 1 и 4, мы используем void * вместо object.

Но что, если одновременно используется несколько укупорочных средств, скажем, 2? Очевидно, что с подходом, описанным выше, нам нужны 2 глобальных объекта и две функции-оболочки для достижения нашей цели:

#version 3: two function pointers at the same time
%%cython 
cdef extern from *:
    """
    typedef int (*func_t)();
    static int some_f(func_t fun){
        return fun();
    }
    static void *obj_a=NULL;
    static void *obj_b=NULL;
    """
    ctypedef int (*func_t)()
    int some_f(func_t myFunc)
    void *obj_a
    void *obj_b

cdef int fun(void *obj):
    print(<object>obj)
    return len(<object>obj)

cdef int wrap_fun_a():
    global obj_a
    return fun(obj_a)

cdef int wrap_fun_b():
    global obj_b
    return fun(obj_b)

cdef func_t create_fun(obj) except NULL:
    global obj_a, obj_b
    if obj_a == NULL:
        obj_a=<void *>obj
        return &wrap_fun_a
    if obj_b == NULL:
        obj_b=<void *>obj
        return &wrap_fun_b
    raise Exception("Not enough slots")

cdef void delete_fun(func_t fun):
    global obj_a, obj_b
    if fun == &wrap_fun_a:
        obj_a=NULL
    if fun == &wrap_fun_b:
        obj_b=NULL

def doit(s):
    ss = s+s
    cdef func_t fun1 = create_fun(s)
    cdef func_t fun2 = create_fun(ss)
    print(some_f(fun2))
    print(some_f(fun1))
    delete_fun(fun1)
    delete_fun(fun2)

После компиляции, как и ожидалось:

>>> doit('A')
AA
2
A
1    

Но что, если нам нужно предоставить произвольное количество указателей на функции одновременно?

Проблема в том, что нам нужно создать функции-оболочки во время выполнения, потому что нет способа узнать, сколько нам понадобится при компиляции, поэтому единственное, что я могу придумать, это jit-скомпилировать эти функции-оболочки. когда они нужны.

Функция-оболочка здесь на ассемблере выглядит довольно просто:

wrapper_fun:
    movq address_of_params, %rdi      ; void *param is the parameter of fun
    movq address_of_fun, %rax         ; addresse of the function which should be called
    jmp  *%rax                        ;jmp instead of call because it is last operation

Адреса params и fun будут известны во время выполнения, поэтому нам просто нужно связать - заменить заполнитель в полученном машинном коде.

В своей реализации я следую примерно этой замечательной статье: https://eli.thegreenplace.net/2017/adventures-in-jit-compilation-part-4-in-python/

#4. version: jit-compiled wrapper
%%cython   

from libc.string cimport memcpy

cdef extern from *:
    """
    typedef int (*func_t)(void);
    static int some_f(func_t fun){
        return fun();
    }
    """
    ctypedef int (*func_t)()
    int some_f(func_t myFunc)



cdef extern from "sys/mman.h":
       void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, size_t offset);    
       int munmap(void *addr, size_t length);

       int PROT_READ  #  #define PROT_READ  0x1     /* Page can be read.  */
       int PROT_WRITE #  #define PROT_WRITE 0x2     /* Page can be written.  */
       int PROT_EXEC  #  #define PROT_EXEC  0x4     /* Page can be executed.  */

       int MAP_PRIVATE    # #define MAP_PRIVATE  0x02    /* Changes are private.  */
       int MAP_ANONYMOUS  # #define MAP_ANONYMOUS  0x20    /* Don't use a file.  */


#                             |-----8-byte-placeholder ---|
blue_print =      b'\x48\xbf\x00\x00\x00\x00\x00\x00\x00\x00'  # movabs 8-byte-placeholder,%rdi
blue_print+=      b'\x48\xb8\x00\x00\x00\x00\x00\x00\x00\x00'  # movabs 8-byte-placeholder,%rax
blue_print+=      b'\xff\xe0'                                       # jmpq   *%rax ; jump to address in %rax

cdef func_t link(void *obj, void *fun_ptr) except NULL:
    cdef size_t N=len(blue_print)
    cdef char *mem=<char *>mmap(NULL, N, 
                                PROT_READ | PROT_WRITE | PROT_EXEC,
                                MAP_PRIVATE | MAP_ANONYMOUS,
                                -1,0)
    if <long long int>mem==-1:
        raise OSError("failed to allocated mmap")

    #copy blueprint:
    memcpy(mem, <char *>blue_print, N);

    #inject object address:
    memcpy(mem+2, &obj, 8);

    #inject function address:
    memcpy(mem+2+8+2, &fun_ptr, 8);

    return <func_t>(mem)


cdef int fun(void *obj):
    print(<object>obj)
    return len(<object>obj)


cdef func_t create_fun(obj) except NULL:
    return link(<void *>obj, <void *>&fun)

cdef void delete_fun(func_t fun):
    munmap(fun, len(blue_print))

def doit(s):
    ss, sss = s+s, s+s+s
    cdef func_t fun1 = create_fun(s)
    cdef func_t fun2 = create_fun(ss)   
    cdef func_t fun3 = create_fun(sss)  
    print(some_f(fun2))
    print(some_f(fun1))
    print(some_f(fun3))
    delete_fun(fun1)
    delete_fun(fun2)
    delete_fun(fun3)

А теперь ожидаемое поведение:

>>doit('A')
AA
2
A
1
AAA
3  

Посмотрев на это, может быть, есть изменение API, которое можно изменить?

Я думаю, что это полезный ответ, в основном для частей 1 («стандартный» способ передачи «неизвестных данных» с void*) и 4 (приятно видеть, как вы на самом деле подойдете к созданию функции времени выполнения). Я думаю, что для подхода void* должно быть предупреждение о "реф-подсчете" - это не редкость, когда обратный вызов и указатель данных сохраняются на будущее, и вам нужно быть осторожным, чтобы убедиться, что объект выживает до тех пор.

DavidW 27.06.2018 16:51
Ответ принят как подходящий

Я сохраняю свой другой ответ по историческим причинам - он показывает, что нет способа делать то, что вы хотите, без jit-компиляции, и помог мне понять, насколько хорош совет @DavidW в этот ответ.

Для простоты я использую несколько более простую подпись функций и надеюсь, что вы измените ее в соответствии с вашими потребностями.

Вот план закрытия, который позволяет ctypes выполнять jit-компиляцию за кулисами:

%%cython
#needs Cython > 0.28 to run because of verbatim C-code 
cdef extern from *:   #fill some_t with life
    """
    typedef int (*func_t)(int);
    static int some_f(func_t fun){
        return fun(42);
    }
    """
    ctypedef int (*func_t)(int)
    int some_f(func_t myFunc)

#works with any recent Cython version:
import ctypes
cdef class Closure:
    cdef object python_fun
    cdef object jitted_wrapper

    def inner_fun(self, int arg):
        return self.python_fun(arg)

    def __cinit__(self, python_fun):
        self.python_fun=python_fun
        ftype = ctypes.CFUNCTYPE(ctypes.c_int,ctypes.c_int) #define signature
        self.jitted_wrapper=ftype(self.inner_fun)           #jit the wrapper

    cdef func_t get_fun_ptr(self):
        return (<func_t *><size_t>ctypes.addressof(self.jitted_wrapper))[0]

def use_closure(Closure closure):
    print(some_f(closure.get_fun_ptr()))

А теперь пользуемся:

>>> cl1, cl2=Closure(lambda x:2*x), Closure(lambda x:3*x)
>>> use_closure(cl1)
84
>>> use_closure(cl2)
126

Другие вопросы по теме