Как сделать указатель с C -функцией, что замыкание на другую функцию C и параметр функции Python, из Python - PullRequest
4 голосов
/ 26 апреля 2020

Я работаю с интересной загадкой - как создать замыкания C функций, которые ссылаются на данные Python во время выполнения? Это немного длинно, но есть некоторые технически интересные вещи, если вы любите программировать пазлы, так что стоит прочитать.

Допустим, у нас есть:

def f(data): ... implementation...

затем

def make_closure(f, data):
  def g(): return f(d)
  return g

г является закрытием f и d. У нас есть captured параметр d внутри g, и теперь у нас есть функция, которую мы можем вызвать, которая использует d, без необходимости предоставлять d. Это хлебное масло в Python -землях и во всех языках, которые имеют функции как первоклассные типы данных.

Теперь моя проблема (версия 1). Я использую (из Python) библиотеку C, которая принимает обратный вызов:

void register_func(callback_t g) {... }

Библиотека не позволяет мне передавать какие-либо свои собственные данные в этот обратный вызов, и поэтому мне нужно сделать функцию обратного вызова C, которая закрывает свои данные. К счастью, CFUNCTYPE делает что-то волшебное для нас:

from ctypes import CFUNCTYPE

def make_closure(f, data):
  @CFUNCTYPE(...) 
  def g(): return f(d)

  return g

lib = ctypes.CDLL("thirdpartylib", mode=ctypes.RTLD_GLOBAL)
g = make_closure(f, data)   # Python function f, Python data object data
lib.register_func(g) # Call the C function, provide a Python function as the callback.

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

Проблема решена? Не так быстро. Моя актуальная проблема (версия 2) немного сложнее.

С сторонней библиотекой C, имеющей API:

result_t callback(int iterstatus);   // Prototpe of function callback_t

void register_iterator(char* name, callback_t callback);

Библиотека будет выполнить зарегистрированный итератор более или менее следующим образом (реализовано в C):

// Initialize the iterator's state (iterstatus==0)
context = callback(0, NULL)

// Process each result of the iterator (iterstatus==1)
while (true){
  result = callback(1, context);
  if (result) yield_data(result);
}

// Cleanup the iterator's state (iterstatus==2)
// free(context->user_data)
callback(2, context); 

(я знаю - мы не получаем нужные нам API, только те, которые у нас есть).

Итак, я могу зарегистрировать обратный вызов, используя объект CFUNCTYPE, предоставленный в качестве указателя на обратный вызов register_iterator(name, g). Тем не менее, Python является чрезвычайно медленным для выполнения части итерации. На самом деле, мне нужно Python просто для создания данных итерации (массив numpy) и иметь C -программу итерации по ней. Например:

/// Data that callback_closured need in its implementation
/// We be assigned a CFUNCTYPE value and point to a Python function
static py_iterator_allocator_t closure_alloc_func = NULL;

void* callback_closured(int iterstatus, iterstate_t* state){
   switch (iterstatus){
   case 0:
        assert(state==NULL);
        state = closure_alloc_func();   // Ask python to allocate the state
        return state;
    case 1:
        if(state->next==state->end) return NULL;  // no more data
        state->next++;
        return (state->next)-1;
    case 3:                           // Cleanup
        py_decref(state);             
    default:
        return NULL;
    } 
}

void my_register_iterator(char* name, callback_t callback, py_iterator_allocator_t alloc_func){
   closure_alloc_func = alloc_func;
   register_iterator(name, my_register_iterator);
}

... так что в Python мы вызываем:


@CFUNCTYPE(...) 
def alloc_iterator():  
   ...
   return pyobject;

lib.my_register_iterator(name, alloc_iterator)

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

Итак, вопрос:

  • Можно ли динамически создать указатель функции callback_closured для любого распределителя python во время выполнения из Python? :
    // Python-land

    @CFUNCTYPE(...) 
    def alloc_iterator():  
       ...
       return pyobject;

    # lib.iter_impl has the prototype
    # void* iter_impl(int, iterstate_t*, alloc_funct_t )
    iter_impl2 = make_iterator_impl(lib.iter_impl, alloc_func);

    # iter_impl2 references alloc_func, somehow... and has the calling prototype and references 
    #void* (int, iterstate_t* state) is the new C prototype
    register_iterator(name, iter_impl2);

и вызов iter_impl2(int s, void* state) примерно эквивалентно

return iter_impl(s, state, alloc_func)

... динамически создаваемому замыканию, но в C.

Преимущество заключается в том, что распределение может быть реализовано в Python (гибкий behvaior), но итерация реализуется в C (быстрый):

// C-land
void* iter_impl(int iterstatus, iterstate_t* state, alloc_funct_t alloc_func){
   switch (iterstatus){
   case 0:
        assert(state==NULL);
        state = alloc_func();   // Ask python to allocate the state
        return state;
    case 1:
        if(state->next==state->end) return NULL;  // no more data
        state->next++;
        return (state->next)-1;
    case 3:                           // Cleanup
        py_decref(state);             
    default:
        return NULL;
    } 
}

Это не кажется тривиальным. Но так как кажется, что ctypes может генерировать C -зыблируемые указатели функций, которые являются замыканиями Python данных, но это не кажется невозможным. Ctypes, кажется, изобрел новые указатели функций на текстовую память программы. Единственный известный мне способ сделать указатель на функцию в C - это либо: i) объявить и скомпилировать новые функции с новыми именами, либо ii) загрузить совместно используемую библиотеку. Итак, возможно, есть способ согнуть ctypes (или его метод), чтобы сделать это?

Я посмотрел на CFFI. Он предоставил способ компиляции и связывания кода C во время выполнения, но, очевидно, не обеспечивает способ захвата указателя на Python данные, на которые можно ссылаться из этого кода.

...