Foreign Function Interface for Python calling C code.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Creating Python callbacks for C code and managing Python object handles in C. Essential for bidirectional communication between Python and C.
Creates C-callable function pointers from Python functions for callback-based C APIs.
def callback(self, cdecl, python_callable=None, error=None, onerror=None):
"""
Create C callback from Python function.
Parameters:
- cdecl (str): C function pointer type declaration
- python_callable: Python function to call (or None for decorator mode)
- error: Return value on Python exception (default: 0 or NULL)
- onerror: Function to call on Python exception
Returns:
C function pointer or decorator function
"""Usage Examples:
# Direct callback creation
def my_comparison(a, b):
return (a > b) - (a < b) # -1, 0, or 1
compare_func = ffi.callback("int(int, int)", my_comparison)
# Use with C library
ffi.cdef("void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));")
libc = ffi.dlopen(None)
# Decorator mode
@ffi.callback("int(int, int)")
def compare_ints(a_ptr, b_ptr):
a = ffi.cast("int *", a_ptr)[0]
b = ffi.cast("int *", b_ptr)[0]
return (a > b) - (a < b)
# Error handling in callbacks
def risky_callback(x):
if x < 0:
raise ValueError("Negative input")
return x * 2
safe_callback = ffi.callback("int(int)", risky_callback, error=-1)
# Custom error handler
def error_handler(exception, exc_value, traceback):
print(f"Callback error: {exc_value}")
guarded_callback = ffi.callback("int(int)", risky_callback, onerror=error_handler)Manages Python object references in C code, allowing C to store and retrieve Python objects safely.
def new_handle(self, x):
"""
Create handle for Python object.
Parameters:
- x: Python object to store
Returns:
CData handle (void* pointer to internal storage)
"""
def from_handle(self, x):
"""
Retrieve Python object from handle.
Parameters:
- x: Handle returned by new_handle()
Returns:
Original Python object
"""
def release(self, x):
"""
Release handle and allow Python object to be garbage collected.
Parameters:
- x: Handle to release
Returns:
None
"""Usage Examples:
# Store Python objects in C
class MyData:
def __init__(self, value):
self.value = value
def process(self):
return self.value * 2
# Create handle
data_obj = MyData(42)
handle = ffi.new_handle(data_obj)
# Pass handle to C (as void*)
ffi.cdef("void store_python_object(void* handle);")
lib = ffi.dlopen("./mylib.so")
lib.store_python_object(handle)
# Later, retrieve from C
ffi.cdef("void* get_python_object();")
retrieved_handle = lib.get_python_object()
retrieved_obj = ffi.from_handle(retrieved_handle)
print(retrieved_obj.process()) # 84
# Clean up when done
ffi.release(handle)class EventSystem:
def __init__(self):
self.handlers = {}
self.c_callback = ffi.callback("void(int, void*)", self._dispatch_event)
def _dispatch_event(self, event_type, data_handle):
if event_type in self.handlers:
# Retrieve Python data from handle
data = ffi.from_handle(data_handle) if data_handle else None
self.handlers[event_type](data)
def register_handler(self, event_type, handler):
self.handlers[event_type] = handler
def get_c_callback(self):
return self.c_callback
# Usage
events = EventSystem()
def on_user_login(user_data):
print(f"User logged in: {user_data['username']}")
events.register_handler(1, on_user_login)
# Register C callback
ffi.cdef("void register_event_handler(void (*handler)(int, void*));")
lib.register_event_handler(events.get_c_callback())import threading
import queue
class AsyncCallbackManager:
def __init__(self):
self.callback_queue = queue.Queue()
self.c_callback = ffi.callback("void(int, void*)", self._queue_callback)
self.worker_thread = threading.Thread(target=self._process_callbacks)
self.worker_thread.daemon = True
self.worker_thread.start()
def _queue_callback(self, callback_id, data_handle):
# Queue callback for processing in Python thread
self.callback_queue.put((callback_id, data_handle))
def _process_callbacks(self):
while True:
callback_id, data_handle = self.callback_queue.get()
try:
# Process callback in Python thread context
self._handle_callback(callback_id, data_handle)
except Exception as e:
print(f"Callback error: {e}")
finally:
self.callback_queue.task_done()
def _handle_callback(self, callback_id, data_handle):
# Implement callback logic here
if data_handle:
data = ffi.from_handle(data_handle)
print(f"Processing callback {callback_id} with data: {data}")
# Usage
async_manager = AsyncCallbackManager()class FunctionTable:
def __init__(self):
self.functions = {}
self.c_functions = {}
def register_function(self, name, signature, py_func):
"""Register Python function with C signature"""
c_func = ffi.callback(signature, py_func)
self.functions[name] = py_func
self.c_functions[name] = c_func
return c_func
def create_vtable(self, function_names):
"""Create C function pointer table"""
vtable_size = len(function_names)
vtable = ffi.new("void*[]", vtable_size)
for i, name in enumerate(function_names):
if name in self.c_functions:
vtable[i] = ffi.cast("void*", self.c_functions[name])
return vtable
# Usage
table = FunctionTable()
def add_impl(a, b):
return a + b
def multiply_impl(a, b):
return a * b
# Register functions
add_func = table.register_function("add", "int(int, int)", add_impl)
mul_func = table.register_function("multiply", "int(int, int)", multiply_impl)
# Create vtable for C
vtable = table.create_vtable(["add", "multiply"])class CallbackException(Exception):
pass
def safe_callback_wrapper(py_func, error_return=0):
"""Wrap Python function to handle exceptions safely"""
def wrapper(*args):
try:
return py_func(*args)
except Exception as e:
# Log exception
print(f"Callback exception: {e}")
# Return safe error value
return error_return
return wrapper
# Usage
def risky_operation(value):
if value < 0:
raise CallbackException("Invalid value")
return value * 2
safe_callback = ffi.callback("int(int)",
safe_callback_wrapper(risky_operation, -1))class CallbackManager:
def __init__(self):
self.active_callbacks = []
self.handles = []
def create_callback(self, signature, py_func, keep_alive=True):
"""Create callback with automatic lifetime management"""
callback = ffi.callback(signature, py_func)
if keep_alive:
self.active_callbacks.append(callback)
return callback
def create_handle(self, obj, keep_alive=True):
"""Create handle with automatic lifetime management"""
handle = ffi.new_handle(obj)
if keep_alive:
self.handles.append(handle)
return handle
def cleanup(self):
"""Clean up all managed callbacks and handles"""
for handle in self.handles:
ffi.release(handle)
self.active_callbacks.clear()
self.handles.clear()
# Usage
manager = CallbackManager()
def my_callback(x):
return x + 1
# Callback stays alive until cleanup
callback = manager.create_callback("int(int)", my_callback)
# Handle stays alive until cleanup
data = {"key": "value"}
handle = manager.create_handle(data)
# Clean up when done
manager.cleanup()# Minimize callback overhead
def fast_callback(data_ptr, count):
"""Process bulk data in single callback"""
# Unpack array once
data = ffi.unpack(ffi.cast("int*", data_ptr), count)
# Process in Python
result = [x * 2 for x in data]
# Store result back
result_array = ffi.new("int[]", result)
ffi.memmove(data_ptr, result_array, count * ffi.sizeof("int"))
# Better than many small callbacks
bulk_callback = ffi.callback("void(void*, int)", fast_callback)class HandleCache:
def __init__(self):
self.obj_to_handle = {}
self.handle_to_obj = {}
def get_handle(self, obj):
"""Get handle for object, reusing if possible"""
obj_id = id(obj)
if obj_id not in self.obj_to_handle:
handle = ffi.new_handle(obj)
self.obj_to_handle[obj_id] = handle
self.handle_to_obj[handle] = obj
return self.obj_to_handle[obj_id]
def get_object(self, handle):
"""Get object from handle"""
return ffi.from_handle(handle)
def cleanup(self):
"""Release all cached handles"""
for handle in self.handle_to_obj:
ffi.release(handle)
self.obj_to_handle.clear()
self.handle_to_obj.clear()
# Usage for frequently passed objects
cache = HandleCache()Install with Tessl CLI
npx tessl i tessl/pypi-cffi