CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-cffi

Foreign Function Interface for Python calling C code.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

callbacks-handles.mddocs/

Callbacks and Handles

Creating Python callbacks for C code and managing Python object handles in C. Essential for bidirectional communication between Python and C.

Capabilities

Python Callbacks for 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)

Handle Management

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)

Advanced Callback Patterns

Event System Integration

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())

Asynchronous Callback Handling

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()

Function Pointer Tables

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"])

Error Handling Patterns

Exception Translation

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))

Callback Lifetime Management

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()

Performance Considerations

Callback Overhead

# 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)

Handle Caching

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

docs

callbacks-handles.md

core-ffi.md

data-conversion.md

error-handling.md

index.md

memory-management.md

source-generation.md

type-system.md

tile.json