or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

callbacks-handles.mdcore-ffi.mddata-conversion.mderror-handling.mdindex.mdmemory-management.mdsource-generation.mdtype-system.md

callbacks-handles.mddocs/

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

```