I have a lib (in source form, if it helps) defining, among other things the following function:
void subscribe(const char* ip_addr,
uint16_t port,
uint32_t token,
void (*cb)(void *buffer, uint32_t length, uint32_t token)
);
I defined this in python (v3.10.4, if it matters):
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)
...
Registration and callback apparently work fine, but I'm receiving the pointer itself in the callback and not the content (I think that's consistent with what I coded) as printout looks like:
Data: [b'0cc2ddd9:c07f0000'] (24|42)
and b'0cc2ddd9:c07f0000'
looks suspiciously similar to a pointer (I'm on an amd64 machine).
How do I convince ctypes
to return a bytes(24)
or, alternatively, given the above pointer how do I access pointed array?
I am new to ctypes
and I could have missed something in the docs, but I didn't find the answer there.
In a callback, if POINTER(c_char)
is used for data (esp. if the data contains nulls), then string slicing works to read the data as a Python byte string. It's also recommended to set .argtypes
and .restype
for functions called by ctypes
, so it can convert Python-to-C types correctly (and vice versa):
test.c - working example for reproducible testing:
#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)
Output:
b'\x00\x01\x02\x03\x04\x05\x06\x07'
Data: [00010203:04050607] (8|42)
EDIT
Per comments, below demonstrates how to wrap this all up in a class. Note that if decoration is used on the unbound class method, the callback won't be called correctly due to self
being a required parameter. It's also important that if the bound callback is wrapped as shown in __init__
, then the callback will be wrapped for the lifetime of the use of the class instance. Do not wrap the instance method as it is passed to the subscribe
, e.g.:
self.lib.subscribe(self.ip, self.port, token, CALLBACK(self.callback))
because the wrapped object lifetime will end after the subscribe
call. If the callback is referenced later, e.g. by the event
call I've added, it would fail. Wrapping the bound instance method in __init__
ensures the wrapping lifetime exists for the lifetime of the instance.
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')
Output:
b'\x00\x01\x02\x03\x04\x05\x06\x07'
Data: [00010203:04050607] (8|42)