Как обернуть функцию C++, которая принимает указатель функции в python, используя SWIG

Вот упрощенный пример того, что я хочу сделать. Предположим, у меня есть следующий код С++ в test.h

double f(double x);
double myfun(double (*f)(double x));

На данный момент не имеет большого значения, что делают эти функции. Важно то, что myfun принимает указатель на функцию.

После включения файла test.h в мой файл интерфейса я скомпилировал «тест» модуля Python с помощью SWIG. Теперь в Python я запускаю следующие команды:

import test
f = test.f

Это создает правильно работающую функцию f, которая принимает значение типа double. Однако, когда я пытаюсь передать «f» в myfun внутри python, происходит следующее:

myfun(f)
TypeError: in method 'myfun', argument 1 of type 'double (*)(double)'

Как это исправить? Я полагаю, что мне нужно объявление карты типов в моем файле интерфейса SWIG, но я не уверен, каков правильный синтаксис или где его разместить. Я старался

%typemap double f(double);

но это не сработало. Есть идеи?


person Bluegreen17    schedule 07.04.2014    source источник
comment
можешь показать свой .i тоже? А что вы подразумеваете под правильно работающей функцией f? это функция Python? или указатель функции C, который вы экспортировали в Python через SWIG?   -  person Oliver    schedule 09.04.2014


Ответы (1)


Примечание. В этом ответе есть длинный раздел, посвященный обходным путям. Если вы просто хотите использовать это, перейдите прямо к решению 5.

Эта проблема

Вы столкнулись с тем, что в Python все является объектом. Прежде чем мы рассмотрим исправление вещей, сначала давайте разберемся, что происходит как есть. Я создал полный пример для работы с заголовочным файлом:

double f(double x) {
  return x*x;
}

double myfun(double (*f)(double x)) {
  fprintf(stdout, "%g\n", f(2.0));
  return -1.0;
}

typedef double (*fptr_t)(double);
fptr_t make_fptr() {
  return f;
}

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

При этом первый модуль SWIG может выглядеть так:

%module test

%{
#include "test.h"
%}

%include "test.h"

И мы можем скомпилировать его с помощью:

swig2.0 -Wall -python test.i && gcc -Wall -Wextra -I/usr/include/python2.6 -std=gnu99 -shared -o _test.so test_wrap.c

Итак, теперь мы можем запустить это и спросить Python о типах, которые у нас есть — типе test.f и типе результата вызова test.make_fptr()):

Python 2.6.6 (r266:84292, Dec 27 2010, 00:02:40)
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import test
>>> type(test.f)
<type 'builtin_function_or_method'>
>>> repr(test.f)
'<built-in function f>'
>>> type(test.make_fptr())
<type 'SwigPyObject'>
>>> repr(test.make_fptr())
"<Swig Object of type 'fptr_t' at 0xf7428530>"

Таким образом, проблема в ее нынешнем виде должна стать ясной - нет преобразования встроенных функций в тип SWIG для указателей на функции, поэтому ваш вызов myfun(test.f) не будет работать.

Решение

Вопрос в том, как (и где) это исправить? На самом деле есть по крайней мере четыре возможных решения, которые мы могли бы выбрать, в зависимости от того, сколько других языков вы используете и насколько «Pythonic» вы хотите быть.

Решение 1:

Первое решение тривиально. Мы уже использовали test.make_fptr(), чтобы вернуть нам дескриптор Python для указателя функции для функции f. Таким образом, мы можем вызвать:

f=test.make_fptr()
test.myfun(f)

Лично мне это решение не очень нравится, это не то, что ожидают программисты на Python, и не то, что ожидают программисты на C. Единственное, что идет на это, это простота реализации.

Решение 2:

SWIG предоставляет нам механизм предоставления указателей функций для целевого языка с помощью %constant. (Обычно это используется для предоставления констант времени компиляции, но, по сути, все указатели функций в любом случае действительно находятся в их простейшей форме).

Итак, мы можем изменить наш файл интерфейса SWIG:

%module test

%{
#include "test.h"
%}

%constant double f(double);
%ignore f;

%include "test.h"

Директива %constant говорит SWIG обернуть f как указатель на функцию, а не функцию. %ignore необходим, чтобы избежать предупреждения о просмотре нескольких версий одного и того же идентификатора.

(Примечание: на этом этапе я также удалил функции typedef и make_fptr() из заголовочного файла)

Что теперь позволяет нам запустить:

Python 2.6.6 (r266:84292, Dec 27 2010, 00:02:40)
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import test
>>> type(test.f)
<type 'SwigPyObject'>
>>> repr(test.f)
"<Swig Object of type 'double (*)(double)' at 0xf7397650>"

Отлично - у него есть указатель на функцию. Но с этим есть загвоздка:

>>> test.f(0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'SwigPyObject' object is not callable

Теперь мы не можем вызывать test.f со стороны Python. Что приводит к следующему решению:

Решение 3:

Чтобы исправить это, давайте сначала представим test.f как оба указатель на функцию и встроенную функцию. Мы можем сделать это, просто используя %rename вместо %ignore:

%тест модуля

%{
#include "test.h"
%}

%constant double f(double);
%rename(f_call) f;

%include "test.h"
Python 2.6.6 (r266:84292, Dec 27 2010, 00:02:40)
[GCC 4.4.5] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import test
>>> repr(test.f)
"<Swig Object of type 'double (*)(double)' at 0xf73de650>"
>>> repr(test.f_call)
'<built-in function f_call>'

Это шаг, но мне все еще не нравится идея помнить, должен ли я писать test.f_call или только test.f в зависимости от контекста того, что я хочу сделать с f в данный момент. Мы можем добиться этого, просто написав некоторый код Python в нашем интерфейсе SWIG:

%module test

%{
#include "test.h"
%}

%rename(_f_ptr) f;
%constant double f(double);
%rename(_f_call) f;

%feature("pythonprepend") myfun %{
  args = f.modify(args)
%}

%include "test.h"

%pythoncode %{
class f_wrapper(object):
  def __init__(self, fcall, fptr):
    self.fptr = fptr
    self.fcall = fcall
  def __call__(self,*args):
    return self.fcall(*args)
  def modify(self, t):
    return tuple([x.fptr if isinstance(x,self.__class__) else x for x in t])

f = f_wrapper(_f_call, _f_ptr)
%}

Здесь есть несколько функциональных битов. Во-первых, мы создаем новый, чистый класс Python, чтобы обернуть функцию как вызываемую, так и указатель на функцию. Он содержит в качестве членов настоящий SWIG, обернутый (и переименованный) указатель функции и функцию. Теперь они переименованы и начинаются с подчеркивания в соответствии с соглашением Python. Во-вторых, мы устанавливаем test.f как экземпляр этой оболочки. Когда он вызывается как функция, он передает вызов. Наконец, мы вставляем в оболочку myfun некоторый дополнительный код, чтобы поменять местами реальный указатель на функцию, а не нашу оболочку, стараясь не изменять какие-либо другие аргументы, если таковые имеются.

Это работает, как и ожидалось, например, с:

import test
print "As a callable"
test.f(2.0)
print "As a function pointer"
test.myfun(test.f)

Мы могли бы сделать это немного лучше, например, с помощью макроса SWIG, чтобы избежать повторения %rename, %constant и создания экземпляра оболочки, но мы не можем избежать необходимости использовать %feature("pythonprepend") везде, где мы передаем эти оболочки обратно в SWIG. (Если это возможно сделать прозрачно, это выходит далеко за рамки моих знаний Python).

Решение 4:

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

Однако есть еще один нюанс, помимо необходимости использовать pythonprepend для каждого отдельного использования указателей функций — если вы запустите swig -python -builtin, это просто не сработает, потому что в первую очередь нет кода Python для добавления! (Вам нужно будет изменить конструкцию оболочки на: f = f_wrapper(_test._f_call, _test._f_ptr), но этого будет недостаточно).

Таким образом, мы можем обойти это, написав некоторый Python C API в нашем интерфейсе SWIG:

%module test

%{
#include "test.h"
%}

%{
static __thread PyObject *callback;
static double dispatcher(double d) {
  PyObject *result = PyObject_CallFunctionObjArgs(callback, PyFloat_FromDouble(d), NULL);
  const double ret = PyFloat_AsDouble(result);
  Py_DECREF(result);

  return ret;
}
%}

%typemap(in) double(*)(double) {
  if (!PyCallable_Check($input)) SWIG_fail;
  $1 = dispatcher;
  callback = $input;
}

%include "test.h"

Это немного некрасиво по двум причинам. Во-первых, он использует глобальную переменную (локальную для потока) для хранения вызываемого Python. Это тривиально поправимо для большинства реальных обратных вызовов, где есть аргумент void* пользовательских данных, а также фактические входные данные для обратного вызова. В таких случаях «пользовательские данные» могут быть вызваны Python.

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

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

%module test

%{
#include "test.h"
%}

%{
static __thread PyObject *callback;
static double dispatcher(double d) {
  PyObject *result = PyObject_CallFunctionObjArgs(callback, PyFloat_FromDouble(d), NULL);
  const double ret = PyFloat_AsDouble(result);
  Py_DECREF(result);

  return ret;
}

SWIGINTERN PyObject *_wrap_f(PyObject *self, PyObject *args);

double (*lookup_method(PyObject *m))(double) {
  if (!PyCFunction_Check(m)) return NULL;
  PyCFunctionObject *mo = (PyCFunctionObject*)m;
  if (mo->m_ml->ml_meth == _wrap_f)
    return f;
  return NULL;
}
%}

%typemap(in) double(*)(double) {
  if (!PyCallable_Check($input)) SWIG_fail;
  $1 = lookup_method($input);
  if (!$1) {
    $1 = dispatcher;
    callback = $input;
  }
}

%include "test.h"

Для этого требуется некоторый код указателя на функцию, но теперь это оптимизация, а не требование, и его можно сделать более универсальным с помощью макроса SWIG или двух.

Решение 5:

Я работал над более аккуратным 5-м решением, которое использовало бы %typemap(constcode), чтобы позволить использовать %constant как метод, так и указатель на функцию. Однако оказывается, что в SWIG уже есть поддержка именно этого, что я обнаружил, читая некоторые исходники SWIG. Итак, на самом деле все, что нам нужно сделать, это просто:

%module test

%{
#include "test.h"
%}

%pythoncallback;
double f(double);
%nopythoncallback;

%ignore f;
%include "test.h"

%pythoncallback включает некоторое глобальное состояние, которое заставляет последующие функции быть обернутыми, чтобы их можно было использовать как в качестве указателя функции, так и в качестве функции! %nopythoncallback отключает это.

Который затем работает (с -builtin или без него) с:

import test
test.f(2.0)
test.myfun(test.f)

Который решает практически все проблемы за один раз. Это даже задокументировано в руководстве, хотя там, кажется, не упоминается %pythoncallback. Таким образом, предыдущие четыре решения в основном полезны только как примеры настройки интерфейсов SWIG.

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

person Flexo    schedule 09.04.2014
comment
Вау, это отличный флексо. Мне понадобится немного времени, чтобы впитать все это, но спасибо! - person Bluegreen17; 10.04.2014
comment
@user1589038 user1589038 небольшой момент с лицом: я собирался исправить SWIG, чтобы добавить более аккуратное 5-е решение, оказывается, оно уже существует! - person Flexo; 10.04.2014
comment
Можем ли мы использовать функцию Python в качестве обратного вызова, то есть передать f = lambda x: return x*5 в test.myfun() ? - person kawing-chiu; 26.03.2017
comment
@kawing-chiu У меня есть пример этого здесь: stackoverflow.com/a/11522655/168175 вы можете комбинировать это с ffi так же, как я показал здесь для Java: - person Flexo; 26.03.2017
comment
@Flexo Спасибо! Посмотрю. - person kawing-chiu; 26.03.2017
comment
Возможно, это глупый вопрос, но как получить возвращаемое значение обратного вызова при запуске myfun? В настоящее время я выполняю оболочку python для функции C++ так же, как и в решении 5. Моя функция работает с python, но я не знаю, как прочитать возвращаемое значение обратного вызова, которое я передаю в качестве параметра. Может кто-нибудь объяснить, как это сделать? - person SomeGenericNameto_Display; 13.11.2020
comment
@SomeGenericNameto_Display C это последний аргумент printf в моем примере - person Flexo; 13.11.2020
comment
@Flexo, есть ли другой способ получить результат обратного вызова, кроме printf из myfun? Например. каким-то образом на стороне питона? - person SomeGenericNameto_Display; 14.11.2020
comment
Я имею в виду без printf внутри myfun. - person SomeGenericNameto_Display; 15.11.2020
comment
Или, возможно, создать функцию f на стороне Python, которую можно передать в качестве параметра для test.myfun(pyfunction)? Это возможно? - person SomeGenericNameto_Display; 16.11.2020
comment
Я не совсем понимаю - почему бы вам тогда просто не вызвать test.f в питоне? - person Flexo; 17.11.2020
comment
@Flexo, я пытаюсь с помощью этого образца получить ход выполнения myfun через функцию test.f. У меня это работает, я могу вызвать f из myfun и передать в f текущий прогресс, и это здорово. Но я хочу каким-то образом отслеживать ход выполнения myfun на стороне python. - person SomeGenericNameto_Display; 17.11.2020
comment
Я также пытался создать образец с директорами, которые хранятся на swig github github .com/swig/swig/tree/master/Examples/python/callback, но у меня это не работает, когда создается оболочка, и я пытаюсь создать объект класса Callback, я получаю сообщение об ошибке, что такое имя не определено. Если я пытаюсь создать new_Callback, объект создается, но я не могу вызвать какие-либо методы, потому что у меня есть ошибка, что у объекта нет такого атрибута. Вот почему я пытаюсь подробнее остановиться на примере, где могут быть определены только глобальные функции, - person SomeGenericNameto_Display; 17.11.2020