使用 python-c-api 调用的 Cython 回调段错误

Posted

技术标签:

【中文标题】使用 python-c-api 调用的 Cython 回调段错误【英文标题】:Cython callback segfaults using python-c-api calls 【发布时间】:2016-07-29 07:04:10 【问题描述】:

为了控制 eGige 相机,拥有一个 C 库,我创建了一个 cython 代码 project,其想法是让每种语言都拥有最好的东西。

该库提供了一种方法来监听来自摄像头的心跳以了解它是否已断开连接。我已经在 C++ 类中进行了回调,但是从这个 C++ 类调用一个类的 python 方法在我尝试过的所有方式中都陷入了分段错误。 p>

我已经将它封装在一个特定的 C++ 类中:

#include <Python.h>
/* (...) */
PyCallback::PyCallback(PyObject* self, const char* methodName)

  Py_XINCREF(self);
  _self = self;
  _method = PyObject_GetAttrString(self, methodName);

PyCallback::~PyCallback()

  Py_XDECREF(_self);

void PyCallback::execute()

  try
  
    PyObject *args = PyTuple_Pack(1,_self);
    PyObject_CallFunctionObjArgs(_method, args);
  catch(...)
    _error("Exception calling python");
  

来自 cython 对象的代码是:

cdef class Camera(...):
    # (...)
    cdef registerRemovalCallback(self):
        cdef:
            PyCallback* obj
        obj = new PyCallback(<PyObject*> self, <char*> "cameraRemovalCallback")
    cdef cameraRemovalCallback(self):
        self._isPresent = False

回溯的最低部分只是在尝试准备参数时。

#0  0x00007ffff7b24592 in PyErr_Restore () from /usr/lib64/libpython2.6.so.1.0
#1  0x00007ffff7b23fef in PyErr_SetString () from /usr/lib64/libpython2.6.so.1.0
#2  0x00007ffff7b314dd in ?? () from /usr/lib64/libpython2.6.so.1.0
#3  0x00007ffff7b313ca in ?? () from /usr/lib64/libpython2.6.so.1.0
#4  0x00007ffff7b316c1 in ?? () from /usr/lib64/libpython2.6.so.1.0
#5  0x00007ffff7b31d2f in ?? () from /usr/lib64/libpython2.6.so.1.0
#6  0x00007ffff7b31e9c in Py_BuildValue () from /usr/lib64/libpython2.6.so.1.0
#7  0x00007ffff637cbf8 in PyCallback::execute (this=0x16212a0) at pylon/PyCallback.cpp:53
#8  0x00007ffff6376248 in CppCamera::removalCallback (this=0x161fb30, pDevice=<value optimized out>) at pylon/Camera.cpp:387

我尝试使用 _Py_BuildValue("(self)", self); 进行参数设置,但后来出现了 segfault

我也尝试在参数字段中使用 PyObject_CallFunctionObjArgsNULL,认为可能指向“self”的指针已经嵌入因为该方法指向此对象中的特定地址。但是他们我有 segfault 那里。

有人看到我的错误吗?那里的东西应该以不同的方式制造?我希望这是我对谁来做这件事的误解。

更新@2016/08/01

根据cmets的指示,在代码中做了两处修改:

首先,指向 PyCallback 的指针存储已被存储为 Camera cython 类的成员:

cdef class Camera(...):
    cdef:
        #(...)
        PyCallback* _cbObj
    # (...)
    cdef registerRemovalCallback(self):
        self._cbObj = new PyCallback(<PyObject*> self, <char*> "cameraRemovalCallback")
    cdef cameraRemovalCallback(self):
        self._isPresent = False

即使这是 segfaults 的基本来源,它看起来也没有涉及当前的问题。

然后是c++中的PyCallback::execute(),我做了一些改动。在阅读了GIL(全局解释器锁)并为其添加了一些调用之后,我添加了一个可能会指导解决方案的检查:

PyCallback::PyCallback(PyObject* self, const char* methodName)

  Py_Initialize();
  Py_XINCREF(self);
  _self = self;
  _method = PyObject_GetAttrString(self, methodName);


PyCallback::~PyCallback()

  Py_XDECREF(_self);
  Py_Finalize();


void PyCallback::execute()

  PyGILState_STATE gstate;

  gstate = PyGILState_Ensure();
  try
  
    if ( PyCallable_Check(_method) )
    
      _info("Build arguments and call method");
      PyObject *args = Py_BuildValue("(O)", _self);
      PyObject *kwargs = Py_BuildValue("", "", NULL);
      PyObject_Call(_method, args, kwargs);
    
    else
    
      _warning("The given method is not callable!");
    
  
  catch(...)
  
    // TODO: collect and show more information about the exception
    _error("Exception calling python");
  
  PyGILState_Release(gstate);

即使我不确定如何调用,主要是_PyCallable_Check_返回false。

我还测试了使用 typedef 选项和 C 指向函数的指针以相同的 segfault 结果调用它。

更新@2016/08/03

我已着手进行建议的修改。 cameraRemovalCallback 现在从 cdef 更改为 def 并且 PyCallback 中的一些 ifs 报告现在找到了该方法。还向~PyCallback() 添加了对Py_XDECREF(_method) 的调用,以防在构造函数中找到它。没用的try-catch也被删除了。

从对Python's Object protocol 的引用,DavidW 提到的,我检查了许多*Call*combinations:下降到segfault。

我认为这个问题正在变得肮脏并且正在出现论坛(问题->回答->重播->...)。对此我很抱歉,我会尝试,下次我会写,告诉 segfault 已解决,到底是什么。

【问题讨论】:

回调函数可以用cython代码实现。请参阅a callback example、an old question 和 an old question。 PyObject_CallFunctionObjArgs docs.python.org/2/c-api/… 的文档暗示您应该传递可变数量的PyObject*s,后跟NULLNULL 很重要,因为它告诉 Python args 列表已结束 例如 PyObject_CallFunctionObjArgs(_method, self, NULL); 不幸的是,您的示例还不够完整,无法判断这是否是唯一的问题。 (另外:至少在提供的代码中,obj 实际上并没有保存在registerRemovalCallback 的任何位置。) 感谢 cmets。我已经看过这个例子和其他问题。但它们是我第一次尝试,直到我想到使用 c-api。关于第二条评论,你是对的,我做了一个尝试。但是第三个看起来是一个很好的候选人,因为你是对的,我没有存储指针。通常是许多段错误的根源。 【参考方案1】:

我不保证这是唯一的问题,但这肯定是一个问题:

cameraRemovalCallback 是一个cdef 函数。这意味着该函数完全可以从 C/Cython 访问,但不能从 Python 访问。这意味着 PyObject_GetAttrString 失败(因为 cameraRemovalCallback 不是 Python 属性)。

你应该用def而不是cdef定义cameraRemovalCallback,这样就可以通过普通的Python机制访问它。您还应该检查 PyObject_GetAttrString 的结果 - 如果它返回 NULL 则它未能找到该属性。

因此,您最终会尝试将 NULL 作为 Python 函数调用。


其他小问题:

您应该在 ~PyCallback 中声明 _method

您应该致电Py_InitializePy_Finalize。无论如何,您似乎是在 Python 中创建该类,因此它不需要初始化或完成。敲定肯定会给你带来麻烦。

我认为您不需要将self 作为参数传递给PyObject_Call。 (虽然我可能是错的)

Python C api 不会引发 C++ 异常,因此您的 try catch(...) 永远不会捕获任何东西。而是检查返回值。

您需要对Py_BuildValue 的两个调用的结果(当您完成它们时)以及PyObject_Call 的结果进行decref。如果你不这样做,你就会泄漏内存。


以下完整示例适用于我(使用 Python 3.5 - 我无法使用早期版本轻松测试它)。如果它对您有用,那么您可能需要确定您的情况有什么不同?如果它不适合你,那就更神秘了。

pycallback.hpp:

#include <Python.h>
#include <stdexcept>

inline PyObject* getCallable(PyObject* o, const char* methodName) 
    // assume o is not null
    PyObject* callable = PyObject_GetAttrString(o,methodName);
    if (callable == nullptr) 
        throw std::runtime_error("Attribute does not exist");
    
    return callable;


class PyCallback 
private:
    PyObject* _callable;

public:
    PyCallback(PyObject* callable) 
        // assume callable isn't null
        if (!PyCallable_Check(callable)) 
            throw std::runtime_error("object passed to PyCallback is not callable");
        
        _callable = callable;
        Py_XINCREF(_callable);
    

    PyCallback(PyObject* o, const char* methodName) : 
    PyCallback(getCallable(o,methodName))  // needs C++11 to compile
    

    // don't define copy operators
    PyCallback(const PyCallback&) = delete;
    PyCallback& operator=(const PyCallback&) = delete;

    ~PyCallback() 
        Py_XDECREF(_callable);
    

    void execute() 
        PyGILState_STATE gstate;
        gstate = PyGILState_Ensure();

        PyObject* result = PyObject_CallFunctionObjArgs(_callable,nullptr);

        Py_XDECREF(result); // allowed to be null
        PyGILState_Release(gstate);
    
;

camera.pyx

cdef extern from "pycallback.hpp":
    cdef cppclass PyCallback:
        PyCallback(object) except +
        PyCallback(object, const char*) except +
        void execute()

cdef class Camera:
    cdef PyCallback* o
    cdef public ispresent

    def __init__(self):
        self.o = NULL
        self.ispresent = True

    def registerRemovalCallback(self):
        self.o = new PyCallback(self,'cameraRemovalCallback')
        #self.o = new PyCallback(self.cameraRemovalCallback)

    def cameraRemovalCallback(self):
        self.ispresent = False

    def triggerCallback(self):
        if self.o != NULL:
            self.o.execute()

setup.py

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

setup(
    ext_modules = [
        Extension('camera',sources=["camera.pyx"],
            language="c++",
            extra_compile_args=['-std=c++11'])],
    cmdclass='build_ext': build_ext)

test.py

import camera

c = camera.Camera()
print(c.ispresent)
c.triggerCallback()
print(c.ispresent)
c.registerRemovalCallback()
print(c.ispresent)
c.triggerCallback()
print(c.ispresent)

注意 - 这有一个小问题。 Camera 和它持有的回调形成一个引用循环,因此它们永远不会被释放。这会导致少量内存泄漏,但不会导致分段错误。

【讨论】:

感谢您指出的问题,并按照您提到的方式修改了代码。但是当我尝试调用python方法时仍然是一个段错误。 @srgblnch 我确实添加了一个似乎对我有用的半“最小”示例(并且与您的代码没有太大不同)。我怀疑这实际上并不能解决您的问题,但它向我表明您的问题比您在此处显示的位更深地存在于您的代码中。 [添加评论是因为我意识到提问者实际上并没有收到有关答案编辑的通知] 非常感谢,你明白了。我已经开始阅读和比较你的建议。我犯的最后一个错误是 GIL。我已经与Py_Initialize 一起删除了,认为它们会一起使用。当相机被拔下并从 gige api 报告时,我终于在 python 中看到了日志消息。我非常感谢您所做的努力。

以上是关于使用 python-c-api 调用的 Cython 回调段错误的主要内容,如果未能解决你的问题,请参考以下文章

dubbo泛化调用使用及原理解析

使用汇编代码调用系统调用

使用反射的方法调用的问题问题,怎么解决

如何使用POST 方法调用服务

微服务RPC调用-OpenFeign的简单使用

(js)使用new对函数进行构造调用