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

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

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

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

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

import test
f = test.f

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

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

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

%typemap double f(double);

но это не сработало. Любые идеи?

Ответ 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 в указатель функции для funciton 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), но этого будет недостаточно).

Итак, мы можем обойти это, написав несколько API Python C в нашем интерфейсе 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"

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

Вторая проблема немного сложнее решить - потому что вызываемая - это обернутая функция 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).