วิธีล้อมฟังก์ชัน c++ ซึ่งรับตัวชี้ฟังก์ชันใน python โดยใช้ SWIG

นี่คือตัวอย่างง่ายๆ ของสิ่งที่ฉันต้องการทำ สมมติว่าฉันมีรหัส c++ ต่อไปนี้ใน 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);

แต่นั่นไม่ได้ผล มีความคิดอะไรบ้าง?


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 ไปยังตัวชี้ฟังก์ชันสำหรับ 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 ให้เป็นอินสแตนซ์ของ wrapper นี้ เมื่อมันถูกเรียกว่าเป็นฟังก์ชัน มันจะส่งผ่านการโทร ในที่สุดเราก็ใส่โค้ดพิเศษลงใน wrapper myfun เพื่อสลับในตัวชี้ฟังก์ชันจริงแทนที่จะเป็น wrapper ของเรา โดยระวังอย่าเปลี่ยนอาร์กิวเมนต์อื่น ๆ หากมี

สิ่งนี้ทำงานได้ตามที่คาดไว้ ตัวอย่างเช่น:

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

เราอาจทำให้สิ่งนี้ดีขึ้นเล็กน้อย เช่น ด้วยมาโคร SWIG เพื่อหลีกเลี่ยงการสร้างอินสแตนซ์ %rename, %constant และ Wrapper ซ้ำ แต่เราไม่สามารถหลีกหนีจากความต้องการใช้ %feature("pythonprepend") ในทุกที่ที่เราส่ง Wrapper เหล่านี้กลับไปยัง SWIG (หากเป็นไปได้ที่จะทำสิ่งนั้นอย่างโปร่งใส แสดงว่าเกินความรู้ Python ของฉัน)

โซลูชันที่ 4:

วิธีแก้ปัญหาก่อนหน้านี้ค่อนข้างเรียบร้อยกว่า มันทำงานโปร่งใสตามที่คุณคาดหวัง (ในฐานะทั้งผู้ใช้ C และ Python) และกลไกของมันถูกห่อหุ้มโดยไม่มีอะไรนอกจาก Python ที่นำไปใช้งาน

ยังมี gotcha นอกเหนือจากความจำเป็นในการใช้ pythonprepend สำหรับการใช้งานทุกครั้งของตัวชี้ฟังก์ชัน - หากคุณเรียกใช้ swig -python -builtin มันก็จะไม่ทำงานเพราะไม่มีโค้ด Python ที่ต้องเติมไว้ตั้งแต่แรก! (คุณจะต้องเปลี่ยนโครงสร้างของ wrapper ให้เป็น: 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* รวมถึงอินพุตจริงของการโทรกลับ "userdata" สามารถเรียก Python ได้ในกรณีเหล่านั้น

ปัญหาที่สองนั้นยากกว่าเล็กน้อยในการแก้ไข - เนื่องจาก callable นั้นเป็นฟังก์ชัน C ที่ห่อหุ้มไว้ ลำดับการโทรตอนนี้จึงเกี่ยวข้องกับการห่อทุกอย่างให้เป็นประเภท Python และการเดินทางขึ้นและกลับจากล่าม Python เพียงเพื่อทำสิ่งที่ควรเป็นเรื่องเล็กน้อย นั่นเป็นค่าใช้จ่ายค่อนข้างน้อย

เราสามารถทำงานย้อนกลับจาก PyObject ที่กำหนดได้ และลองหาว่าฟังก์ชันใด (ถ้ามี) เป็น wrapper สำหรับ:

%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 ในแผนผังพิมพ์ของคุณ จากนั้น iff นั้นล้มเหลวในการเลือกวิธี PyCallable แทน)

person Flexo    schedule 09.04.2014
comment
ว้าว นี่เป็นเฟล็กโซที่ยอดเยี่ยม ฉันต้องใช้เวลาสักหน่อยในการซึมซับทั้งหมดนี้แต่ก็ขอบคุณมาก! - person Bluegreen17; 10.04.2014
comment
@ 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 ที่นี่: stackoverflow.com/a/40273233/168175 - person Flexo; 26.03.2017
comment
@Flexo ขอบคุณ! จะได้ดู. - person kawing-chiu; 26.03.2017
comment
บางทีนี่อาจเป็นคำถามโง่ ๆ แต่จะรับค่าส่งคืนของการโทรกลับได้อย่างไรเมื่อ myfun กำลังทำงานอยู่ ขณะนี้ฉันกำลังใช้ python wrapper สำหรับฟังก์ชัน 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 บนฝั่งหลามซึ่งสามารถส่งผ่านเป็นพารามิเตอร์ไปยัง test.myfun(pyfunction) ได้? เป็นไปได้ไหม? - person SomeGenericNameto_Display; 16.11.2020
comment
ฉันไม่ค่อยติดตาม - ทำไมคุณไม่เรียก test.f ใน python ล่ะ? - 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 แต่มันใช้งานไม่ได้สำหรับฉัน เมื่อ wrapper สร้างขึ้นมาและฉันพยายามสร้างวัตถุของ class Callback ฉันได้รับข้อผิดพลาดดังกล่าว ไม่ได้กำหนดชื่อ หากฉันพยายามสร้าง new_Callback วัตถุกำลังสร้าง แต่ฉันไม่สามารถเรียกวิธีการใด ๆ ได้ เนื่องจากฉันมีข้อผิดพลาดว่าวัตถุไม่มีแอตทริบิวต์ดังกล่าว นั่นเป็นเหตุผลที่ฉันพยายามอธิบายรายละเอียดเพิ่มเติมด้วยตัวอย่างที่สามารถกำหนดฟังก์ชันโกลบอลได้เท่านั้น - person SomeGenericNameto_Display; 17.11.2020