Python对C回调的封装

2 投票
1 回答
4046 浏览
提问于 2025-04-16 19:54

我正在尝试创建一个Python的回调函数,这个函数需要在Windows环境下调用DLL中的C回调时被触发。请看下面的代码,以便理解我遇到的问题。

from ctypes import *

#---------qsort Callback-------------#
IntArray5 = c_int * 5
ia = IntArray5(5,1,7,33,99)
libc = cdll.msvcrt
qsort = libc.qsort
qsort.restype = None

CMPFUNC = CFUNCTYPE(c_int,POINTER(c_int),POINTER(c_int) )
test = 0
def py_cmp_func(a,b):
    #print 'py_cmp_func:',a[0],b[0]
    global test
    test = 10000
    return a[0]-b[0]

cmp_func = CMPFUNC(py_cmp_func)
qsort(ia, len(ia), sizeof(c_int), cmp_func)
print "global test=",test
for item in ia : print item

#----------Load DLL & Connect ------------#
gobiDLL = WinDLL("C:\LMS\QCWWAN2k.dll")
print  'Output of connect : ',gobiDLL.QCWWANConnect()

#----------SetByteTotalsCallback----------#
tx = POINTER(c_ulonglong)
rx = POINTER(c_ulonglong)
proto_callback = WINFUNCTYPE(c_void_p,tx,rx)

gtx = grx = 0 # Used to copy the response in the py_callback
def py_callback(t,r):
    sleep(10)
    print 'python callback ...'
    print "tx=",t,"rx=",r
    global gtx,grx
    gtx = 5000 # gtx = t
    grx = 2000 # grx = r
    #return 0

callback = proto_callback(py_callback)
gobiDLL.SetByteTotalsCallback.restype = c_ulong
gobiDLL.SetByteTotalsCallback.argtypes = [proto_callback,c_byte]                    

print "SetByteTotalsCallback = ",gobiDLL.SetByteTotalsCallback(callback, c_byte(256))
print "gtx = ",gtx
print "grx = ",grx

这个DLL文档中描述了SetByteTotalsCallback()方法的原型和回调,如下所示。

原型:

ULONG QCWWANAPI2K SetSessionStateCallback( tFNSessionState pCallback );

回调:

void ByteTotalsCallback( ULONGLONG  txTotalBytes, ULONGLONG rxTotalBytes );

输出:

>>> 
global test= 10000
1
5
7
33
99
Output of connect :  0
SetByteTotalsCallback =  0
gtx =  0
grx =  0
>>>>

目前的问题是,整个程序都能正常调用,但Python的回调函数根本没有被调用。在调用gobiDLL.SetByteTotalsCallback(callback, c_byte(256))方法时,程序以0状态退出,但在这个过程中,Python中写的callback()方法并没有被触发。

你能指出什么可能帮助我调用Python的回调函数吗?另外一个示例qsort()方法能够很好地传递指向Python函数的指针。我现在搞不清楚问题的根本原因。

谢谢!

安东尼

1 个回答

6

你不能直接这么做。C/C++ 的函数不能直接访问 Python 的函数——那个函数原型可能是期待一个指向 C 的指针。Python 会传递一个指向它内部数据结构的指针,专门用于那个特定的函数。

这时候你需要创建一个 C 扩展来包装那个 DLL,并把它暴露给 Python。基本上,你要让 C 的回调函数去调用 Python 的回调函数,因为这是可以做到的。更清楚地说,你想要实现的是:

                | This side is C land i.e. "real" addresses
                |
Python objects --> C extension -- register callback with --> DLL
                |                                             |
 in the python  |                                     Calls callback
                |                                             |
  interpreter <-------------- Callback in C extension  <-------
                |

下面是一个非常简单的解释,讲的是如何从 C 调用 Python 函数。你需要用构建你 Python 发行版时使用的 MSVC(或者其他工具)来编译这段代码;用 depends.exe 来找出它链接的 msvcXX.dll

全局状态通常被认为是不好的,但为了简单起见,我用了这个:

static PyObject* pyfunc_event_handler = NULL;
static PyObject* pyfunc_event_args = NULL;

然后我添加了一个设置处理函数,这样设置回调就更简单了。不过,你不一定需要这样做,你只需要

static PyObject* set_event_handler(PyObject *self, PyObject *args)
{
    PyObject *result = NULL;
    PyObject *temp;

下一行是关键——我允许传递两个 Python 对象(传给 PyArg_ParseTupleO 参数)。一个对象是函数,另一个是它的参数。

    if (PyArg_ParseTuple(args, "OO", &temp, &pyfunc_event_args)) {

        if (!PyCallable_Check(temp)) {
            PyErr_SetString(PyExc_TypeError, "parameter must be a function");
            return NULL;
        }

处理引用。Python 需要你这样做。

        Py_XINCREF(temp);                    /* Add a reference to new func */
        Py_XDECREF(pyfunc_event_handler);    /* Dispose of previous callback */
        pyfunc_event_handler = temp;         /* Remember new callback */

        /* Boilerplate to return "None" */
        Py_INCREF(Py_None);
        result = Py_None;
    }
    return result;
}

然后你可以在其他地方这样调用:

 PyObject* arglist = Py_BuildValue("(O)", pyfunc_event_args);
 pyobjresult = PyObject_CallObject(pyfunc_event_handler, arglist);
 Py_DECREF(arglist);

别忘了 DECREF,你需要让 Python 垃圾回收 arglist。

从 Python 使用这个非常简单:

set_event_handler(func, some_tuple)

其中 func 的参数要匹配,像这样:

def func(obj):
    /* handle obj */

你可能想了解的内容:

撰写回答