在回调中反向转换ctypes.py_object

6 投票
3 回答
3163 浏览
提问于 2025-04-16 01:16

我正在尝试用ctypes来封装一个C语言的库。这个库有一个功能是ondestroy回调,当库返回的句柄快要被销毁时会调用这个回调。

这个回调的格式是:

void cb(f *beingdestroyed);

这个API允许我们在库返回时,关联一个用户指定的void *。所以我可以把用来封装它的py_object作为用户数据关联上。我的计划是设置一个is_valid字段,当回调被触发时提取用户数据,并把这个字段设置为false。

我遇到的问题是如何提取我的高层py_object;我可以获取用户数据作为ctypes.void_p,然后转换成ctypes.py_object,但这样我只能使用Python的C API。有没有办法通过写user_object.is_valid = 0来反向转换成我可以使用的高层对象呢?

3 个回答

2

我把Bob Pyron的回答里的信息应用到了我的代码中,但我不喜欢回调函数需要知道它是被C程序调用的,还要处理一些ctypes的东西。结果发现,只需稍微改动一下,_private_enumerateBunnies()就可以接收一个Python对象,而不是一个空指针。我的代码里做了类似的改动:

FLUFFYBUNNY_CALLBACK = CFUNCTYPE(c_int, py_object)

这样一来,所有复杂的C语言相关的东西就隐藏在了已经处理这些内容的代码里,不会扩散到使用这个API的用户(也就是提供回调的那些人)身上。当然,你需要传递一个Python对象,但这算是个小限制。

我从Bob的原始回答中受益匪浅(当我克服了那些兔子之后),所以谢谢他!

4

通常的做法是完全避免这个问题。

可以用一个方法来代替函数作为回调,这样隐含的self就能让你访问到user_data这个字段。

12

为了更详细地解释Thomas Heller的回答:

  • 回调函数的原型应该把上下文参数指定为 c_void_p
  • 库函数的参数类型应该把上下文参数指定为 py_object
  • 调用这个函数时应该用 py_object(my_python_context_object)
  • 你在Python中实现的回调函数应该把上下文转换为py_object,并提取它的值:cast(context, py_object).value

下面是一个可以运行的例子。首先是一个简单DLL的C源代码:

// FluffyBunny.c
// Compile on windows with command line
//      cl /Gd /LD FluffyBunny.c
// Result is FluffyBunny.DLL, which exports one function:
//      FluffyBunny() uses __cdecl calling convention.

#include <windows.h>

BOOL APIENTRY DllMain(HMODULE, DWORD, LPVOID) {
  return TRUE;
}

typedef int (*FLUFFYBUNNY_CALLBACK)(void *context);

__declspec(dllexport) int FluffyBunny(FLUFFYBUNNY_CALLBACK cb, void *context) {
  int result = 0;
  int count = 0;
  if (cb) {
    while (result == 0) {
      result = (*cb)(context);
      ++count;
    }
  }
  return count;
}

接下来是一个调用这个DLL的Python程序:

# FluffyBunny.py
from ctypes import *

# Declare a class that will be used for context info in calls to FluffyBunny()
class Rabbit:
    def __init__(self):
        self.count = 0

# FluffyBunny() wants a callback function with the following C prototype:
#     typedef int (*FLUFFYBUNNY_CALLBACK)(void *context);
FLUFFYBUNNY_CALLBACK = CFUNCTYPE(c_int, c_void_p)

# This DLL has been compiled with __cdecl calling convention.
FluffyBunny_dll = CDLL('FluffyBunny.dll')

# Get the function from the library. Its C prototype is:
#     int FluffyBunny(FLUFFYBUNNY_CALLBACK cb, void *context);
# Note that I use "py_object" instead of "c_void_p" for the context argument.
FluffyBunny          = FluffyBunny_dll.FluffyBunny
FluffyBunny.restype  = c_int
FluffyBunny.argtypes = [FLUFFYBUNNY_CALLBACK, py_object]

# Create Python version of the callback function.
def _private_enumerateBunnies(context):
    # Convert the context argument to a py_object, and extract its value.
    # This gives us the original Rabbit object that was passed in to 
    # FluffyBunny().
    furball = cast(context, py_object).value
    # Do something with the context object.
    if furball:
        furball.count += 1
        print 'furball.count =', furball.count
        # Return non-zero as signal that FluffyBunny() should terminate
        return 0 if (furball.count < 10) else -1
    else:
        return -1

# Convert it to a C-callable function.
enumerateBunnies = FLUFFYBUNNY_CALLBACK(_private_enumerateBunnies)

# Try with no context info.
print 'no context info...'
result = FluffyBunny(enumerateBunnies, None)
print 'result=', result

# Give it a Python object as context info.
print 'instance of Rabbit as context info...'
furball = Rabbit()
result = FluffyBunny(enumerateBunnies, py_object(furball))
print 'result=', result

撰写回答