189 lines
6.1 KiB
Markdown
189 lines
6.1 KiB
Markdown
|
|
# Better Debugging With LLDB (in VS Code + CodeLLDB)
|
||
|
|
|
||
|
|
## The Problem
|
||
|
|
|
||
|
|
When debugging Unreal with VS Code + CodeLLDB, the **Variables** pane and
|
||
|
|
the **Watch** pane use two completely different evaluation paths:
|
||
|
|
|
||
|
|
- **Variables pane** walks a tree built by lldb's *synthetic children
|
||
|
|
providers* (including Unreal's Python formatters for TArray, TMap, FName,
|
||
|
|
FString, TSharedRef, etc.). Values are looked up by offset/type — no
|
||
|
|
compilation. Base classes appear as named children ("SCompoundWidget",
|
||
|
|
"UWidget"), smart-pointer inners get unwrapped, container elements get
|
||
|
|
indexed.
|
||
|
|
|
||
|
|
- **Watch pane** (and `p`/`expression`) runs your text through Clang to
|
||
|
|
compile a real C++ expression against the program's full type system,
|
||
|
|
then applies formatters to the result. For Unreal, this is slow and
|
||
|
|
often fails outright — synthetic children don't exist as C++ members.
|
||
|
|
|
||
|
|
The practical consequence: a path you see in the Variables pane — like
|
||
|
|
`Widget.Object.SCompoundWidget.SWidget.bCanSupportFocus` — cannot be
|
||
|
|
typed into the Watch pane, because the intermediate labels
|
||
|
|
(`SCompoundWidget`, `SWidget`) aren't real C++ members. Even "Copy as
|
||
|
|
Expression" in the Variables pane gives you that broken synthetic path.
|
||
|
|
|
||
|
|
Hand-writing the equivalent real C++ expression (with explicit
|
||
|
|
`->` for pointers, `static_cast`s through base classes, formatter-internal
|
||
|
|
array indexing math, etc.) is impractical.
|
||
|
|
|
||
|
|
## The Fix: A Python Helper That Walks Synthetic Children
|
||
|
|
|
||
|
|
We added a function `fv(path)` to `tools/UEDataFormatter.py`. It walks a
|
||
|
|
dotted path through the synthetic tree using the same APIs the Variables
|
||
|
|
pane uses (`GetChildMemberWithName`, then fallback to iterating children
|
||
|
|
by name so base classes match). Pointers are auto-dereferenced.
|
||
|
|
|
||
|
|
CodeLLDB's `/py` expression mode evaluates Python in the Watch pane and
|
||
|
|
renders returned `SBValue`s through the full formatter pipeline — same
|
||
|
|
expandable tree as Variables. So:
|
||
|
|
|
||
|
|
/py fv("Widget.Object.SCompoundWidget.SWidget")
|
||
|
|
|
||
|
|
works in Watch the way you'd originally expect `Widget.Object.SCompoundWidget.SWidget`
|
||
|
|
to work.
|
||
|
|
|
||
|
|
`fv` is also injected into Python builtins, so from the Debug Console
|
||
|
|
you can also just:
|
||
|
|
|
||
|
|
script fv("Widget.Object.SCompoundWidget.SWidget")
|
||
|
|
|
||
|
|
### How `fv` is loaded
|
||
|
|
|
||
|
|
The launch configurations in `Integration.code-workspace.tpl.json` run:
|
||
|
|
|
||
|
|
command script import /home/jyelon/integration/tools/UEDataFormatter.py
|
||
|
|
|
||
|
|
This both (a) loads all the Unreal data formatters and (b) defines `fv`
|
||
|
|
and injects it into builtins.
|
||
|
|
|
||
|
|
### Reloading without restarting the debug session
|
||
|
|
|
||
|
|
If you edit `tools/UEDataFormatter.py`, reload in the Debug Console:
|
||
|
|
|
||
|
|
script import importlib; importlib.reload(UEDataFormatter)
|
||
|
|
|
||
|
|
## The Remaining Gap: "Add to Watch"
|
||
|
|
|
||
|
|
VS Code's "Add to Watch" context menu copies the variable's `evaluateName`
|
||
|
|
as reported by the debug adapter. CodeLLDB sends the synthetic path — so
|
||
|
|
"Add to Watch" produces exactly the unusable expression that started this
|
||
|
|
whole problem.
|
||
|
|
|
||
|
|
The clean solution is a small VS Code extension that registers a new
|
||
|
|
command **Add to Watch (fv)** on the Variables pane context menu. When
|
||
|
|
invoked, it reads the variable's evaluateName and adds
|
||
|
|
`/py fv("<evaluateName>")` to the watch expressions.
|
||
|
|
|
||
|
|
### Scaffolding the extension
|
||
|
|
|
||
|
|
Create a folder, e.g. `tools/vscode-fv-watch/`:
|
||
|
|
|
||
|
|
```
|
||
|
|
tools/vscode-fv-watch/
|
||
|
|
package.json
|
||
|
|
src/extension.ts
|
||
|
|
tsconfig.json
|
||
|
|
```
|
||
|
|
|
||
|
|
**`package.json`** declares the contribution points — the command, and
|
||
|
|
its appearance in the Variables pane context menu:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"name": "fv-watch",
|
||
|
|
"version": "0.0.1",
|
||
|
|
"engines": { "vscode": "^1.80.0" },
|
||
|
|
"activationEvents": ["onDebug"],
|
||
|
|
"main": "./out/extension.js",
|
||
|
|
"contributes": {
|
||
|
|
"commands": [{
|
||
|
|
"command": "fv.addToWatch",
|
||
|
|
"title": "Add to Watch (fv)"
|
||
|
|
}],
|
||
|
|
"menus": {
|
||
|
|
"debug/variables/context": [{
|
||
|
|
"command": "fv.addToWatch",
|
||
|
|
"group": "3_modification@1"
|
||
|
|
}]
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"devDependencies": {
|
||
|
|
"@types/vscode": "^1.80.0",
|
||
|
|
"typescript": "^5.0.0"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**`src/extension.ts`** implements the command. The variable reference is
|
||
|
|
passed as the command argument; we pull its `evaluateName` and call the
|
||
|
|
built-in `debug.addToWatchExpressions` with the wrapped form:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
import * as vscode from 'vscode';
|
||
|
|
|
||
|
|
export function activate(ctx: vscode.ExtensionContext) {
|
||
|
|
ctx.subscriptions.push(vscode.commands.registerCommand(
|
||
|
|
'fv.addToWatch',
|
||
|
|
async (variable: any) => {
|
||
|
|
const name = variable?.evaluateName ?? variable?.name;
|
||
|
|
if (!name) return;
|
||
|
|
const expr = `/py fv(${JSON.stringify(name)})`;
|
||
|
|
await vscode.commands.executeCommand(
|
||
|
|
'debug.addToWatchExpressions',
|
||
|
|
{ variable: { evaluateName: expr } }
|
||
|
|
);
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function deactivate() {}
|
||
|
|
```
|
||
|
|
|
||
|
|
**`tsconfig.json`** is standard:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"compilerOptions": {
|
||
|
|
"module": "commonjs",
|
||
|
|
"target": "ES2020",
|
||
|
|
"outDir": "out",
|
||
|
|
"strict": true,
|
||
|
|
"esModuleInterop": true
|
||
|
|
},
|
||
|
|
"include": ["src"]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Building and installing
|
||
|
|
|
||
|
|
```
|
||
|
|
cd tools/vscode-fv-watch
|
||
|
|
npm install
|
||
|
|
npx tsc
|
||
|
|
ln -s "$PWD" ~/.vscode/extensions/fv-watch
|
||
|
|
```
|
||
|
|
|
||
|
|
Restart VS Code. Right-click any entry in the Variables pane and
|
||
|
|
**Add to Watch (fv)** appears alongside the built-in **Add to Watch**.
|
||
|
|
|
||
|
|
## Summary of Workflow
|
||
|
|
|
||
|
|
| Action | Where | How |
|
||
|
|
|---|---|---|
|
||
|
|
| Explore a value | Variables pane | Click disclosure triangles |
|
||
|
|
| Track a value across steps | Watch pane | "Add to Watch (fv)" from Variables context menu, or type `/py fv("...")` manually |
|
||
|
|
| One-shot inspection | Debug Console | `script fv("path")` |
|
||
|
|
| Reload formatter edits | Debug Console | `script import importlib; importlib.reload(UEDataFormatter)` |
|
||
|
|
|
||
|
|
## Why Not Make It Automatic?
|
||
|
|
|
||
|
|
The cleanest solution would be to change CodeLLDB's default Watch
|
||
|
|
evaluator to route through `frame variable` / synthetic children instead
|
||
|
|
of Clang. That's not exposed as a setting — CodeLLDB's `"expressions"`
|
||
|
|
option only chooses between its own parser, lldb's `expr`, or raw Python.
|
||
|
|
None of those hit the synthetic-children walk.
|
||
|
|
|
||
|
|
A feature request against CodeLLDB to add a "native frame-variable"
|
||
|
|
expression mode would address this at the source. In the meantime, the
|
||
|
|
`fv` helper + extension combo reproduces the missing behavior.
|