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
,不是字符串
在回调中,如果 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)
我有一个库(源代码形式,如果有帮助的话)定义了以下函数:
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
,不是字符串
在回调中,如果 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)