toolbox 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/bake/ruby/gdb.rb +135 -0
  4. data/bake/toolbox/gdb.rb +137 -0
  5. data/bake/toolbox/lldb.rb +137 -0
  6. data/context/fiber-debugging.md +171 -0
  7. data/context/getting-started.md +200 -0
  8. data/context/heap-debugging.md +351 -0
  9. data/context/index.yaml +28 -0
  10. data/context/object-inspection.md +208 -0
  11. data/context/stack-inspection.md +188 -0
  12. data/data/toolbox/command.py +479 -0
  13. data/data/toolbox/constants.py +200 -0
  14. data/data/toolbox/context.py +371 -0
  15. data/data/toolbox/debugger/__init__.py +101 -0
  16. data/data/toolbox/debugger/gdb_backend.py +664 -0
  17. data/data/toolbox/debugger/lldb_backend.py +986 -0
  18. data/data/toolbox/fiber.py +877 -0
  19. data/data/toolbox/format.py +205 -0
  20. data/data/toolbox/heap.py +679 -0
  21. data/data/toolbox/init.py +89 -0
  22. data/data/toolbox/print.py +79 -0
  23. data/data/toolbox/rarray.py +116 -0
  24. data/data/toolbox/rbasic.py +99 -0
  25. data/data/toolbox/rbignum.py +48 -0
  26. data/data/toolbox/rclass.py +136 -0
  27. data/data/toolbox/readme.md +214 -0
  28. data/data/toolbox/rexception.py +150 -0
  29. data/data/toolbox/rfloat.py +88 -0
  30. data/data/toolbox/rhash.py +151 -0
  31. data/data/toolbox/rstring.py +230 -0
  32. data/data/toolbox/rstruct.py +149 -0
  33. data/data/toolbox/rsymbol.py +278 -0
  34. data/data/toolbox/rvalue.py +183 -0
  35. data/data/toolbox/stack.py +620 -0
  36. data/lib/toolbox/gdb.rb +21 -0
  37. data/lib/toolbox/lldb.rb +21 -0
  38. data/lib/toolbox/version.rb +7 -1
  39. data/lib/toolbox.rb +9 -24
  40. data/license.md +21 -0
  41. data/readme.md +64 -0
  42. data/releases.md +9 -0
  43. data.tar.gz.sig +0 -0
  44. metadata +95 -165
  45. metadata.gz.sig +0 -0
  46. data/Rakefile +0 -61
  47. data/lib/dirs.rb +0 -9
  48. data/lib/toolbox/config.rb +0 -211
  49. data/lib/toolbox/default_controller.rb +0 -393
  50. data/lib/toolbox/helpers.rb +0 -11
  51. data/lib/toolbox/rendering.rb +0 -413
  52. data/lib/toolbox/searching.rb +0 -85
  53. data/lib/toolbox/session_params.rb +0 -63
  54. data/lib/toolbox/sorting.rb +0 -74
  55. data/locale/de/LC_MESSAGES/toolbox.mo +0 -0
  56. data/public/images/add.png +0 -0
  57. data/public/images/arrow_down.gif +0 -0
  58. data/public/images/arrow_up.gif +0 -0
  59. data/public/images/close.png +0 -0
  60. data/public/images/edit.gif +0 -0
  61. data/public/images/email.png +0 -0
  62. data/public/images/page.png +0 -0
  63. data/public/images/page_acrobat.png +0 -0
  64. data/public/images/page_add.png +0 -0
  65. data/public/images/page_copy.png +0 -0
  66. data/public/images/page_delete.png +0 -0
  67. data/public/images/page_edit.png +0 -0
  68. data/public/images/page_excel.png +0 -0
  69. data/public/images/page_list.png +0 -0
  70. data/public/images/page_save.png +0 -0
  71. data/public/images/page_word.png +0 -0
  72. data/public/images/remove.png +0 -0
  73. data/public/images/show.gif +0 -0
  74. data/public/images/spinner.gif +0 -0
  75. data/public/javascripts/popup.js +0 -498
  76. data/public/javascripts/toolbox.js +0 -18
  77. data/public/stylesheets/context_menu.css +0 -168
  78. data/public/stylesheets/popup.css +0 -30
  79. data/public/stylesheets/toolbox.css +0 -107
  80. data/view/toolbox/_collection.html.erb +0 -24
  81. data/view/toolbox/_collection_header.html.erb +0 -7
  82. data/view/toolbox/_context_menu.html.erb +0 -17
  83. data/view/toolbox/_dialogs.html.erb +0 -6
  84. data/view/toolbox/_form.html.erb +0 -30
  85. data/view/toolbox/_form_collection_row.html.erb +0 -18
  86. data/view/toolbox/_form_fieldset.html.erb +0 -30
  87. data/view/toolbox/_form_fieldset_row.html.erb +0 -19
  88. data/view/toolbox/_list.html.erb +0 -25
  89. data/view/toolbox/_list_row.html.erb +0 -10
  90. data/view/toolbox/_menu.html.erb +0 -7
  91. data/view/toolbox/_search_field.html.erb +0 -8
  92. data/view/toolbox/_show.html.erb +0 -12
  93. data/view/toolbox/_show_collection_row.html.erb +0 -6
  94. data/view/toolbox/_show_fieldset.html.erb +0 -21
  95. data/view/toolbox/edit.html.erb +0 -5
  96. data/view/toolbox/index.html.erb +0 -3
  97. data/view/toolbox/new.html.erb +0 -9
  98. data/view/toolbox/show.html.erb +0 -39
@@ -0,0 +1,986 @@
1
+ """
2
+ LLDB backend implementation for unified debugger interface.
3
+
4
+ Note: This is a proof-of-concept implementation. Full LLDB support
5
+ would require more extensive testing and edge case handling.
6
+ """
7
+
8
+ import lldb
9
+ import format
10
+
11
+ # Command categories (LLDB doesn't have exact equivalents, using symbolic constants)
12
+ COMMAND_DATA = 0
13
+ COMMAND_USER = 1
14
+
15
+ # Exception types
16
+ Error = RuntimeError # LLDB doesn't have a specific error type
17
+ MemoryError = RuntimeError # Map to RuntimeError for now
18
+
19
+
20
+ class Value:
21
+ """Wrapper for LLDB values providing unified interface."""
22
+
23
+ def __init__(self, lldb_value):
24
+ """Initialize with an LLDB value.
25
+
26
+ Args:
27
+ lldb_value: Native lldb.SBValue object
28
+ """
29
+ self._value = lldb_value
30
+
31
+ def __int__(self):
32
+ """Convert value to integer."""
33
+ return self._value.GetValueAsUnsigned()
34
+
35
+ def __str__(self):
36
+ """Convert value to string."""
37
+ return self._value.GetValue() or str(self._value.GetValueAsUnsigned())
38
+
39
+ def __eq__(self, other):
40
+ """Compare values for equality.
41
+
42
+ Compares by address/integer value to avoid debugger conversion issues.
43
+
44
+ Args:
45
+ other: Another Value, lldb.SBValue, or integer
46
+
47
+ Returns:
48
+ True if values are equal
49
+ """
50
+ if isinstance(other, Value):
51
+ return self._value.GetValueAsUnsigned() == other._value.GetValueAsUnsigned()
52
+ elif isinstance(other, lldb.SBValue):
53
+ return self._value.GetValueAsUnsigned() == other.GetValueAsUnsigned()
54
+ else:
55
+ return self._value.GetValueAsUnsigned() == int(other)
56
+
57
+ def __hash__(self):
58
+ """Return hash of value for use in sets/dicts.
59
+
60
+ Returns:
61
+ Hash of the integer value
62
+ """
63
+ return hash(self._value.GetValueAsUnsigned())
64
+
65
+ def __lt__(self, other):
66
+ """Less than comparison for pointer ordering.
67
+
68
+ Args:
69
+ other: Another Value, lldb.SBValue, or integer
70
+
71
+ Returns:
72
+ True if this value is less than other
73
+ """
74
+ if isinstance(other, Value):
75
+ return self._value.GetValueAsUnsigned() < other._value.GetValueAsUnsigned()
76
+ elif isinstance(other, lldb.SBValue):
77
+ return self._value.GetValueAsUnsigned() < other.GetValueAsUnsigned()
78
+ else:
79
+ return self._value.GetValueAsUnsigned() < int(other)
80
+
81
+ def __le__(self, other):
82
+ """Less than or equal comparison for pointer ordering.
83
+
84
+ Args:
85
+ other: Another Value, lldb.SBValue, or integer
86
+
87
+ Returns:
88
+ True if this value is less than or equal to other
89
+ """
90
+ if isinstance(other, Value):
91
+ return self._value.GetValueAsUnsigned() <= other._value.GetValueAsUnsigned()
92
+ elif isinstance(other, lldb.SBValue):
93
+ return self._value.GetValueAsUnsigned() <= other.GetValueAsUnsigned()
94
+ else:
95
+ return self._value.GetValueAsUnsigned() <= int(other)
96
+
97
+ def __gt__(self, other):
98
+ """Greater than comparison for pointer ordering.
99
+
100
+ Args:
101
+ other: Another Value, lldb.SBValue, or integer
102
+
103
+ Returns:
104
+ True if this value is greater than other
105
+ """
106
+ if isinstance(other, Value):
107
+ return self._value.GetValueAsUnsigned() > other._value.GetValueAsUnsigned()
108
+ elif isinstance(other, lldb.SBValue):
109
+ return self._value.GetValueAsUnsigned() > other.GetValueAsUnsigned()
110
+ else:
111
+ return self._value.GetValueAsUnsigned() > int(other)
112
+
113
+ def __ge__(self, other):
114
+ """Greater than or equal comparison for pointer ordering.
115
+
116
+ Args:
117
+ other: Another Value, lldb.SBValue, or integer
118
+
119
+ Returns:
120
+ True if this value is greater than or equal to other
121
+ """
122
+ if isinstance(other, Value):
123
+ return self._value.GetValueAsUnsigned() >= other._value.GetValueAsUnsigned()
124
+ elif isinstance(other, lldb.SBValue):
125
+ return self._value.GetValueAsUnsigned() >= other.GetValueAsUnsigned()
126
+ else:
127
+ return self._value.GetValueAsUnsigned() >= int(other)
128
+
129
+ def __add__(self, other):
130
+ """Add to this value.
131
+
132
+ Args:
133
+ other: Value, SBValue, or integer to add
134
+
135
+ Returns:
136
+ Integer result of addition
137
+ """
138
+ if isinstance(other, Value):
139
+ return self._value.GetValueAsUnsigned() + other._value.GetValueAsUnsigned()
140
+ elif isinstance(other, lldb.SBValue):
141
+ return self._value.GetValueAsUnsigned() + other.GetValueAsUnsigned()
142
+ else:
143
+ return self._value.GetValueAsUnsigned() + int(other)
144
+
145
+ def __radd__(self, other):
146
+ """Reverse add - when Value is on the right side of +.
147
+
148
+ Args:
149
+ other: Value, SBValue, or integer to add
150
+
151
+ Returns:
152
+ Integer result of addition
153
+ """
154
+ return self.__add__(other)
155
+
156
+ def __sub__(self, other):
157
+ """Subtract from this value.
158
+
159
+ Args:
160
+ other: Value, SBValue, or integer to subtract
161
+
162
+ Returns:
163
+ Integer result of subtraction
164
+ """
165
+ if isinstance(other, Value):
166
+ return self._value.GetValueAsUnsigned() - other._value.GetValueAsUnsigned()
167
+ elif isinstance(other, lldb.SBValue):
168
+ return self._value.GetValueAsUnsigned() - other.GetValueAsUnsigned()
169
+ else:
170
+ return self._value.GetValueAsUnsigned() - int(other)
171
+
172
+ def __rsub__(self, other):
173
+ """Reverse subtract - when Value is on the right side of -.
174
+
175
+ Args:
176
+ other: Value, SBValue, or integer to subtract from
177
+
178
+ Returns:
179
+ Integer result of subtraction
180
+ """
181
+ if isinstance(other, Value):
182
+ return other._value.GetValueAsUnsigned() - self._value.GetValueAsUnsigned()
183
+ elif isinstance(other, lldb.SBValue):
184
+ return other.GetValueAsUnsigned() - self._value.GetValueAsUnsigned()
185
+ else:
186
+ return int(other) - self._value.GetValueAsUnsigned()
187
+
188
+ def cast(self, type_obj):
189
+ """Cast this value to a different type.
190
+
191
+ Args:
192
+ type_obj: Type object to cast to
193
+
194
+ Returns:
195
+ New Value with cast type
196
+ """
197
+ if isinstance(type_obj, Type):
198
+ return Value(self._value.Cast(type_obj._type))
199
+ else:
200
+ # Assume it's a native LLDB type
201
+ return Value(self._value.Cast(type_obj))
202
+
203
+ def dereference(self):
204
+ """Dereference this pointer value.
205
+
206
+ Returns:
207
+ Value at the address
208
+ """
209
+ return Value(self._value.Dereference())
210
+
211
+ @property
212
+ def address(self):
213
+ """Get the address of this value.
214
+
215
+ Returns:
216
+ Value representing the address
217
+ """
218
+ return Value(self._value.AddressOf())
219
+
220
+ def __getitem__(self, key):
221
+ """Access struct field or array element.
222
+
223
+ Args:
224
+ key: Field name (str) or array index (int)
225
+
226
+ Returns:
227
+ Value of the field/element, or None if field doesn't exist or is invalid
228
+ """
229
+ if isinstance(key, str):
230
+ result = self._value.GetChildMemberWithName(key)
231
+ # Check if the result is valid
232
+ if result.IsValid() and not result.GetError().Fail():
233
+ return Value(result)
234
+ else:
235
+ return None
236
+ else:
237
+ # For integer index: check if this is a pointer type or array type
238
+ # Both need pointer-style arithmetic (arrays for flexible array members)
239
+ type_obj = self._value.GetType()
240
+ if type_obj.IsPointerType() or type_obj.IsArrayType():
241
+ # For arrays, treat the array address as a pointer to the element type
242
+ if type_obj.IsArrayType():
243
+ # Get the element type from the array
244
+ element_type = type_obj.GetArrayElementType()
245
+ # The address of the array IS the address of the first element
246
+ base_addr = self._value.GetLoadAddress()
247
+ else:
248
+ # For pointers, get the pointee type and dereference
249
+ element_type = type_obj.GetPointeeType()
250
+ base_addr = self._value.GetValueAsUnsigned()
251
+
252
+ type_size = element_type.GetByteSize()
253
+ new_addr = base_addr + (key * type_size)
254
+
255
+ # Optimization: if element type is a pointer, we can use CreateValueFromAddress
256
+ # which is much faster than reading memory and creating SBData
257
+ target = lldb.debugger.GetSelectedTarget()
258
+ if element_type.IsPointerType():
259
+ # Fast path for pointer arrays: use CreateValueFromAddress
260
+ # CreateValueFromAddress needs an SBAddress
261
+ sb_addr = lldb.SBAddress(new_addr, target)
262
+ result = target.CreateValueFromAddress(
263
+ f"element_{key}",
264
+ sb_addr,
265
+ element_type
266
+ )
267
+ return Value(result)
268
+ else:
269
+ # Slow path for struct/primitive arrays: read memory
270
+ process = target.GetProcess()
271
+ error = lldb.SBError()
272
+
273
+ # Read the value from memory
274
+ data = process.ReadMemory(new_addr, type_size, error)
275
+ if error.Fail():
276
+ raise MemoryError(f"Failed to read memory at 0x{new_addr:x}: {error.GetCString()}")
277
+
278
+ # Create an SBData from the bytes
279
+ sb_data = lldb.SBData()
280
+ sb_data.SetData(error, data, target.GetByteOrder(), element_type.GetByteSize())
281
+
282
+ # Create a value from the data
283
+ result = target.CreateValueFromData(
284
+ f"element_{key}",
285
+ sb_data,
286
+ element_type
287
+ )
288
+ return Value(result)
289
+ else:
290
+ # For structs, use GetChildAtIndex
291
+ return Value(self._value.GetChildAtIndex(key))
292
+
293
+ def __add__(self, offset):
294
+ """Pointer arithmetic: add offset.
295
+
296
+ Args:
297
+ offset: Integer offset to add
298
+
299
+ Returns:
300
+ New Value with adjusted pointer
301
+ """
302
+ # LLDB requires more explicit pointer arithmetic
303
+ # Get the type size and calculate new address
304
+ type_size = self._value.GetType().GetPointeeType().GetByteSize()
305
+ new_addr = self._value.GetValueAsUnsigned() + (offset * type_size)
306
+
307
+ # Create a new pointer value using CreateValueFromExpression
308
+ target = lldb.debugger.GetSelectedTarget()
309
+ addr_value = target.CreateValueFromExpression(
310
+ "temp",
311
+ f"({self._value.GetType().GetName()})0x{new_addr:x}"
312
+ )
313
+ return Value(addr_value)
314
+
315
+ def __sub__(self, offset):
316
+ """Pointer arithmetic: subtract offset.
317
+
318
+ Args:
319
+ offset: Integer offset to subtract (or another Value for pointer difference)
320
+
321
+ Returns:
322
+ New Value with adjusted pointer, or integer difference if subtracting pointers
323
+ """
324
+ if isinstance(offset, (Value, type(self._value))):
325
+ # Subtracting two pointers - return the difference in elements
326
+ other_value = offset if isinstance(offset, Value) else Value(offset)
327
+ type_size = self._value.GetType().GetPointeeType().GetByteSize()
328
+ addr_diff = self._value.GetValueAsUnsigned() - other_value._value.GetValueAsUnsigned()
329
+ return addr_diff // type_size
330
+ else:
331
+ # Subtracting integer offset from pointer
332
+ type_size = self._value.GetType().GetPointeeType().GetByteSize()
333
+ new_addr = self._value.GetValueAsUnsigned() - (offset * type_size)
334
+
335
+ # Create a new pointer value using CreateValueFromExpression
336
+ target = lldb.debugger.GetSelectedTarget()
337
+ addr_value = target.CreateValueFromExpression(
338
+ "temp",
339
+ f"({self._value.GetType().GetName()})0x{new_addr:x}"
340
+ )
341
+ return Value(addr_value)
342
+
343
+ @property
344
+ def type(self):
345
+ """Get the type of this value.
346
+
347
+ Returns:
348
+ Type object
349
+ """
350
+ return Type(self._value.GetType())
351
+
352
+ @property
353
+ def native(self):
354
+ """Get the underlying native LLDB value.
355
+
356
+ Returns:
357
+ lldb.SBValue object
358
+ """
359
+ return self._value
360
+
361
+
362
+ class Type:
363
+ """Wrapper for LLDB types providing unified interface."""
364
+
365
+ def __init__(self, lldb_type):
366
+ """Initialize with an LLDB type.
367
+
368
+ Args:
369
+ lldb_type: Native lldb.SBType object
370
+ """
371
+ self._type = lldb_type
372
+
373
+ def __str__(self):
374
+ """Convert type to string."""
375
+ return self._type.GetName()
376
+
377
+ def pointer(self):
378
+ """Get pointer type to this type.
379
+
380
+ Returns:
381
+ Type representing pointer to this type
382
+ """
383
+ return Type(self._type.GetPointerType())
384
+
385
+ def array(self, count):
386
+ """Get array type of this type.
387
+
388
+ Args:
389
+ count: Number of elements in the array (count - 1 for LLDB)
390
+
391
+ Returns:
392
+ Type representing array of this type
393
+ """
394
+ return Type(self._type.GetArrayType(count + 1))
395
+
396
+ @property
397
+ def native(self):
398
+ """Get the underlying native LLDB type.
399
+
400
+ Returns:
401
+ lldb.SBType object
402
+ """
403
+ return self._type
404
+
405
+ @property
406
+ def sizeof(self):
407
+ """Get the size of this type in bytes.
408
+
409
+ Returns:
410
+ Size in bytes as integer
411
+ """
412
+ return self._type.GetByteSize()
413
+
414
+
415
+ # Global registry of LLDB command wrappers
416
+ _lldb_command_wrappers = {}
417
+
418
+ class Command:
419
+ """Base class for debugger commands.
420
+
421
+ Subclass this and implement invoke() to create custom commands.
422
+ """
423
+
424
+ # Registry of commands for LLDB
425
+ _commands = {}
426
+
427
+ def __init__(self, name, category=COMMAND_DATA):
428
+ """Initialize and register a command.
429
+
430
+ Args:
431
+ name: Command name (e.g., "rb-print")
432
+ category: Command category (not used in LLDB)
433
+ """
434
+ self.name = name
435
+ self.category = category
436
+
437
+ # Register in our command registry
438
+ Command._commands[name] = self
439
+
440
+ # Note: LLDB command registration happens in init.py after all commands are loaded
441
+ # This is because LLDB needs the wrapper functions to be in a module's namespace,
442
+ # and that's easier to manage from the init script
443
+
444
+ def invoke(self, arg, from_tty):
445
+ """Handle command invocation.
446
+
447
+ Override this method in subclasses.
448
+
449
+ Args:
450
+ arg: Command arguments as string
451
+ from_tty: True if command invoked from terminal
452
+ """
453
+ raise NotImplementedError("Subclasses must implement invoke()")
454
+
455
+ @classmethod
456
+ def get_command(cls, name):
457
+ """Get a registered command by name.
458
+
459
+ Args:
460
+ name: Command name
461
+
462
+ Returns:
463
+ Command instance or None
464
+ """
465
+ return cls._commands.get(name)
466
+
467
+
468
+ def parse_and_eval(expression):
469
+ """Evaluate an expression in the debugger.
470
+
471
+ Args:
472
+ expression: Expression string (e.g., "$var", "ruby_current_vm_ptr", "42")
473
+
474
+ Returns:
475
+ Value object representing the result
476
+ """
477
+ target = lldb.debugger.GetSelectedTarget()
478
+
479
+ # If no target is selected, use the dummy target for constant evaluation
480
+ if not target.IsValid():
481
+ target = lldb.debugger.GetDummyTarget()
482
+
483
+ # Try to evaluate with a frame context if available (for variables, memory access, etc.)
484
+ process = target.GetProcess()
485
+ if process.IsValid():
486
+ thread = process.GetSelectedThread()
487
+ if thread.IsValid():
488
+ frame = thread.GetSelectedFrame()
489
+ if frame.IsValid():
490
+ # We have a full context - use frame evaluation
491
+ result = frame.EvaluateExpression(expression)
492
+ if result.IsValid():
493
+ return Value(result)
494
+
495
+ # Fallback to target-level evaluation (works for constants, globals, type casts)
496
+ # This works even with core dumps or dummy targets
497
+ result = target.EvaluateExpression(expression)
498
+
499
+ if not result.IsValid():
500
+ raise Error(f"Failed to evaluate expression: {expression}")
501
+
502
+ # Check if the expression evaluation had an error (even if result is "valid")
503
+ # LLDB returns valid=True with value=0 for undefined symbols
504
+ error = result.GetError()
505
+ if error.Fail():
506
+ raise Error(f"Failed to evaluate expression '{expression}': {error.GetCString()}")
507
+
508
+ return Value(result)
509
+
510
+
511
+ def lookup_type(type_name):
512
+ """Look up a type by name.
513
+
514
+ Args:
515
+ type_name: Type name (e.g., "struct RString", "VALUE")
516
+
517
+ Returns:
518
+ Type object
519
+
520
+ Raises:
521
+ Error: If type cannot be found in debug symbols
522
+ """
523
+ target = lldb.debugger.GetSelectedTarget()
524
+
525
+ # LLDB's FindFirstType searches loaded debug symbols
526
+ lldb_type = target.FindFirstType(type_name)
527
+
528
+ if not lldb_type.IsValid():
529
+ raise Error(f"Failed to find type: {type_name}")
530
+
531
+ return Type(lldb_type)
532
+
533
+
534
+ def set_convenience_variable(name, value):
535
+ """Set an LLDB convenience variable.
536
+
537
+ Args:
538
+ name: Variable name (without $ prefix)
539
+ value: Value to set (can be Value wrapper or native value)
540
+
541
+ Note: LLDB convenience variables are created using expression evaluation.
542
+ The variable name will be prefixed with $ automatically.
543
+ """
544
+ if isinstance(value, Value):
545
+ native_value = value._value
546
+ else:
547
+ native_value = value
548
+
549
+ # LLDB approach: use expression to create a persistent variable
550
+ # Variables in LLDB are prefixed with $
551
+ target = lldb.debugger.GetSelectedTarget()
552
+ if not target:
553
+ return
554
+
555
+ process = target.GetProcess()
556
+ if not process:
557
+ return
558
+
559
+ thread = process.GetSelectedThread()
560
+ if not thread:
561
+ return
562
+
563
+ frame = thread.GetSelectedFrame()
564
+ if not frame:
565
+ return
566
+
567
+ # Create a persistent variable by evaluating an assignment expression
568
+ if hasattr(native_value, 'GetValue'):
569
+ # It's an SBValue
570
+ addr = native_value.GetValueAsUnsigned()
571
+ type_name = native_value.GetType().GetName()
572
+
573
+ # Use expr command to create persistent variable
574
+ # Format: expr <type> $varname = (<type>)address
575
+ expr = f"{type_name} ${name} = ({type_name})0x{addr:x}"
576
+
577
+ options = lldb.SBExpressionOptions()
578
+ options.SetIgnoreBreakpoints(True)
579
+ options.SetFetchDynamicValue(lldb.eNoDynamicValues)
580
+ options.SetTimeoutInMicroSeconds(100000) # 0.1 second timeout
581
+ options.SetTryAllThreads(False)
582
+
583
+ # Evaluate the expression to create the persistent variable
584
+ result = frame.EvaluateExpression(expr, options)
585
+
586
+ # Check for errors
587
+ if result.GetError().Fail():
588
+ error_msg = result.GetError().GetCString()
589
+ # Silently ignore errors for now - convenience variables are optional
590
+ pass
591
+
592
+
593
+ def execute(command, from_tty=False, to_string=False):
594
+ """Execute a debugger command.
595
+
596
+ Args:
597
+ command: Command string to execute
598
+ from_tty: Whether command is from terminal (unused in LLDB)
599
+ to_string: If True, return command output as string
600
+
601
+ Returns:
602
+ String output if to_string=True, None otherwise
603
+ """
604
+ debugger = lldb.debugger
605
+ interpreter = debugger.GetCommandInterpreter()
606
+
607
+ result = lldb.SBCommandReturnObject()
608
+ interpreter.HandleCommand(command, result)
609
+
610
+ if not result.Succeeded():
611
+ raise Error(f"Command failed: {result.GetError()}")
612
+
613
+ if to_string:
614
+ return result.GetOutput()
615
+ return None
616
+
617
+
618
+ def lookup_symbol(address):
619
+ """Look up symbol name for an address.
620
+
621
+ Args:
622
+ address: Memory address (as integer)
623
+
624
+ Returns:
625
+ Symbol name string, or None if no symbol found
626
+ """
627
+ try:
628
+ symbol_info = execute(f"image lookup -a 0x{address:x}", to_string=True)
629
+ # LLDB output format: "Summary: module`symbol_name at file:line"
630
+ # or "Symbol: ... name = "symbol_name""
631
+ for line in symbol_info.split('\n'):
632
+ if 'Summary:' in line:
633
+ # Extract symbol from "Summary: ruby`rb_f_puts at io.c:8997:1"
634
+ if '`' in line:
635
+ start = line.index('`') + 1
636
+ # Symbol ends at ' at ' or end of line
637
+ if ' at ' in line[start:]:
638
+ end = start + line[start:].index(' at ')
639
+ else:
640
+ end = len(line)
641
+ return line[start:end].strip()
642
+ elif 'Symbol:' in line and 'name = "' in line:
643
+ # Alternative format
644
+ start = line.index('name = "') + 8
645
+ end = line.index('"', start)
646
+ return line[start:end]
647
+ return None
648
+ except:
649
+ return None
650
+
651
+
652
+ def invalidate_cached_frames():
653
+ """Invalidate cached frame information.
654
+
655
+ Note: LLDB handles frame caching differently than GDB.
656
+ This is a no-op for now.
657
+ """
658
+ # LLDB typically invalidates frames automatically when the
659
+ # process state changes. Manual invalidation is rarely needed.
660
+ pass
661
+
662
+
663
+ def get_enum_value(enum_name, member_name):
664
+ """Get an enum member value.
665
+
666
+ Args:
667
+ enum_name: The enum type name (e.g., 'ruby_value_type')
668
+ member_name: The member name (e.g., 'RUBY_T_STRING')
669
+
670
+ Returns:
671
+ Integer value of the enum member
672
+
673
+ Raises:
674
+ Error if enum member cannot be found
675
+
676
+ Note: In LLDB, enum members must be accessed with the enum prefix:
677
+ (int)enum_name::member_name
678
+ """
679
+ enum_expr = f"(int){enum_name}::{member_name}"
680
+ return int(parse_and_eval(enum_expr))
681
+
682
+
683
+ def read_memory(address, size):
684
+ """Read memory from the debugged process.
685
+
686
+ Args:
687
+ address: Memory address (as integer or pointer value)
688
+ size: Number of bytes to read
689
+
690
+ Returns:
691
+ bytes object containing the memory contents
692
+
693
+ Raises:
694
+ MemoryError: If memory cannot be read
695
+ """
696
+ # Convert to integer address if needed
697
+ if hasattr(address, '__int__'):
698
+ address = int(address)
699
+
700
+ process = lldb.debugger.GetSelectedTarget().GetProcess()
701
+ error = lldb.SBError()
702
+ data = process.ReadMemory(address, size, error)
703
+
704
+ if not error.Success():
705
+ raise MemoryError(f"Cannot read {size} bytes at 0x{address:x}: {error}")
706
+
707
+ return bytes(data)
708
+
709
+
710
+ def read_cstring(address, max_length=256):
711
+ """Read a NUL-terminated C string from memory.
712
+
713
+ Args:
714
+ address: Memory address (as integer or pointer value)
715
+ max_length: Maximum bytes to read before giving up
716
+
717
+ Returns:
718
+ Tuple of (bytes, actual_length) where actual_length is the string
719
+ length not including the NUL terminator
720
+
721
+ Raises:
722
+ MemoryError: If memory cannot be read
723
+ """
724
+ # Convert to integer address if needed
725
+ if hasattr(address, '__int__'):
726
+ address = int(address)
727
+
728
+ process = lldb.debugger.GetSelectedTarget().GetProcess()
729
+ error = lldb.SBError()
730
+ data = process.ReadMemory(address, max_length, error)
731
+
732
+ if not error.Success():
733
+ raise MemoryError(f"Cannot read memory at 0x{address:x}: {error}")
734
+
735
+ buffer = bytes(data)
736
+ n = buffer.find(b'\x00')
737
+ if n == -1:
738
+ n = max_length
739
+ return (buffer[:n], n)
740
+
741
+
742
+ def create_value(address, value_type):
743
+ """Create a typed Value from a memory address.
744
+
745
+ Args:
746
+ address: Memory address (as integer, pointer value, or lldb.SBValue)
747
+ value_type: Type object (or native lldb.SBType) to cast to
748
+
749
+ Returns:
750
+ Value object representing the typed value at that address
751
+
752
+ Examples:
753
+ >>> rbasic_type = debugger.lookup_type('struct RBasic').pointer()
754
+ >>> obj = debugger.create_value(0x7fff12345678, rbasic_type)
755
+ """
756
+ # Unwrap Type if needed
757
+ if isinstance(value_type, Type):
758
+ value_type = value_type._type
759
+
760
+ # Handle different address types
761
+ if isinstance(address, Value):
762
+ # It's already a wrapped Value, just cast it
763
+ return Value(address._value.Cast(value_type))
764
+ elif isinstance(address, lldb.SBValue):
765
+ # It's a native lldb.SBValue, cast it directly
766
+ return Value(address.Cast(value_type))
767
+ else:
768
+ # Convert to integer address if needed
769
+ if hasattr(address, '__int__'):
770
+ address = int(address)
771
+
772
+ target = lldb.debugger.GetSelectedTarget()
773
+
774
+ # For array types, use expression evaluation (CreateValueFromData doesn't work for large arrays)
775
+ if value_type.IsArrayType():
776
+ type_name = value_type.GetName()
777
+ expr = f"*({type_name}*)0x{address:x}"
778
+ addr_value = target.CreateValueFromExpression(
779
+ f"array_0x{address:x}",
780
+ expr
781
+ )
782
+ return Value(addr_value)
783
+
784
+ # OPTIMIZATION: For simple types like VALUE (unsigned long), use CreateValueFromData
785
+ # which avoids expression evaluation and is much faster for bulk operations
786
+ process = target.GetProcess()
787
+
788
+ # For scalar types (integers/pointers), we can read and create directly
789
+ type_size = value_type.GetByteSize()
790
+ error = lldb.SBError()
791
+ data_bytes = process.ReadMemory(address, type_size, error)
792
+
793
+ if error.Fail():
794
+ # Fall back to expression-based approach if memory read fails
795
+ type_name = value_type.GetName()
796
+ expr = f"({type_name})0x{address:x}"
797
+ addr_value = target.CreateValueFromExpression(
798
+ f"addr_0x{address:x}",
799
+ expr
800
+ )
801
+ return Value(addr_value)
802
+
803
+ # Create SBData from the memory bytes
804
+ sb_data = lldb.SBData()
805
+ sb_data.SetData(error, data_bytes, target.GetByteOrder(), type_size)
806
+
807
+ # Create value from the data (no expression evaluation!)
808
+ # Note: This creates a value-by-value, but for VALUE (unsigned long) that's correct
809
+ addr_value = target.CreateValueFromData(
810
+ f"val_0x{address:x}",
811
+ sb_data,
812
+ value_type
813
+ )
814
+
815
+ # Store the original address as metadata so we can use it for debugging
816
+ # The value itself contains the VALUE integer read from that address
817
+ return Value(addr_value)
818
+
819
+
820
+ def create_value_from_address(address, value_type):
821
+ """Create a typed Value from a memory address.
822
+
823
+ This uses CreateValueFromAddress to create a value of the given type at the
824
+ specified address. This is more efficient than CreateValueFromExpression
825
+ and supports array types directly.
826
+
827
+ Args:
828
+ address: Memory address (as integer, or Value object representing a pointer)
829
+ value_type: Type object (or native lldb.SBType) representing the type
830
+
831
+ Returns:
832
+ Value object representing the data at that address
833
+
834
+ Examples:
835
+ >>> rbasic_type = debugger.lookup_type('struct RBasic')
836
+ >>> array_type = rbasic_type.array(100)
837
+ >>> page_start = page['start'] # Value object
838
+ >>> page_array = debugger.create_value_from_address(page_start, array_type)
839
+ """
840
+ # Convert to integer address if needed (handles Value objects via __int__)
841
+ if hasattr(address, '__int__'):
842
+ address = int(address)
843
+
844
+ # Unwrap Type if needed
845
+ if isinstance(value_type, Type):
846
+ value_type = value_type._type
847
+
848
+ target = lldb.debugger.GetSelectedTarget()
849
+
850
+ # CreateValueFromAddress takes an SBAddress, not an integer
851
+ # We need to create an SBAddress from the load address
852
+ sb_addr = target.ResolveLoadAddress(address)
853
+ if not sb_addr.IsValid():
854
+ raise MemoryError(f"Invalid address: 0x{address:x}")
855
+
856
+ # CreateValueFromAddress takes an address and creates a value of the given type
857
+ # reading from that memory location
858
+ addr_value = target.CreateValueFromAddress(
859
+ f"val_at_0x{address:x}",
860
+ sb_addr,
861
+ value_type
862
+ )
863
+
864
+ if not addr_value.IsValid():
865
+ raise MemoryError(f"Failed to create value from address 0x{address:x}")
866
+
867
+ return Value(addr_value)
868
+
869
+
870
+ def create_value_from_int(int_value, value_type):
871
+ """Create a typed Value from an integer (not a memory address to read from).
872
+
873
+ This is used when the integer itself IS the value (like VALUE which is a pointer).
874
+ Unlike create_value(), this doesn't read from memory - it creates a value containing
875
+ the integer itself.
876
+
877
+ Args:
878
+ int_value: Integer value, or Value object that will be converted to int
879
+ value_type: Type object (or native lldb.SBType) to cast to
880
+
881
+ Returns:
882
+ Value object with the integer value
883
+
884
+ Examples:
885
+ >>> value_type = debugger.lookup_type('VALUE')
886
+ >>> obj_address = page['start'] # Value object
887
+ >>> obj = debugger.create_value_from_int(obj_address, value_type)
888
+ """
889
+ # Convert to integer if needed (handles Value objects via __int__)
890
+ if hasattr(int_value, '__int__'):
891
+ int_value = int(int_value)
892
+
893
+ # Unwrap Type if needed
894
+ if isinstance(value_type, Type):
895
+ value_type = value_type._type
896
+
897
+ # Create SBData with the integer value
898
+ target = lldb.debugger.GetSelectedTarget()
899
+ type_size = value_type.GetByteSize()
900
+
901
+ # Convert integer to bytes (little-endian for x86_64)
902
+ int_bytes = int_value.to_bytes(type_size, byteorder='little', signed=False)
903
+
904
+ # Create SBData from bytes
905
+ error = lldb.SBError()
906
+ sb_data = lldb.SBData()
907
+ sb_data.SetData(error, int_bytes, target.GetByteOrder(), type_size)
908
+
909
+ # Create value from data
910
+ result = target.CreateValueFromData(
911
+ f"int_0x{int_value:x}",
912
+ sb_data,
913
+ value_type
914
+ )
915
+
916
+ return Value(result)
917
+
918
+
919
+ def register(name, handler_class, usage=None, category=COMMAND_USER):
920
+ """Register a command with LLDB using a handler class.
921
+
922
+ This creates a wrapper Command that handles parsing, terminal setup,
923
+ and delegates to the handler class for actual command logic.
924
+
925
+ Args:
926
+ name: Command name (e.g., "rb-print")
927
+ handler_class: Class to instantiate for handling the command
928
+ usage: Optional command.Usage specification for validation/help
929
+ category: Command category (COMMAND_USER, etc.)
930
+
931
+ Example:
932
+ class PrintHandler:
933
+ def invoke(self, arguments, terminal):
934
+ depth = arguments.get_option('depth', 1)
935
+ print(f"Depth: {depth}")
936
+
937
+ usage = command.Usage(
938
+ summary="Print something",
939
+ options={'depth': (int, 1)},
940
+ flags=['debug']
941
+ )
942
+ debugger.register("my-print", PrintHandler, usage=usage)
943
+
944
+ Returns:
945
+ The registered Command instance
946
+ """
947
+ class RegisteredCommand(Command):
948
+ def __init__(self):
949
+ super(RegisteredCommand, self).__init__(name, category)
950
+ self.usage_spec = usage
951
+ self.handler_class = handler_class
952
+
953
+ def invoke(self, arg, from_tty):
954
+ """LLDB entry point - parses arguments and delegates to handler."""
955
+ # Create terminal first (needed for help text)
956
+ import format
957
+ terminal = format.create_terminal(from_tty)
958
+
959
+ try:
960
+ # Parse and validate arguments
961
+ if self.usage_spec:
962
+ arguments = self.usage_spec.parse(arg if arg else "")
963
+ else:
964
+ # Fallback to basic parsing without validation
965
+ import command
966
+ arguments = command.parse_arguments(arg if arg else "")
967
+
968
+ # Instantiate handler and invoke
969
+ handler = self.handler_class()
970
+ handler.invoke(arguments, terminal)
971
+
972
+ except ValueError as e:
973
+ # Validation error - show colored help
974
+ print(f"Error: {e}")
975
+ if self.usage_spec:
976
+ print()
977
+ self.usage_spec.print_to(terminal, name)
978
+ except Exception as e:
979
+ print(f"Error: {e}")
980
+ import traceback
981
+ traceback.print_exc()
982
+
983
+ # Instantiate and register the command with LLDB
984
+ return RegisteredCommand()
985
+
986
+