---
name: delphi-rtti-inspection
description: Use when the user asks "what is in this object", "inspect Self", or mentions RTTI, Delphi published properties, or wanting to walk the contents of a class instance. Teaches the rdbg `get_self` / `inspect_object` / `read` workflow for materializing Delphi state at a paused breakpoint.
---

# Delphi RTTI inspection with rdbg

Delphi's RTTI is rich. Every class compiled with `{$M+}` or
descended from `TPersistent` exposes published properties with
typed metadata. rdbg reads this directly from the target
process — you can walk arbitrary objects without source if you
have an address.

## Getting an address

Three common starting points:

1. **`get_self(tid?)`** when paused inside a method. Returns
   the `Self` object's address, class, and shallow fields. Use
   this whenever you've broken in a method and want to see what
   the receiver is.
2. **A field of an object you already have.** `inspect_object`
   surfaces field values; pointer-typed fields (`TObject`,
   `TPersistent`, etc.) carry the pointee's address. Recurse.
3. **A register or stack location.** `get_registers` returns the
   ABI-appropriate field set: on x64, `rcx` carries `Self` for
   instance-method entry, `rdx`/`r8`/`r9` are the first three
   args. On x86 (Delphi register convention), `Self` is in `eax`,
   first arg in `edx`, second in `ecx`. The response's `arch`
   field tells you which set to read. For deeper args,
   `read(kind="memory", address=rsp+offset)` (or `esp+offset`
   on x86).
4. **`inspect_locals(frame_idx=0)`** when paused inside a procedure
   compiled with `-V`. Returns the current procedure's parameters
   and stack-locals by name with their stack offsets / register
   IDs — much friendlier than reading raw registers + computing
   ABI offsets by hand. Use this as the discovery step, then
   `evaluate("LocalName.Field")` to chase from there. Falls back
   to `no_debug_section` when the target isn't built with `-V`;
   the older register-arithmetic approach above still works in
   that case.

## `inspect_object`

```
inspect_object(address=0x..., max_depth=3)
```

Returns:

- `class` — leaf class name, e.g. `TForm1`.
- `parent_chain` — `TForm` → `TCustomForm` → `TScrollingWinControl` → ...
- `instance_size` — bytes, including all inherited fields.
- `fields` — every field (including private/protected/published)
  with name, type, offset, and value. Pointer-typed fields
  include the pointee's address; primitive types include the
  value directly.
- `published_props` — typed properties with current values (read
  via getter where applicable).
- `methods` — method names with addresses (useful for setting
  BPs on virtual methods).
- `depth` / `max_depth` — recursion is bounded; at the boundary,
  pointer fields show only the address, not the pointee.

`max_depth=2` is a good default. `max_depth=5` for a one-time
deep walk. Don't go higher without a reason — recursive walks
can return tens of MB of JSON.

### Method-volume control

The full methods array on a typical exception class is ~3KB —
mostly inherited noise the agent rarely needs. **For
*"what's in this exception"* questions pass
`include_methods=false`** to drop the entire array:

```
inspect_object(address=0x..., max_depth=2, include_methods=false)
```

When you do want methods but only the leaf class's,
`methods_from_leaf_only=true` skips ancestors. Useful for
*"what overrides did this descendant add?"* without `TObject`'s
`Free` / `Destroy` / etc. cluttering the response.

## Materializing strings

Delphi `string` (UnicodeString) is a pointer to a heap-allocated
buffer with metadata at negative offsets. `inspect_object`
shows you the field as `address: 0x...`; to read the actual
text:

```
read(kind="string", address=0x..., string_kind="unicode")
```

Returns `{kind, length, code_page, elem_size, text}`. `string_kind`:

- `"unicode"` — `UnicodeString`/`WideString` (the default in
  modern Delphi).
- `"ansi"` — `AnsiString`.
- `"short"` — `ShortString` (length-prefixed, max 255 bytes).

## Materializing dynamic arrays

```
read(kind="dyn_array", address=0x..., element_size=4, max_elements=64)
```

Returns the raw bytes. Decoding (interpreting bytes as your
target type) is your job — use `inspect_object` first if the
elements are themselves objects.

## Walking nested objects

Common pattern: the user wants to know the `Caption` of a
button on a form. From `get_self()` (you're in the form's event
handler):

```
self = get_self()
# self.fields includes 'Components: TList' or 'Controls: TList'
components = self.fields["Components"]   # an address
list = inspect_object(components, max_depth=2)
# list.fields["FList"] is a TList, with .FCount and .FItems
# .FItems[i] gives addresses of each component
btn = inspect_object(<component_address>)
caption = read(kind="string", address=btn.fields["FCaption"])
```

For VCL trees specifically, prefer `enumerate_vcl_tree` — it
encapsulates this walk in a single call.

## Pitfalls

- **Stale pointers.** If the target is running and you walk
  through addresses, the heap may shift under you. Inspect only
  while paused.
- **Encrypted/packed fields.** If `inspect_object` returns
  garbage strings for `Caption`, the control may use a custom
  getter (e.g. localized strings). Try `read` directly on the
  raw field address, then check the control's `getter` method
  via `methods`.
- **`max_depth` boundaries.** A field at the boundary shows only
  the address, not the pointee. Re-call `inspect_object` on that
  address to recurse.
