Delphi Debugger (Win32 and Win64)Debug Delphi 32-bit and 64-bit applications directly in VS Code — no Embarcadero IDE required. 📖 Documentation, tutorial and feature tour → mcasoftware.devStart there. Installation, a first debug session, a 12-chapter VS Code tutorial, the MCP / AI-agent guide and the architecture notes, with screenshots. What follows here is the reference. This extension registers the The adapter is always a 64-bit process, whichever target it debugs. A 32-bit application is debugged across the WOW64 boundary, so the debugger neither shares nor inherits a 32-bit address space — which is where a large project's symbol data would otherwise run out of room. One binary handles both; the target's PE header decides, and there is nothing to configure. The original debug type name, With delphi-devkit (DDK)
installed, no
The same engine also talks to AI agents. This extension bundles an MCP
server whose tools let an agent set breakpoints, step, read locals and
evaluate expressions in a live Delphi process — the debugger's own
capabilities, driven by something other than a human. Agents hosted by VS Code
(1.101 or later) see it as FeaturesRun control
Breakpoints
Call stack and threads
Variables
Expression evaluation A Pascal expression evaluator backs Watch, hover data tips, the Debug Console (REPL) and breakpoint conditions:
Exceptions
Multi-module
Housekeeping
Raw stack scan: seeing past code you have no debug information forYou stop deep inside something you did not compile — an exception in the VCL, a callback out of a third-party control suite, an access violation under the RTL — and the call stack shows a couple of frames and stops. Everything below was built without debug information, or without a frame pointer, so there is nothing to unwind through. And the one thing worth knowing is exactly what is hidden: which of your own routines is underneath all that? This is the ordinary case on 32-bit targets, which carry no unwind data at all, so the walk ends at the first routine compiled without a frame pointer. It also happens on 64-bit in any application assembled largely from packages that were not built with debug information. Auto-Release of Frozen Threads (the button next to it) switches the
step-isolation deadlock detector for the rest of the session, and the button
itself shows the state: an open lock while auto-release is ON, a closed lock
while it is OFF, a crossed circle when the session never freezes threads at
all ( Press It takes effect on the stop you are already looking at. No restart, no editing
How much to trust a hitEvery appended entry is marked twice, because either marker on its own can be lost — the prefix survives a copy-paste into a bug report, the grey row is what you see at a glance:
Neither of them means the routine is still on the current chain. A call that has already returned leaves its return address behind on the stack, and no sweep can tell that apart from a live one. Read these as places the program has been — which is usually enough to answer "how did execution get here" — and never as callers. That is also why they are appended below the real stack instead of being mixed into it, and why the feature is off by default: a stack you did not ask to have swept must never show a position sitting next to a real frame. On 64-bit every hit is Requirements1. Compile the program you want to debug with debug information.
The Source lines have three independent sources, and any one of them is enough for
breakpoints and stepping: the TD32 2. Let something that knows your project describe it — one of three ways. The debugger needs the executable (or the host application of a package), its symbol files, the source search paths and the project's packages. A real Delphi project can carry a couple of hundred search paths; that configuration is not something anyone wants to write by hand. Any one of these supplies it:
3. Install DelphiLSP
( Windows x64 only. Getting startedDebugging a DDK projectWith delphi-devkit installed, a configuration names the project and nothing else:
Without DDKAdd a configuration to your project's
Press Launch properties
Source files are resolved by searching DLLs and runtime packagesThe adapter normally finds a module's debug info next to the loaded binary on disk. When it lives elsewhere, bind it explicitly:
Attach to a running processUse
The source,
Picking the process from a listHardcoding a PID means editing
A quick pick opens with the running processes — type part of the name, the PID or
the command line to filter. Candidates whose name matches the workspace folder
come first, then ordinary applications, with Windows infrastructure last.
Cancelling it ( Choosing between instances of one executableWhen you already know the executable, hunting for it among three hundred system
processes is busywork. Add
This works because VS Code passes the enclosing launch configuration to a
The name matches case-insensitively, with or without the To keep the filter out of the configuration's own properties — for instance to
pick an executable unrelated to
An explicit Where the list comes fromThe debug adapter builds it. That also means the picker knows each process's architecture, and shows it. Both 32-bit and 64-bit processes are attachable — the adapter is 64-bit and debugs a 32-bit target across the WOW64 boundary. A process the adapter has not been verified against (ARM64) is still listed, but marked with the adapter's own reason rather than offered as a candidate that fails on selection. If the adapter cannot be found or fails, the picker says so and the session is aborted. It never falls back to a second source. Attaching requires
Note that stopping the session while the target is paused at a breakpoint still resumes it before detaching, so the application continues from where it stopped. Exception handlingFiltersThe BREAKPOINTS view offers four filters:
|
| Field | Matches |
|---|---|
class |
Exact runtime class name, or an array of names (any-of), case-insensitive. Leaf only — does not match descendants |
classIs |
The runtime class or any ancestor (Delphi is semantics); name or array, case-insensitive |
code |
The Win32 exception code, or an array of codes (any-of). Hex ("0xC0000005", "$C0000005") or decimal (3221225477, signed -1073741819), string or number |
message |
Case-insensitive substring of the exception message |
messageRegex |
Regular expression against the message (ignore-case) |
unit |
Unit where the exception was raised. "*unknown*" matches raises with no resolvable source |
line |
Exact source line where raised |
lineFrom / lineTo |
Inclusive source-line range |
Unit and line refer to the raise site — the first stack frame with known
source, since a Delphi raise faults inside the RTL.
code is the only criterion that can match a native Windows exception (one
raised through RaiseException, not by a Delphi raise). Such an exception has
no exception object, therefore no class and no message, so class, classIs,
message and messageRegex can never match it — before code existed, the only
way to silence one was to switch the whole "all first-chance exceptions" filter
off:
"exceptionRules": [
{ "code": "0xE06D7363", "action": "ignore" }, // C++ exception from a C++ DLL
{ "code": "0xC0000094", "action": "logStack" } // integer divide by zero
]
The one native code you never need a rule for is 0x406D1388, the thread-name
announcement TThread.NameThreadForDebugging raises: it is debugger protocol
traffic and the adapter consumes it before the rules run.
Actions:
| Action | Effect |
|---|---|
ignore |
Resume immediately — no stop, no log |
log |
Write class: message to the Debug Console, then resume |
logStack |
Like log, plus the formatted call stack |
break |
Pause in the debugger |
The rules editor
Editing rule tables by hand in JSON gets old quickly, and order is semantic. Use:
Command Palette (Ctrl+Shift+P) → Delphi Debugger: Edit Exception Rules...
That route always works — no session, no open Delphi file, nothing else required. It is the one to reach for if you cannot find the button.
The same command is also a numbered-list icon (☰) in the title bar of the
BREAKPOINTS section, inside the Run and Debug view — not in the "RUN AND
DEBUG" header at the top of the panel, but in the header of the section itself,
where it appears on hover next to VS Code's own icons. It is on the CALL
STACK section header too, for when you are already in a session.
Two reasons it can be missing there while the palette command still works:
- the section is collapsed or was hidden through the panel's
...menu; - the panel is in its "simple" state — VS Code drops the Call Stack section entirely when no session is running and no launch configuration is selected. Breakpoints survives as soon as one breakpoint exists or you have debugged once in that workspace, which is why the button lives there as well.
First pick where the rules live. The list is ordered the way the debugger evaluates them — narrowest scope first:
<Project>.dprojrules (local) and<Project>.dprojrules (shared) — the two files belonging to the Delphi project the configuration debugs, described under Rules that belong to a project below. Offered as soon as a configuration names its project throughdelphiProjectFile;- Shared rules (all projects) — the machine-wide file described below.
Both are created, together with their directory, the first time you save. Where no configuration names a project, the shared file is the only target and is opened without asking.
Launch configurations are not in that list. exceptionRules inside
launch.json is no longer read at all: it made Debug X and Attach to X —
the same project, started two ways — able to disagree about which exceptions
matter, which is the problem the project files exist to solve.
The editor then opens with one line per rule, numbered in evaluation order: what the rule matches, and a badge for what it does. Click a line to open that rule's form, click it again to close it, or use Expand all. A rule form is ten fields of which a typical rule fills two, so a table of twenty rules stays readable instead of becoming several screens of empty inputs.
Open cards separate the match criteria from the action and carry up/down buttons to reorder. Rules are validated as you type — unknown fields, invalid actions, regular expressions that do not compile, inverted line ranges — saving is blocked while the table is invalid, and any rule with a problem opens itself so the complaint is never about something you cannot see.
Save replaces only the rule array of the selected target; every other byte of the file, comments included, is left untouched, and the shared file keeps its shape (a bare array stays a bare array, an object keeps its other keys). The file is left open and unsaved so you can review or undo before it hits disk. In an untrusted workspace the editor is read-only and offers Copy JSON instead.
Creating a rule from the exception you are looking at
While the debugger is stopped on an exception, a scales-of-justice button
appears in the floating debug toolbar, next to continue/step, and the command
Delphi Debugger: Create a Rule for This Exception... becomes available in the
Command Palette. Both disappear as soon as you resume or stop somewhere else —
the rule is about the exception you are looking at, so it is offered only while
you are looking at one.
It reads the current exception (class and message) and its raise site (unit and line from the top stack frame) and offers the decisions you actually want to make, pre-filled:
- ignore this class everywhere;
- ignore it when raised in this unit;
- ignore it at this exact unit and line;
- log it with its call stack instead of breaking;
- ignore it when the message contains this text;
- open the rules editor on a fully pre-filled rule, to adjust it first.
Then choose the target — one of the project's own files, the launch configuration, or the shared file. The new rule is inserted first, because matching is first-match-wins: a more specific rule appended at the bottom would never be reached. As with the editor, the file is left open and unsaved for review.
Rules that belong to a project
A launch configuration is the wrong home for a rule that belongs to a
package. If you maintain one .dpk inside a host application that loads
dozens of them, your rules have nothing to do with which host .exe anyone
launches to test it — and copying them into every configuration that starts that
host is how they go stale.
So a configuration may name the Delphi project it debugs:
"delphiProjectFile": "${workspaceFolder}/packages/ReportEngine.dpk"
The RAD Studio plugin writes this line for you when it generates the launch
configuration; there is nothing to maintain by hand. It may name the .dpr,
the .dpk or the .dproj — only the folder and the base name matter.
Two rule files next to that project then join the chain:
| File | Who it is for |
|---|---|
<Project>.ExceptionSettings.json |
the project's own rules, commit them — every developer on the package gets them |
<Project>.ExceptionSettings.local.json |
your own overrides, gitignore it |
Both have the same shape as the shared file (an object with an exceptionRules
array, or a bare array), are hot-reloaded on resume like it, and are ignored if
missing or malformed.
Without delphiProjectFile nothing project-scoped is looked for, and rules
resolve exactly as they did before this existed — an older launch.json keeps
working untouched.
Which rule wins
Rules are evaluated top-down across all four scopes, narrowest first, and the first match wins:
| # | Scope | Where |
|---|---|---|
| 1 | your own, for this project | <Project>.ExceptionSettings.local.json |
| 2 | the project's, shared with its team | <Project>.ExceptionSettings.json |
| 3 | one launch configuration | exceptionRules in launch.json |
| 4 | every project on this machine | the shared file below |
If nothing matches, the exception filters decide.
Shared rules across projects
A machine-wide baseline can live in
%USERPROFILE%\.DelphiWinDebugger\exceptionRules.json — a JSON object with an
exceptionRules array (a bare array is accepted too):
{
"exceptionRules": [
{ "class": "EAbort", "action": "ignore" },
{ "messageRegex": "ORA-\\d+", "action": "logStack" }
]
}
| Property | Description |
|---|---|
useGlobalExceptionRules |
Load the shared file. Default true |
globalExceptionRulesPath |
Use a different shared-rules file |
The shared file is the widest scope: everything in the table under Which rule wins is consulted before it. A missing or malformed shared file is ignored — it never breaks debugging. It is hot-reloaded, as are the project files: edit one while stopped, resume, and the new rules apply without restarting the session. Creating a file that was not there when the session started counts too.
The rules editor and the "create a rule for this exception" command can both
write to this file — pick Shared rules (all projects) when they ask for a
target. If a configuration in the workspace sets globalExceptionRulesPath, they
follow it rather than the default location.
Progress indicator
Symbol loading, expression evaluation and stepping can take a moment on large applications, so the adapter reports what it is doing.
progressLocation |
Behaviour |
|---|---|
statusBar (default) |
A spinner in the status bar, out of the way. Several concurrent operations collapse into one item with a +n counter and a tooltip listing them all |
notification |
Standard DAP progress events, which VS Code renders as notification toasts |
Only one mechanism is active at a time, so a DAP client other than this extension still gets standard progress events.
Debug information it reads
| Format | Where it comes from |
|---|---|
| TD32 | The .debug section embedded in the PE by the Delphi compiler — source lines, symbols, types |
.map |
Delphi MAP file — line numbers and symbol addresses |
.rsm |
Delphi remote-debug symbol file — types, locals, globals |
.dcp |
Delphi compiled package — locals for BPLs |
| JCL | JEDI Code Library debug data, either a linked JCLDEBUG PE section or a sidecar .jdbg — source lines and procedure names, so a module carrying it needs no .map beside it |
ASLR is handled: the running ImageBase is compared against the PE preferred
base at load time, so addresses resolve correctly for the executable and every
loaded module.
What are the .idx files next to my binaries?
MyApp.rsm.idx, MyPackage.dcp.idx and friends are symbol-index caches.
The first time the debugger reads a .rsm or .dcp it has to scan the whole
container to build a lookup index — about half a second for a 45 MB package —
and writes the result next to the file so every later session loads it in
milliseconds instead.
Safe to delete. The next session rebuilds the one it needs.
Build artifacts. Ignore them in version control, next to
.mapand.rsm:*.idx.Self-invalidating. A sidecar older than its source file, or written in an older index format, is ignored and rebuilt — a stale one is never used.
Can be built ahead of time.
DevTools\PrebuildIdx.exe <directory>indexes a whole directory offline, so the first debug session does not pay the scan:DevTools\Win64\Debug\PrebuildIdx.exe "C:\Users\Public\Documents\Embarcadero\Studio\23.0\Dcp\Win64" -j 2Worth doing for the RTL and third-party packages, which never change between your builds. Your own packages are recompiled constantly, which invalidates their sidecars by design.
Limitations
- 32-bit locals need an unoptimised build. Win32 locals and parameters are
supported for
-$O-builds;-$O+omits the frame pointer routinely. This is the usual Debug configuration, but it is a real constraint on an optimised build. (64-bit targets are not affected: they unwind from.pdata.) - 32-bit call stacks can stop short. 32-bit code carries no unwind data, so the stack is walked by the saved-EBP chain, and a routine built without a frame pointer sitting between two framed ones still hides its caller. The Toggle Raw Stack Scan button in the Call Stack title bar is the fallback — it reports positions on the stack, not callers.
- Single process. The debuggee is launched with
DEBUG_ONLY_THIS_PROCESS; child processes it spawns are not tracked. - No disassembly view.
- Variable inspection needs
.rsm/ TD32. With only a.mapfile you get breakpoints, stepping and a named call stack, but limited variable data. - If the stepped thread blocks on a lock held by another thread, the step cannot complete — the same constraint VS Code has with other debuggers.
Where the debugger's own logging goes
The Debug Console is for your program. Everything the debugger says about itself — symbol loading, module events, breakpoint binding, timings — goes to a separate channel instead:
Output panel (
Ctrl+Shift+U) → the dropdown on the right → Delphi Debugger
It used to be interleaved with the debuggee's own output, which made a
Writeln from your program hard to find among the debugger's chatter and vice
versa. They are now two panels and neither drowns the other.
The channel appears once a Delphi debug session has started — VS Code lists an Output channel only after something has created it, so an empty dropdown before the first session is expected rather than a fault.
For a much more verbose record, set "diagnosticLog": true in the launch
configuration; that writes every DAP request, response and debug event to
%TEMP%\dap_adapter.log. Leave it off for normal use.
Staying up to date
Installed from the Marketplace, the extension is updated by VS Code like any other, and nothing below applies: it never asks GitHub.
Installed from the setup zip (Setup.exe, or a .vsix installed by hand),
it is pinned: VS Code installed that exact file and never updates it, so nothing
in VS Code would tell you a new version exists — and in practice that means
people run an old one for months without knowing. To move such a copy to
Marketplace updates, uninstall it and install it again from the Marketplace.
So a pinned copy checks. Once a day at most, the extension asks GitHub for the
latest release of this project and, if it is newer than what you have, shows
one notification with a link to the download page. Which kind of install it is
comes from the extensions.json VS Code keeps beside its extensions; when that
cannot be read, the copy is treated as pinned.
What it does not do, deliberately:
- it never downloads or installs anything — the link opens the release page and you decide;
- it does not send anything about you, your code or your projects. The request is an anonymous read of a public releases endpoint;
- it stays silent when the check fails, when GitHub is unreachable, or when the answer is not newer. A version check that reports its own troubles is a nuisance;
- it does not nag: Skip this version is remembered, and the check is rate limited to once a day whatever happens.
Turn it off entirely with the setting delphi-win64.checkForUpdates
(Settings → Extensions → Delphi Debugger), or in settings.json:
"delphi-win64.checkForUpdates": false
Troubleshooting
Breakpoints do not bind, or land on the wrong line — the binary is usually
older than the source. Check that the .exe, .map and .rsm timestamps match
your last build, and rebuild.
Variables show garbage or "unavailable" — the target was probably compiled
with optimizations on, or without -V -VN -VR.
Something else — set "diagnosticLog": true in the launch configuration and
inspect %TEMP%\dap_adapter.log. It records every DAP request and response and
every debug event, so it is verbose; leave it off for normal use.
Related
Delphi IDE integration — see Requirements: the companion IDE plugin generates the workspace and launch configurations this debugger consumes. https://github.com/csm101/EditInVsCodeDelphiPlugin
Source code — https://github.com/csm101/delphi-visual-studio-code-debugger
License
MIT. See LICENSE in the repository.