Cara membungkus fungsi c++ yang menggunakan penunjuk fungsi dengan python menggunakan SWIG

Berikut adalah contoh sederhana dari apa yang ingin saya lakukan. Misalkan saya memiliki kode c++ berikut di test.h

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

Saat ini, tidak terlalu penting apa fungsi fungsi-fungsi ini. Yang penting adalah myfun menggunakan penunjuk fungsi.

Setelah memasukkan file test.h ke dalam file antarmuka saya, saya mengkompilasi "tes" modul python menggunakan SWIG. Sekarang, dengan Python, saya menjalankan perintah berikut:

import test
f = test.f

Ini menciptakan fungsi f yang berfungsi dengan baik, yang mengambil ganda. Namun, ketika saya mencoba meneruskan "f" ke myfun di dalam python, inilah yang terjadi:

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

Bagaimana cara memperbaikinya? Saya pikir saya memerlukan deklarasi peta ketik di file antarmuka SWIG saya, tapi saya tidak yakin apa sintaks yang tepat atau di mana meletakkannya. Saya mencoba

%typemap double f(double);

tapi itu tidak berhasil. Ada ide?


person Bluegreen17    schedule 07.04.2014    source sumber
comment
bisakah kamu menunjukkan .i kamu juga? Dan apa yang Anda maksud dengan fungsi f yang berfungsi dengan baik? apakah ini fungsi Python? atau penunjuk fungsi C yang telah Anda ekspor ke Python melalui SWIG?   -  person Oliver    schedule 09.04.2014


Jawaban (1)


Catatan: jawaban ini memiliki bagian panjang tentang solusinya. Jika Anda hanya ingin menggunakan ini, langsung saja ke solusi 5.

Masalah

Anda telah menemukan fakta bahwa dalam Python semuanya adalah sebuah objek. Sebelum kita melihat cara memperbaiki sesuatu, pertama-tama mari kita pahami apa yang sebenarnya terjadi. Saya telah membuat contoh lengkap untuk digunakan, dengan file header:

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;
}

Perubahan utama yang saya buat sejauh ini adalah menambahkan definisi pada deklarasi Anda sehingga saya bisa mengujinya dan fungsi make_fptr() yang mengembalikan sesuatu ke Python yang kita tahu akan dibungkus sebagai penunjuk fungsi.

Dengan ini modul SWIG pertama mungkin terlihat seperti:

%module test

%{
#include "test.h"
%}

%include "test.h"

Dan kita dapat mengkompilasinya dengan:

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

Jadi sekarang kita dapat menjalankan ini dan menanyakan Python tentang tipe yang kita miliki - tipe test.f dan tipe hasil pemanggilan 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>"

Jadi masalahnya seharusnya menjadi jelas - tidak ada konversi dari fungsi bawaan ke tipe SWIG untuk penunjuk fungsi, jadi panggilan Anda ke myfun(test.f) tidak akan berfungsi.

Solusinya

Pertanyaannya kemudian adalah bagaimana (dan di mana) kita memperbaikinya? Faktanya, setidaknya ada empat kemungkinan solusi yang dapat kami pilih, bergantung pada berapa banyak bahasa lain yang Anda targetkan dan seberapa "Pythonic" yang Anda inginkan.

Solusi 1:

Solusi pertama adalah hal yang sepele. Kami telah menggunakan test.make_fptr() untuk mengembalikan pegangan Python ke penunjuk fungsi untuk fungsi f. Jadi kita sebenarnya bisa menelepon:

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

Secara pribadi saya tidak terlalu menyukai solusi ini, ini bukan yang diharapkan oleh pemrogram Python dan bukan pula yang diharapkan oleh pemrogram C. Satu-satunya hal yang berhasil adalah kesederhanaan implementasinya.

Solusi 2:

SWIG memberi kita mekanisme untuk mengekspos penunjuk fungsi ke bahasa target, menggunakan %constant. (Biasanya ini digunakan untuk mengekspos konstanta waktu kompilasi, tapi pada dasarnya semua penunjuk fungsi benar-benar dalam bentuk yang paling sederhana).

Jadi kita dapat memodifikasi file antarmuka SWIG kita:

%module test

%{
#include "test.h"
%}

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

%include "test.h"

Direktif %constant memberitahu SWIG untuk membungkus f sebagai penunjuk fungsi, bukan fungsi. %ignore diperlukan untuk menghindari peringatan tentang melihat beberapa versi pengenal yang sama.

(Catatan: Saya juga menghapus fungsi typedef dan make_fptr() dari file header saat ini)

Yang sekarang memungkinkan kita menjalankan:

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>"

Hebat - ada penunjuk fungsinya. Tapi ada kendala dalam hal ini:

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

Sekarang kita tidak bisa memanggil test.f dari sisi Python. Yang mengarah ke solusi berikutnya:

Solusi 3:

Untuk memperbaikinya, pertama-tama mari kita ekspos test.f sebagai keduanya penunjuk fungsi dan fungsi bawaan. Kita dapat melakukannya hanya dengan menggunakan %rename, bukan %ignore:

%uji modul

%{
#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>'

Itu sebuah langkah, tapi saya masih tidak menyukai gagasan harus mengingat apakah saya harus menulis test.f_call atau hanya test.f tergantung pada konteks apa yang ingin saya lakukan dengan f pada saat itu. Kita dapat mencapainya hanya dengan menulis beberapa kode Python di antarmuka SWIG kita:

%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)
%}

Ada beberapa bit fungsional di sini. Pertama kita membuat kelas Python murni baru untuk menggabungkan suatu fungsi sebagai callable dan penunjuk fungsi. Ini memegang sebagai anggota penunjuk dan fungsi fungsi SWIG asli yang dibungkus (dan diganti namanya). Ini sekarang diganti namanya untuk memulai dengan garis bawah sebagai konvensi Python. Kedua, kami menetapkan test.f menjadi turunan dari pembungkus ini. Ketika dipanggil sebagai fungsi, panggilan tersebut akan diteruskan. Akhirnya kita memasukkan beberapa kode tambahan ke dalam pembungkus myfun untuk menukar penunjuk fungsi sebenarnya daripada pembungkus kita, berhati-hatilah agar tidak mengubah argumen lain jika ada.

Ini berfungsi seperti yang diharapkan, misalnya dengan:

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

Kita bisa membuat ini sedikit lebih bagus, misalnya dengan makro SWIG untuk menghindari pengulangan pembuatan instance %rename, %constant dan wrapper, namun kita tidak bisa lepas dari kebutuhan untuk menggunakan %feature("pythonprepend") di mana pun kita meneruskan wrapper ini kembali ke SWIG. (Jika memungkinkan untuk melakukan itu secara transparan, itu di luar pengetahuan Python saya).

Solusi 4:

Solusi sebelumnya agak lebih rapi, ia bekerja secara transparan seperti yang Anda harapkan (baik sebagai pengguna C dan Python) dan mekanismenya dienkapsulasi hanya dengan implementasi Python.

Namun masih ada masalah, selain kebutuhan untuk menggunakan pythonprepend untuk setiap penggunaan pointer fungsi - jika Anda menjalankan swig -python -builtin itu tidak akan berfungsi, karena tidak ada kode Python untuk ditambahkan di tempat pertama! (Anda perlu mengubah konstruksi pembungkusnya menjadi: f = f_wrapper(_test._f_call, _test._f_ptr), tetapi itu tidak cukup).

Jadi kita bisa menyiasatinya dengan menulis beberapa API Python C di antarmuka SWIG kita:

%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"

Ini agak jelek karena dua alasan. Pertama, ia menggunakan variabel global (thread lokal) untuk menyimpan callable Python. Hal ini dapat diperbaiki dengan mudah untuk sebagian besar panggilan balik dunia nyata, di mana terdapat argumen data pengguna void* serta masukan aktual ke panggilan balik tersebut. "Data pengguna" dapat berupa callable Python dalam kasus tersebut.

Masalah kedua sedikit lebih rumit untuk dipecahkan - karena callable adalah fungsi C yang dibungkus, urutan panggilan sekarang melibatkan membungkus semuanya sebagai tipe Python dan perjalanan bolak-balik dari interpreter Python hanya untuk melakukan sesuatu yang seharusnya sepele. Itu biaya overhead yang cukup besar.

Kita dapat bekerja mundur dari PyObject tertentu dan mencoba mencari tahu fungsi mana (jika ada) yang menjadi pembungkusnya:

%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"

Ini memang memerlukan beberapa kode penunjuk per fungsi, tetapi sekarang ini merupakan pengoptimalan daripada persyaratan dan dapat dibuat lebih umum dengan satu atau dua makro SWIG.

Solusi 5:

Saya sedang mengerjakan solusi ke-5 yang lebih rapi yang akan menggunakan %typemap(constcode) untuk memungkinkan %constant digunakan sebagai metode dan penunjuk fungsi. Ternyata di SWIG sudah ada dukungan untuk melakukan hal tersebut, yang saya temukan saat membaca beberapa sumber SWIG. Jadi sebenarnya yang perlu kita lakukan hanyalah:

%module test

%{
#include "test.h"
%}

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

%ignore f;
%include "test.h"

%pythoncallback mengaktifkan beberapa keadaan global yang menyebabkan fungsi-fungsi selanjutnya dibungkus agar dapat digunakan baik sebagai penunjuk fungsi maupun fungsi! %nopythoncallback menonaktifkannya.

Yang kemudian berfungsi (dengan atau tanpa -builtin) dengan:

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

Yang menyelesaikan hampir semua masalah sekaligus. Hal ini bahkan didokumentasikan dalam manual juga, meskipun tampaknya tidak disebutkan sama sekali %pythoncallback. Jadi empat solusi sebelumnya sebagian besar hanya berguna sebagai contoh penyesuaian antarmuka SWIG.

Masih ada satu kasus di mana solusi 4 akan berguna - jika Anda ingin memadupadankan callback yang diimplementasikan C dan Python, Anda perlu mengimplementasikan gabungan keduanya. (Idealnya Anda akan mencoba dan melakukan konversi tipe penunjuk fungsi SWIG di peta ketik Anda dan kemudian melakukan fallback yang gagal ke metode PyCallable sebagai gantinya).

person Flexo    schedule 09.04.2014
comment
Wow, ini Flexo yang hebat. Saya perlu sedikit waktu untuk menyerap semua ini, tetapi terima kasih! - person Bluegreen17; 10.04.2014
comment
@ user1589038 sedikit momen facepalm: Saya akan menambal SWIG untuk menambahkan solusi ke-5 yang lebih rapi, ternyata sudah ada! - person Flexo; 10.04.2014
comment
Bisakah kita menggunakan fungsi Python sebagai panggilan balik, yaitu meneruskan f = lambda x: return x*5 ke test.myfun() ? - person kawing-chiu; 26.03.2017
comment
@kawing-chiu Saya punya contohnya di sini: stackoverflow.com/a/11522655/168175 Anda dapat menggabungkan dengan ffi dengan cara yang sama seperti yang saya tunjukkan untuk Java di sini: stackoverflow.com/a/40273233/168175 - person Flexo; 26.03.2017
comment
@Flexo Terima kasih! Akan melihatnya. - person kawing-chiu; 26.03.2017
comment
Mungkin ini pertanyaan bodoh, tapi bagaimana cara mendapatkan nilai kembalian callback saat myfun sedang berjalan? Saat ini saya sedang menjalankan fungsi pembungkus python untuk C++ dengan cara yang sama seperti pada solusi 5. Fungsi saya berjalan dari python, tetapi saya tidak tahu cara membaca nilai kembalian panggilan balik yang saya teruskan sebagai parameter. Adakah yang bisa menjelaskan cara melakukan ini? - person SomeGenericNameto_Display; 13.11.2020
comment
@SomeGenericNameto_Display C ini argumen terakhir printf dalam contoh saya - person Flexo; 13.11.2020
comment
@Flexo, Apakah ada cara lain untuk mendapatkan hasil panggilan balik kecuali printf dari myfun? Misalnya. entah bagaimana di sisi python? - person SomeGenericNameto_Display; 14.11.2020
comment
Maksudku tanpa printf di dalam myfun . - person SomeGenericNameto_Display; 15.11.2020
comment
Atau, mungkin, membuat fungsi f di sisi python yang dapat diteruskan sebagai parameter ke test.myfun(pyfunction)? Apakah ini mungkin? - person SomeGenericNameto_Display; 16.11.2020
comment
Saya kurang paham - mengapa Anda tidak memanggil test.f dengan python saja? - person Flexo; 17.11.2020
comment
@Flexo, saya mencoba dengan sampel ini untuk mendapatkan kemajuan dalam melakukan fungsi myfun melalui test.f. Ini berhasil untuk saya, saya dapat menelepon f dari kesenangan saya dan meneruskan kemajuan saat ini ke f dan itu bagus. Tapi saya ingin cara untuk melacak kemajuan kesenangan saya di sisi python. - person SomeGenericNameto_Display; 17.11.2020
comment
Saya juga mencoba membuat sampel dengan direktur yang disimpan di swig github github .com/swig/swig/tree/master/Examples/python/callback tetapi tidak berfungsi untuk saya, ketika pembungkus dibuat dan saya mencoba membuat objek kelas Callback saya mendapatkan kesalahan seperti itu nama tidak ditentukan. Jika saya mencoba membuat new_Callback objek tersebut dibuat tetapi saya tidak dapat memanggil metode apa pun karena saya mendapat kesalahan bahwa objek tidak memiliki atribut seperti itu. Itu sebabnya saya mencoba menguraikan lebih lanjut dengan contoh di mana fungsi global hanya dapat didefinisikan, - person SomeGenericNameto_Display; 17.11.2020