构建可从C调用的Python函数,输入参数具有*输出*语义

1 投票
2 回答
1845 浏览
提问于 2025-04-15 16:56

这个案例是这样的:

  • 有一个用C语言写的DLL(动态链接库),这个DLL是固定的,不能更改
  • 想要用Python来封装这个DLL(选择的方法是:ctypes)

这个DLL里面的一些函数需要同步机制。为了提供最大的灵活性,DLL的设计者完全依赖于客户端提供的回调函数。具体来说,这个DLL需要有:

  • 一个回调函数,用于创建同步对象
  • 一些回调函数,用于获取/释放同步对象的锁
  • 还有一个回调函数,用于销毁同步对象

因为从DLL的角度来看,同步对象是不可见的,所以它会用一个 void * 类型来表示。例如,如果DLL中的某个函数想要获取锁,它会这样做:

void* mutex;

/* get the mutex object via the create_mutex callback */
create_mutex(&mutex);

/* acquire a lock */
lock_mutex(mutex);

... etc

可以看到,回调函数 create_mutex 的输入参数有输出的意思。这是通过 void ** 这种签名实现的。

这个回调(还有其他三个)必须用Python来实现。我尝试过,但失败了 :-) 为了简单起见,我们只关注创建回调,并且为了简单起见,让这个不可见的对象用一个 int 来表示。

这个模拟回调使用的玩具DLL是这样的(ct_test.c):

#include <stdio.h>
#include <stdlib.h>

typedef int (* callback_t)(int**);
callback_t func;
int* global_i_p = NULL;

int mock_callback(int** ipp)
{
 int* dynamic_int_p = (int *) malloc(sizeof(int));
 /* dynamic int value from C */
 *dynamic_int_p = 2;
 *ipp = dynamic_int_p;
 return 0;
}

void set_py_callback(callback_t f)
{
 func = f;
}

void set_c_callback()
{
 func = mock_callback;
}

void test_callback(void)
{
 printf("global_i_p before: %p\n", global_i_p);
 func(&global_i_p);
 printf("global_i_p after: %p, pointed value:%d\n", global_i_p, *global_i_p);

 /* to be nice */
 if (func == mock_callback)
  free(global_i_p);
}

想要提供回调并使用这个DLL的Python代码如下:

from ctypes import *

lib = CDLL("ct_test.so")

# "dynamic" int value from python
int  = c_int(1)
int_p = pointer(int)

def pyfunc(p_p_i):
 p_p_i.contents = int_p

# create callback type and instance
CALLBACK = CFUNCTYPE(c_int, POINTER (POINTER(c_int)))
c_pyfunc = CALLBACK(pyfunc)

# functions from .so
set_py_callback = lib.set_py_callback
set_c_callback = lib.set_c_callback
test_callback = lib.test_callback

# set one of the callbacks
set_py_callback(c_pyfunc)
#set_c_callback()

# test it
test_callback()

当使用DLL提供的回调(通过 set_c_callback() 设置)时,这一切都按预期工作:

~/dev/test$ python ct_test.py
global_i_p before: (nil)
global_i_p after: 0x97eb008, pointed value:2

然而,在另一种情况下 - 使用Python回调 - 就失败了:

~/dev/test$ python ct_test.py
global_i_p before: (nil)
Traceback (most recent call last):
  File "/home/packages/python/2.5/python2.5-2.5.2/Modules/_ctypes/callbacks.c", line 284, in 'converting callback result'
TypeError: an integer is required
Exception  in <function pyfunc at 0xa14079c> ignored
Segmentation fault

我哪里出错了?

2 个回答

0

这个段错误是因为你在Python回调函数中处理指针时出错了。你使用的指针层级比实际需要的多,这可能让你感到困惑。在Python的回调中,你设置了p_p_i.contents,但这只是改变了Python ctypes对象指向的内容,并没有改变底层的指针。要做到这一点,你需要通过数组访问的方式来解引用指针。下面是一个简化的例子:

ip = ctypes.POINTER(ctypes.c_int)()
i = ctypes.c_int(99)
# Wrong way
ipp = ctypes.pointer(ip)
ipp.contents = ctypes.pointer(i)
print bool(ip) # False --> still NULL
# Right way
ipp = ctypes.pointer(ip)
ipp[0] = ctypes.pointer(i)
print ip[0] # 99 --> success!

类型错误是因为类型不兼容,正如Peter Hansen的回答中所描述的那样。

0

你似乎在定义返回类型的时候搞错了。看起来你的C语言回调函数返回的是一个整数(int),而你在Python中声明的返回类型是c_int,但实际上没有明确返回任何东西(所以实际上返回的是None)。如果你加上“return 0”,可能就不会崩溃了。你应该这样做,或者把回调函数的定义改成CFUNCTYPE(None, ...etc)

另外,虽然现在不是问题,但你把“int”这个内置名称给覆盖了。这可能会在以后引发问题。

编辑:正确地将C语言的返回类型称为“int”,而不是“void”。

撰写回答