如何在Python ctypes中使方法作为回调?

15 投票
3 回答
6050 浏览
提问于 2025-04-17 00:56

我有一个C语言的接口,正在用Python的ctypes包来连接。整体运行得很好,但有一个小问题。

为了注册一些函数作为回调,用来处理通知,我调用了这个函数:

void RegisterNotifyCallback( int loginId, int extraFlags, void *(*callbackFunc)(Notification *))

所以在Python中,我的代码看起来是这样的:

CALLBACK = ctypes.CFUNCTYPE(None, ctypes.POINTER(Notification))
func = CALLBACK(myPythonCallback)
myLib.RegisterNofityCallback(45454, 0, func)

如果myPythonCallback的值是一个普通函数(也就是说,不是类的方法),那就没问题,能正确调用回调函数并传入正确的参数。但如果它是一个类的方法,虽然也能调用这个方法,但在方法执行完后就会出现段错误(segfault)。

补充说明

为了更清楚地说明,当我说函数类型时,我是指myPythonCallback被定义为一个普通函数,

def myPythonCallback(Notification):
    ...do something with the Notification object i get passed in

而当我说它是MethodType时,我指的是一个类的方法:

class MyClass(object):
    def myPythonCallback(self, Notification):
        ...do something with Notification

在第二种情况下就会出问题。

有没有简单的方法可以解决这个问题?或者有人能给我解释一下为什么会发生这种情况吗?

非常感谢,
Al。

补充说明

进一步用GDB调试时,我得到了这个:

Program received signal SIGSEGV, Segmentation fault. [Switching to Thread 1544567712 (LWP 5389)] 0x555ae962 in instancemethod_dealloc (im=0x558ed644) at Objects/classobject.c:2360 2360    Objects/classobject.c: No such file or directory.
        in Objects/classobject.c (gdb) backtrace
#0  0x555ae962 in instancemethod_dealloc (im=0x558ed644) at Objects/classobject.c:2360
#1  0x555f6a61 in tupledealloc (op=0x5592010c) at Objects/tupleobject.c:220
#2  0x555aeed1 in instancemethod_call (func=0x558db8ec, arg=0x5592010c, kw=0x0) at Objects/classobject.c:2579
#3  0x5559aedb in PyObject_Call (func=0x558db8ec, arg=0x5592c0ec, kw=0x0) at Objects/abstract.c:2529
#4  0x5563d5af in PyEval_CallObjectWithKeywords (func=0x558db8ec, arg=0x5592c0ec, kw=0x0) at Python/ceval.c:3881
#5  0x5559ae6a in PyObject_CallObject (o=0x0, a=0x0) at Objects/abstract.c:2517

传入instancemethod_dealloc的值是:

(gdb) x 0x558ed644 0x558ed644:     0x00000000

你觉得这个问题能解决吗?

3 个回答

1

有人说过,程序崩溃(segfault)可能是因为方法的垃圾回收问题,你需要保存对它的引用。我是这样解决缺少self参数和垃圾回收问题的。

CALLBACK = ctypes.CFUNCTYPE(None, ctypes.POINTER(Notification))

class MyClass(object):
    def myPythonCallback(self, Notification):
        ...do something with Notification
    
    def __init__(self):        
        self.myPythonCallback = CALLBACK(self.myPythonCallback)
        myLib.RegisterNofityCallback(45454, 0, self.myPythonCallback)

每次创建这个类的新实例时,都会生成一个绑定的方法(self.myPythonCallback),所以每个注册的回调函数都是特定于这个类实例的。我们在类中保存了对回调函数的引用,这样它就不会被垃圾回收掉。CFUNCTIONTYPE回调函数的执行方式和普通全局函数一样,<function_CFUNCTYPE>,而它内部的回调就是绑定的方法<bound method myPythoncCallback of MyClass 0xfffabca0>。绑定的方法会使用self来执行。

注意:虽然看起来你可以直接使用@decorator语法,但那样是行不通的。方法上的装饰器会装饰原始方法,然后再绑定到类上,所以效果是self.myPythonCallback会变成一个<bound CFUNCTYPE on MyClass>,调用的是一个普通方法<function myPythonCallback>

由@David-Heffernan提供的闭包解决方案给了我灵感。

4

这可能是一个关于ctypes的错误,涉及到为方法和函数设置堆栈时的神秘机制。

你有没有试过通过lambda或者函数来做一个包装?

使用lambda:

func = CALLBACK(lambda x: myPythonCallback(x))

使用函数:

def wrapper(x): myPythonCallback(x)
func = CALLBACK(wrapper)

还有第三种方法是使用闭包。如果你是在类里面设置回调,那么下面定义的方法应该会因为作用域的原因继承“self”这个参数——这让它虽然是普通函数,但表现得像个类方法。

class Foo:
    def __init__(self):
        def myPythonCallback(Notification):
            print self # it's there!
            pass # do something with notification

        func = CALLBACK(myPythonCallback)
        myLib.RegisterNofityCallback(45454, 0, func)
16

据我所知,你不能直接调用一个绑定的方法,因为它缺少了一个叫做self的参数。为了处理这个问题,我使用了一个闭包,像这样:

CALLBACK = ctypes.CFUNCTYPE(None, ctypes.POINTER(Notification))

class MyClass(object):

    def getCallbackFunc(self):
        def func(Notification):
            self.doSomething(Notification)
        return CALLBACK(func)

    def doRegister(self):
        myLib.RegisterNofityCallback(45454, 0, self.getCallbackFunc())

撰写回答