0
# Callbacks and Handles
1
2
Creating Python callbacks for C code and managing Python object handles in C. Essential for bidirectional communication between Python and C.
3
4
## Capabilities
5
6
### Python Callbacks for C
7
8
Creates C-callable function pointers from Python functions for callback-based C APIs.
9
10
```python { .api }
11
def callback(self, cdecl, python_callable=None, error=None, onerror=None):
12
"""
13
Create C callback from Python function.
14
15
Parameters:
16
- cdecl (str): C function pointer type declaration
17
- python_callable: Python function to call (or None for decorator mode)
18
- error: Return value on Python exception (default: 0 or NULL)
19
- onerror: Function to call on Python exception
20
21
Returns:
22
C function pointer or decorator function
23
"""
24
```
25
26
**Usage Examples:**
27
28
```python
29
# Direct callback creation
30
def my_comparison(a, b):
31
return (a > b) - (a < b) # -1, 0, or 1
32
33
compare_func = ffi.callback("int(int, int)", my_comparison)
34
35
# Use with C library
36
ffi.cdef("void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));")
37
libc = ffi.dlopen(None)
38
39
# Decorator mode
40
@ffi.callback("int(int, int)")
41
def compare_ints(a_ptr, b_ptr):
42
a = ffi.cast("int *", a_ptr)[0]
43
b = ffi.cast("int *", b_ptr)[0]
44
return (a > b) - (a < b)
45
46
# Error handling in callbacks
47
def risky_callback(x):
48
if x < 0:
49
raise ValueError("Negative input")
50
return x * 2
51
52
safe_callback = ffi.callback("int(int)", risky_callback, error=-1)
53
54
# Custom error handler
55
def error_handler(exception, exc_value, traceback):
56
print(f"Callback error: {exc_value}")
57
58
guarded_callback = ffi.callback("int(int)", risky_callback, onerror=error_handler)
59
```
60
61
### Handle Management
62
63
Manages Python object references in C code, allowing C to store and retrieve Python objects safely.
64
65
```python { .api }
66
def new_handle(self, x):
67
"""
68
Create handle for Python object.
69
70
Parameters:
71
- x: Python object to store
72
73
Returns:
74
CData handle (void* pointer to internal storage)
75
"""
76
77
def from_handle(self, x):
78
"""
79
Retrieve Python object from handle.
80
81
Parameters:
82
- x: Handle returned by new_handle()
83
84
Returns:
85
Original Python object
86
"""
87
88
def release(self, x):
89
"""
90
Release handle and allow Python object to be garbage collected.
91
92
Parameters:
93
- x: Handle to release
94
95
Returns:
96
None
97
"""
98
```
99
100
**Usage Examples:**
101
102
```python
103
# Store Python objects in C
104
class MyData:
105
def __init__(self, value):
106
self.value = value
107
108
def process(self):
109
return self.value * 2
110
111
# Create handle
112
data_obj = MyData(42)
113
handle = ffi.new_handle(data_obj)
114
115
# Pass handle to C (as void*)
116
ffi.cdef("void store_python_object(void* handle);")
117
lib = ffi.dlopen("./mylib.so")
118
lib.store_python_object(handle)
119
120
# Later, retrieve from C
121
ffi.cdef("void* get_python_object();")
122
retrieved_handle = lib.get_python_object()
123
retrieved_obj = ffi.from_handle(retrieved_handle)
124
print(retrieved_obj.process()) # 84
125
126
# Clean up when done
127
ffi.release(handle)
128
```
129
130
## Advanced Callback Patterns
131
132
### Event System Integration
133
134
```python
135
class EventSystem:
136
def __init__(self):
137
self.handlers = {}
138
self.c_callback = ffi.callback("void(int, void*)", self._dispatch_event)
139
140
def _dispatch_event(self, event_type, data_handle):
141
if event_type in self.handlers:
142
# Retrieve Python data from handle
143
data = ffi.from_handle(data_handle) if data_handle else None
144
self.handlers[event_type](data)
145
146
def register_handler(self, event_type, handler):
147
self.handlers[event_type] = handler
148
149
def get_c_callback(self):
150
return self.c_callback
151
152
# Usage
153
events = EventSystem()
154
155
def on_user_login(user_data):
156
print(f"User logged in: {user_data['username']}")
157
158
events.register_handler(1, on_user_login)
159
160
# Register C callback
161
ffi.cdef("void register_event_handler(void (*handler)(int, void*));")
162
lib.register_event_handler(events.get_c_callback())
163
```
164
165
### Asynchronous Callback Handling
166
167
```python
168
import threading
169
import queue
170
171
class AsyncCallbackManager:
172
def __init__(self):
173
self.callback_queue = queue.Queue()
174
self.c_callback = ffi.callback("void(int, void*)", self._queue_callback)
175
self.worker_thread = threading.Thread(target=self._process_callbacks)
176
self.worker_thread.daemon = True
177
self.worker_thread.start()
178
179
def _queue_callback(self, callback_id, data_handle):
180
# Queue callback for processing in Python thread
181
self.callback_queue.put((callback_id, data_handle))
182
183
def _process_callbacks(self):
184
while True:
185
callback_id, data_handle = self.callback_queue.get()
186
try:
187
# Process callback in Python thread context
188
self._handle_callback(callback_id, data_handle)
189
except Exception as e:
190
print(f"Callback error: {e}")
191
finally:
192
self.callback_queue.task_done()
193
194
def _handle_callback(self, callback_id, data_handle):
195
# Implement callback logic here
196
if data_handle:
197
data = ffi.from_handle(data_handle)
198
print(f"Processing callback {callback_id} with data: {data}")
199
200
# Usage
201
async_manager = AsyncCallbackManager()
202
```
203
204
### Function Pointer Tables
205
206
```python
207
class FunctionTable:
208
def __init__(self):
209
self.functions = {}
210
self.c_functions = {}
211
212
def register_function(self, name, signature, py_func):
213
"""Register Python function with C signature"""
214
c_func = ffi.callback(signature, py_func)
215
self.functions[name] = py_func
216
self.c_functions[name] = c_func
217
return c_func
218
219
def create_vtable(self, function_names):
220
"""Create C function pointer table"""
221
vtable_size = len(function_names)
222
vtable = ffi.new("void*[]", vtable_size)
223
224
for i, name in enumerate(function_names):
225
if name in self.c_functions:
226
vtable[i] = ffi.cast("void*", self.c_functions[name])
227
228
return vtable
229
230
# Usage
231
table = FunctionTable()
232
233
def add_impl(a, b):
234
return a + b
235
236
def multiply_impl(a, b):
237
return a * b
238
239
# Register functions
240
add_func = table.register_function("add", "int(int, int)", add_impl)
241
mul_func = table.register_function("multiply", "int(int, int)", multiply_impl)
242
243
# Create vtable for C
244
vtable = table.create_vtable(["add", "multiply"])
245
```
246
247
## Error Handling Patterns
248
249
### Exception Translation
250
251
```python
252
class CallbackException(Exception):
253
pass
254
255
def safe_callback_wrapper(py_func, error_return=0):
256
"""Wrap Python function to handle exceptions safely"""
257
def wrapper(*args):
258
try:
259
return py_func(*args)
260
except Exception as e:
261
# Log exception
262
print(f"Callback exception: {e}")
263
# Return safe error value
264
return error_return
265
return wrapper
266
267
# Usage
268
def risky_operation(value):
269
if value < 0:
270
raise CallbackException("Invalid value")
271
return value * 2
272
273
safe_callback = ffi.callback("int(int)",
274
safe_callback_wrapper(risky_operation, -1))
275
```
276
277
### Callback Lifetime Management
278
279
```python
280
class CallbackManager:
281
def __init__(self):
282
self.active_callbacks = []
283
self.handles = []
284
285
def create_callback(self, signature, py_func, keep_alive=True):
286
"""Create callback with automatic lifetime management"""
287
callback = ffi.callback(signature, py_func)
288
289
if keep_alive:
290
self.active_callbacks.append(callback)
291
292
return callback
293
294
def create_handle(self, obj, keep_alive=True):
295
"""Create handle with automatic lifetime management"""
296
handle = ffi.new_handle(obj)
297
298
if keep_alive:
299
self.handles.append(handle)
300
301
return handle
302
303
def cleanup(self):
304
"""Clean up all managed callbacks and handles"""
305
for handle in self.handles:
306
ffi.release(handle)
307
308
self.active_callbacks.clear()
309
self.handles.clear()
310
311
# Usage
312
manager = CallbackManager()
313
314
def my_callback(x):
315
return x + 1
316
317
# Callback stays alive until cleanup
318
callback = manager.create_callback("int(int)", my_callback)
319
320
# Handle stays alive until cleanup
321
data = {"key": "value"}
322
handle = manager.create_handle(data)
323
324
# Clean up when done
325
manager.cleanup()
326
```
327
328
## Performance Considerations
329
330
### Callback Overhead
331
332
```python
333
# Minimize callback overhead
334
def fast_callback(data_ptr, count):
335
"""Process bulk data in single callback"""
336
# Unpack array once
337
data = ffi.unpack(ffi.cast("int*", data_ptr), count)
338
339
# Process in Python
340
result = [x * 2 for x in data]
341
342
# Store result back
343
result_array = ffi.new("int[]", result)
344
ffi.memmove(data_ptr, result_array, count * ffi.sizeof("int"))
345
346
# Better than many small callbacks
347
bulk_callback = ffi.callback("void(void*, int)", fast_callback)
348
```
349
350
### Handle Caching
351
352
```python
353
class HandleCache:
354
def __init__(self):
355
self.obj_to_handle = {}
356
self.handle_to_obj = {}
357
358
def get_handle(self, obj):
359
"""Get handle for object, reusing if possible"""
360
obj_id = id(obj)
361
if obj_id not in self.obj_to_handle:
362
handle = ffi.new_handle(obj)
363
self.obj_to_handle[obj_id] = handle
364
self.handle_to_obj[handle] = obj
365
return self.obj_to_handle[obj_id]
366
367
def get_object(self, handle):
368
"""Get object from handle"""
369
return ffi.from_handle(handle)
370
371
def cleanup(self):
372
"""Release all cached handles"""
373
for handle in self.handle_to_obj:
374
ffi.release(handle)
375
self.obj_to_handle.clear()
376
self.handle_to_obj.clear()
377
378
# Usage for frequently passed objects
379
cache = HandleCache()
380
```