Python ctypes:如何定义具有缓冲区指针和参数长度的回调?

Python ctypes: how to define a callback having a buffer pointer and a length in argument?

我有一个库(源代码形式,如果有帮助的话)定义了以下函数:

void subscribe(const char* ip_addr,
               uint16_t port,
               uint32_t token,
               void (*cb)(void *buffer, uint32_t length, uint32_t token)
                  );

我在 python(v3.10.4,如果重要的话)中定义了这个:

from ctypes import *

so_file = './mylib.so'
lib = CDLL(so_file)

ADDRESS = b"127.0.0.1"
PORT = 21000

data_callback_type = CFUNCTYPE(None, POINTER(c_byte), c_int32, c_int32)

def py_data_callback(buf, bln, tok):
    print(f'Data: [{hexlify(buf, sep=":", bytes_per_sep=4)}] ({bln}|{tok})')

data_callback = data_callback_type(py_data_callback)
ret = lib.subscribe(c_char_p(ADDRESS), c_uint16(PORT), c_int32(42), data_callback)
print(ret)

...

注册和回调显然工作正常,但我在回调中接收到指针本身而不是内容(我认为这与我编写的代码一致),打印输出如下所示:

Data: [b'0cc2ddd9:c07f0000'] (24|42)

b'0cc2ddd9:c07f0000' 看起来很像指针(我在 amd64 机器上)。

我如何说服 ctypes 到 return 一个 bytes(24) 或者,或者,给定上述指针,我如何访问指向的数组?

我是 ctypes 的新手,我可能遗漏了文档中的某些内容,但我没有在那里找到答案。

使用ctypes.string_at:

buf_data = ctypes.string_at(buf, bln)

尽管名称如此,但此 returns 是一个 bytes,不是字符串

(来源:https://docs.python.org/3/library/ctypes.html

在回调中,如果 POINTER(c_char) 用于数据(尤其是如果数据包含空值),则字符串切片会将数据读取为 Python 字节字符串。还建议为ctypes调用的函数设置.argtypes.restype,这样它可以正确转换Python-to-C类型(反之亦然):

test.c - 可重复测试的工作示例:

#include <stdint.h>

#ifdef _WIN32
#   define API __declspec(dllexport)
#else
#   define API
#endif

API void subscribe(const char* ip_addr,
                   uint16_t port,
                   uint32_t token,
                   void (*cb)(void *buffer, uint32_t length, uint32_t token)) {
    cb("\x00\x01\x02\x03\x04\x05\x06\x07",8,token);
}

test.py

from ctypes import *

CALLBACK = CFUNCTYPE(None, POINTER(c_char), c_uint32, c_uint32)

# decorating a Python function makes it usable as a callback
@CALLBACK
def callback(buf, length, tok):
    print(buf[:length])
    print(f'Data: [{buf[:length].hex(sep=":", bytes_per_sep=4)}] ({length}|{tok})')

lib = CDLL('./test')
# correct argument and return types
lib.subscribe.argtypes = c_char_p, c_uint16, c_uint32, CALLBACK
lib.subscribe.restype = None

lib.subscribe(b'127.0.0.1', 21000, 42, callback)

输出:

b'\x00\x01\x02\x03\x04\x05\x06\x07'
Data: [00010203:04050607] (8|42)

编辑 根据评论,下面演示了如何将这一切包装在 class 中。请注意,如果在未绑定的 class 方法上使用装饰,则由于 self 是必需参数,因此不会正确调用回调。同样重要的是,如果 bound 回调如 __init__ 所示包装,那么回调将在 class 实例的使用生命周期内包装。 不要 包装传递给 subscribe 的实例方法,例如:

self.lib.subscribe(self.ip, self.port, token, CALLBACK(self.callback))

因为包装对象的生命周期将在 subscribe 调用后结束。如果稍后引用回调,例如通过我添加的 event 调用,它会失败。在 __init__ 中包装绑定实例方法可确保包装生命周期在实例的生命周期内存在。

test.c

#include <stdint.h>

#ifdef _WIN32
#   define API __declspec(dllexport)
#else
#   define API
#endif

typedef void (*CALLBACK)(void* buffer, uint32_t length, uint32_t token);

CALLBACK g_cb;
uint32_t g_token;

API void subscribe(const char* ip_addr,
                   uint16_t port,
                   uint32_t token,
                   void (*cb)(void *buffer, uint32_t length, uint32_t token)) {
    g_cb = cb;
    g_token = token;
}

API void event(void* buffer, uint32_t length) {
    if(g_cb)
        g_cb(buffer, length, g_token);
}

test.py

from ctypes import *

CALLBACK = CFUNCTYPE(None, POINTER(c_char), c_uint32, c_uint32)

class Test:

    lib = CDLL('./test')
    lib.subscribe.argtypes = c_char_p, c_uint16, c_uint32, CALLBACK
    lib.subscribe.restype = None

    def __init__(self, ip, port):
        self.ip = ip
        self.port = port
        self.cb = CALLBACK(self.callback)

    def subscribe(self, token):
        self.lib.subscribe(self.ip, self.port, token, self.cb)

    def event(self, data):
        self.lib.event(data, len(data))

    def callback(self, buf, length, tok):
        print(buf[:length])
        print(f'Data: [{buf[:length].hex(sep=":", bytes_per_sep=4)}] ({length}|{tok})')

test = Test(b'127.0.0.1', 21000)
test.subscribe(42)
test.event(b'\x00\x01\x02\x03\x04\x05\x06\x07')

输出:

b'\x00\x01\x02\x03\x04\x05\x06\x07'
Data: [00010203:04050607] (8|42)