我有一个库(如果有帮助的话,以源代码的形式)定义了以下函数:
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
返回bytes(24)
,或者,给定上述指针,我如何访问指向数组?
我是ctypes
的新手,我可能在文档中错过了一些东西,但我没有找到答案。
在回调中,如果POINTER(c_char)
用于数据(特别是如果数据包含null),则字符串切片工作将数据作为Python字节串读取。还建议为ctypes
调用的函数设置.argtypes
和.restype
,以便它可以正确地将python类型转换为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("x00x01x02x03x04x05x06x07",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'x00x01x02x03x04x05x06x07'
Data: [00010203:04050607] (8|42)
EDIT每个注释,下面演示如何将这一切包装在一个类中。请注意,如果在未绑定的类方法上使用修饰,则由于self
是必需的参数,因此回调将无法正确调用。同样重要的是,如果绑定的回调按照__init__
所示进行包装,那么回调将在类实例使用的整个生命周期内进行包装。在传递给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'x00x01x02x03x04x05x06x07')
输出:
b'x00x01x02x03x04x05x06x07'
Data: [00010203:04050607] (8|42)
使用ctypes.string_at
:
buf_data = ctypes.string_at(buf, bln)
不管名字是什么,它返回的是bytes
,而不是字符串
(来源:https://docs.python.org/3/library/ctypes.html)