Engine overview

DebuggerCore is the part that would still be a debugger if both frontends were deleted: the Windows debug-event loop, the symbol readers, and the Pascal expression evaluator.

DebuggerCore

Three responsibilities, none of which know anything about DAP or MCP:

  • The debug-event loop — the Win32 debug API, process and thread events, module load and unload, exceptions.
  • Symbol readers for TD32, .map, .rsm, .dcp and JCL debug information. See Symbol formats.
  • The Pascal expression evaluator, which is what Watch, hover, the Debug Console, conditional breakpoints, logpoint substitutions and MCP evaluate_expression all go through — one implementation, not five.

IDebugTarget and the two engines

IDebugTarget is the interface the rest of the system talks to. Two classes implement it:

ClassTarget
TWinDebugger64-bit debuggee, debugged natively
TWin32Debugger32-bit debuggee, debugged across the WOW64 boundary

Which one is constructed is decided by reading the machine type from the target executable’s PE header, before the process is created. There is no setting, no probing of the running process, and no second adapter to install.

Once the process exists, its actual architecture is cross-checked against that decision. A disagreement produces an explicit [FATAL]-prefixed diagnostic rather than a session that misbehaves in ways that are hard to attribute.

Why the adapter is always a 64-bit process

The debug adapter itself is a 64-bit executable no matter what it debugs. Debugging a 32-bit target across WOW64 is more work than running a 32-bit adapter would be, and it is done deliberately, for one reason: symbol data is the memory hog, and it lives in the debugger’s address space, not the debuggee’s.

The scale is not theoretical. Indexing a 523 MB .rsm from a real single-EXE build peaks at 692 MB of working set. A 32-bit debugger has a 2 GB user address space to fit that into, alongside everything else it needs, and on a large Delphi project that is a wall you hit rather than approach. A 64-bit debugger does not compete with its debuggee for address space at all.

The visible consequence for a user is that there is one binary and nothing to choose at install time — see Preparing your project.

Constraints that follow from the design

  • One process per session. The debuggee is created with DEBUG_ONLY_THIS_PROCESS, so processes it spawns are not tracked.
  • Locals on 32-bit targets need -$O-. Not an engine choice — an optimized 32-bit build routinely has no frame pointer, and there is then nothing to resolve a local against. Run control is unaffected.
  • The whole process stops as a unit at a breakpoint or exception. Stepping is the one place threads are treated individually — see the threading model.

TDebugSession

Above IDebugTarget sits TDebugSession, the facade both frontends use. It owns the engine, the symbol providers, source-file resolution and the session state machine — so state handling exists once rather than twice, and a behaviour fixed for VS Code is fixed for an agent at the same time.

What each frontend adds on top is the next page.