Trying to improve lldb

This commit is contained in:
2026-04-19 05:03:11 -04:00
parent dabb5b8f0b
commit 275698c5aa
3 changed files with 946 additions and 2 deletions

View File

@@ -0,0 +1,188 @@
# 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.

View File

@@ -123,7 +123,7 @@
"type": "lldb", "type": "lldb",
"console": "integratedTerminal", "console": "integratedTerminal",
"initCommands": [ "initCommands": [
"command script import [UNREALENGINE]/Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py", "command script import [INTEGRATION]/tools/UEDataFormatter.py",
"settings set target.inline-breakpoint-strategy always", "settings set target.inline-breakpoint-strategy always",
"process handle SIGTRAP --notify false --pass false --stop false", "process handle SIGTRAP --notify false --pass false --stop false",
"target stop-hook add --one-liner \"p ::UngrabAllInputImpl()\"" "target stop-hook add --one-liner \"p ::UngrabAllInputImpl()\""
@@ -146,7 +146,7 @@
"type": "lldb", "type": "lldb",
"console": "integratedTerminal", "console": "integratedTerminal",
"initCommands": [ "initCommands": [
"command script import [UNREALENGINE]/Engine/Extras/LLDBDataFormatters/UEDataFormatters_2ByteChars.py", "command script import [INTEGRATION]/tools/UEDataFormatter.py",
"settings set target.inline-breakpoint-strategy always" "settings set target.inline-breakpoint-strategy always"
] ]
}, },

756
tools/UEDataFormatter.py Normal file
View File

@@ -0,0 +1,756 @@
# Copyright Epic Games, Inc. All Rights Reserved.
# /*=============================================================================
# LLDB Data Formatters for Unreal Types
# =============================================================================*/
import lldb
import lldb.formatters.Logger
# Uncomment the line below to have the data formatters emit debug logging
# lldb.formatters.Logger._lldb_formatters_debug_level=1
# What documentation there is for parsing values in LLDB can be found here:
# https://lldb.llvm.org/python_reference/index.html
# https://lldb.llvm.org/python_reference/lldb.SBValue-class.html
# To install:
# 1) Open Terminal and run:
# touch ~/.lldbinit
# open ~/.lldbinit
# 2) Add the following text to .lldbini and save - modifying the path as appropriate:
# settings set target.inline-breakpoint-strategy always
# command script import "/Path/To/Epic/UE/Engine/Extras/LLDBDataFormatters/UEDataFormatter.py"
# 3) Restart Xcode
def UETCharSummaryProvider(valobj,dict):
Data = valobj.GetValue()
Val = valobj.GetSummary()
Type = valobj.GetType().GetUnqualifiedType()
if Type.IsPointerType():
DataVal = valobj.GetValueAsUnsigned(0)
if DataVal == 0:
Val = 'NULL'
else:
Expr = '(char16_t*)(%s)' % DataVal
ValRef = valobj.CreateValueFromExpression('string', Expr)
Val = ValRef.GetSummary()
elif Type.IsReferenceType():
Expr = '(char16_t&)(%s)' % Data
ValRef = valobj.CreateValueFromExpression('string', Expr)
Val = ValRef.GetSummary()
elif Type.IsArrayType():
DataVal = valobj.GetChildAtIndex(0).GetValueAsUnsigned(0)
if DataVal == 0:
Val = 'NULL'
else:
Expr = '(char16_t*)(%s)' % valobj.GetAddress()
ValRef = valobj.CreateValueFromExpression('string', Expr)
Val = ValRef.GetSummary()
else:
DataVal = valobj.GetValueAsUnsigned(0)
Expr = '(char16_t)(%s)' % DataVal
ValRef = valobj.CreateValueFromExpression('string', Expr)
Val = ValRef.GetSummary()
return Val
def UESignedCharSummaryProvider(valobj,dict):
Data = valobj.GetValue()
Val = valobj.GetSummary()
Type = valobj.GetType().GetUnqualifiedType()
DataVal = valobj.GetValueAsUnsigned(0)
if DataVal == 0:
Val = 'NULL'
else:
Expr = '(char*)(%s)' % Data
ValRef = valobj.CreateValueFromExpression('string', Expr)
Val = ValRef.GetSummary()
return Val
def UEFStringSummaryProvider(valobj,dict):
Data = valobj.GetChildMemberWithName('Data')
ArrayNumVal = Data.GetChildMemberWithName('ArrayNum').GetValueAsSigned(0)
if ArrayNumVal < 0:
return 'string=Invalid'
elif ArrayNumVal == 0:
return 'string=Empty'
else:
AllocatorInstance = Data.GetChildMemberWithName('AllocatorInstance')
ActualData = AllocatorInstance.GetChildMemberWithName('Data')
Expr = '(char16_t*)(%s)' % ActualData.GetValue()
ValRef = valobj.CreateValueFromExpression('string', Expr)
Val = ValRef.GetSummary()
return 'string=' + Val
def UEFNameIndexToEntry(EntryId):
Index = EntryId.GetChildMemberWithName('Value').GetValueAsUnsigned(0)
# FNameDebugVisualizer::OffsetBits = 16
# FNameDebugVisualizer::OffsetMask = (1 << OffsetBits) - 1 = 65535
NameEntryExpr = '(FNameEntry*)(GNameBlocksDebug['+str(Index)+' >> 16]+((alignof(FNameEntry) * ('+str(Index)+' & 65535))))'
NameEntry = EntryId.CreateValueFromExpression('NameEntry', NameEntryExpr)
return NameEntry
def UEFNameEntrySummaryProvider(valobj,dict):
Header = valobj.GetChildMemberWithName('Header')
Len = min(Header.GetChildMemberWithName('Len').GetValueAsUnsigned(0), 1023)
if Len == 0:
UnderlyingId = valobj.GetValueForExpressionPath('.NumberedName.Id')
Number = valobj.GetValueForExpressionPath('.NumberedName.Number').GetValueAsUnsigned(0)
BaseNameEntry = UEFNameIndexToEntry(UnderlyingId)
BaseName = UEFNameEntrySummaryProvider(BaseNameEntry, dict)
return '%s_%s' % (BaseName, Number-1) # BaseName will already include name= from recursion
else:
DataPtr = valobj.GetChildAtIndex(1).AddressOf().GetValueAsUnsigned(0)
IsWide = Header.GetChildMemberWithName('bIsWide').GetValueAsUnsigned(0)
if IsWide:
DataPtr = valobj.GetValueForExpressionPath(".WideName").AddressOf().GetValueAsUnsigned(0)
SizeOfTChar = valobj.CreateValueFromExpression('size', 'sizeof(TCHAR))').GetValueAsUnsigned(0)
Encoding = "utf-16" if SizeOfTChar == 2 else "utf-32"
NumBytes = Len * SizeOfTChar
Data = valobj.process.ReadMemory(DataPtr,NumBytes,lldb.SBError())
Name = Data.decode(Encoding).encode("utf-8")
else:
DataPtr = valobj.GetValueForExpressionPath(".AnsiName").AddressOf().GetValueAsUnsigned(0)
Name = valobj.process.ReadMemory(DataPtr,Len,lldb.SBError())
return 'name=%s' % Name
def UEFNameSummaryProvider(valobj,dict):
EntryId = valobj.GetChildMemberWithName('DisplayIndex')
if not EntryId.IsValid():
EntryId = valobj.GetChildMemberWithName('ComparisonIndex')
if not EntryId.IsValid():
EntryId = valobj.GetChildMemberWithName('Index')
NameEntry = UEFNameIndexToEntry(EntryId)
Number = valobj.GetChildMemberWithName('Number')
if not Number.IsValid():
return UEFNameEntrySummaryProvider(NameEntry, dict)
else:
NameStr = UEFNameEntrySummaryProvider(NameEntry, dict)
if Number.GetValueAsUnsigned(0) != 0:
return "'%s'_%d" % (NameStr, Number-1)
else:
return "'%s'" % NameStr
def UEUObjectBaseSummaryProvider(valobj,dict):
Name = valobj.GetChildMemberWithName('NamePrivate')
return Name.GetSummary()
def UEFFieldClassSummaryProvider(valobj,dict):
Name = valobj.GetChildMemberWithName('Name')
return Name.GetSummary()
def UEFFieldSummaryProvider(valobj,dict):
Name = valobj.GetChildMemberWithName('NamePrivate')
return Name.GetSummary()
class UETWeakObjectPtrSynthProvider:
def __init__(self, valobj, dict):
logger = lldb.formatters.Logger.Logger()
self.valobj = valobj
def num_children(self):
logger = lldb.formatters.Logger.Logger()
return 1
def get_child_index(self,name):
logger = lldb.formatters.Logger.Logger()
return 0
def get_child_at_index(self,index):
logger = lldb.formatters.Logger.Logger()
logger >> "Retrieving child %s" % index
if self.ObjectSerialNumberVal >= 1:
Expr = 'GObjectArrayForDebugVisualizers->Objects[%s][%s].SerialNumber == %s' % (int(self.ObjectIndexVal/65536), self.ObjectIndexVal%65536, self.ObjectSerialNumberVal)
Val = self.valobj.CreateValueFromExpression("%s" % self.ObjectIndexVal, Expr)
Value = Val.GetValueAsUnsigned(0)
if Value != 0:
Expr = 'GObjectArrayForDebugVisualizers->Objects[%s][%s].Object' % (int(self.ObjectIndexVal/65536), self.ObjectIndexVal%65536)
return self.valobj.CreateValueFromExpression('Object', Expr)
else:
Expr = '(void*)0xDEADBEEF'
return self.valobj.CreateValueFromExpression('Object', Expr)
Expr = 'nullptr'
return self.valobj.CreateValueFromExpression('Object', Expr)
def update(self):
logger = lldb.formatters.Logger.Logger()
try:
self.ObjectSerialNumber = self.valobj.GetChildMemberWithName('ObjectSerialNumber')
self.ObjectSerialNumberVal = self.ObjectSerialNumber.GetValueAsSigned(0)
self.ObjectIndex = self.valobj.GetChildMemberWithName('ObjectIndex')
self.ObjectIndexVal = self.ObjectIndex.GetValueAsSigned(0)
except:
pass
def has_children(self):
return True
def UEFWeakObjectPtrSummaryProvider(valobj,dict):
ObjectSerialNumber = valobj.GetChildMemberWithName('ObjectSerialNumber')
ObjectSerialNumberVal = ObjectSerialNumber.GetValueAsSigned(0)
if ObjectSerialNumberVal < 1:
return 'object=nullptr'
ObjectIndex = valobj.GetChildMemberWithName('ObjectIndex')
ObjectIndexVal = ObjectIndex.GetValueAsSigned(0)
Expr = 'GObjectArrayForDebugVisualizers->Objects[%s][%s].SerialNumber == %s' % (int(ObjectIndexVal/65536), ObjectIndexVal%65536, ObjectSerialNumberVal)
Val = valobj.CreateValueFromExpression('%s' % ObjectIndexVal, Expr)
ValRef = Val.GetValueAsUnsigned(0)
if ValRef == 0:
return 'object=STALE'
else:
Expr = 'GObjectArrayForDebugVisualizers->Objects[%s][%s].Object' % (int(ObjectIndexVal/65536), ObjectIndexVal%65536)
Val = valobj.CreateValueFromExpression("%s" % ObjectIndexVal, Expr)
return 'object=' + Val.GetValue()
class UEChunkedArraySynthProvider:
def __init__(self, valobj, dict):
logger = lldb.formatters.Logger.Logger()
self.valobj = valobj
def num_children(self):
logger = lldb.formatters.Logger.Logger()
try:
NumElementsVal = self.NumElements.GetValueAsSigned(0)
return NumElementsVal;
except:
return 0;
def get_child_index(self,name):
logger = lldb.formatters.Logger.Logger()
try:
return int(name.lstrip('[').rstrip(']'))
except:
return None
def get_child_at_index(self,index):
logger = lldb.formatters.Logger.Logger()
logger >> "Retrieving child %s" % index
Expr = '(unsigned)sizeof(%s::FChunk)/%s' % (self.valobj.GetType().GetUnqualifiedType().GetName(), self.ElementTypeSize)
self.ChunkBytes = self.valobj.CreateValueFromExpression('[%s]' % index, Expr)
self.ChunkBytesSize = self.ChunkBytes.GetValueAsUnsigned(0)
assert self.ChunkBytesSize != 0
Expr = '*(*(((%s**)%s)+%s)+%s)' % (self.ElementType.GetName(), self.AllocatorData.GetValue(), index / self.ChunkBytesSize, index % self.ChunkBytesSize)
return self.valobj.CreateValueFromExpression('[%s]' % index, Expr)
def extract_type(self):
logger = lldb.formatters.Logger.Logger()
ArrayType = self.valobj.GetType().GetUnqualifiedType()
if ArrayType.IsReferenceType():
ArrayType = ArrayType.GetDereferencedType()
elif ArrayType.IsPointerType():
ArrayType = ArrayType.GetPointeeType()
if ArrayType.GetNumberOfTemplateArguments() > 0:
ElementType = ArrayType.GetTemplateArgumentType(0)
else:
ElementType = None
return ElementType
def update(self):
logger = lldb.formatters.Logger.Logger()
try:
self.ElementType = self.extract_type()
self.ElementTypeSize = self.ElementType.GetByteSize()
assert self.ElementTypeSize != 0
self.NumElements = self.valobj.GetChildMemberWithName('NumElements')
self.Chunks = self.valobj.GetChildMemberWithName('Chunks')
self.ArrayNum = self.Chunks.GetChildMemberWithName('ArrayNum')
self.AllocatorInstance = self.Chunks.GetChildMemberWithName('AllocatorInstance')
self.AllocatorData = self.AllocatorInstance.GetChildMemberWithName('Data')
except:
pass
def has_children(self):
return True
def UEChunkedArraySummaryProvider(valobj,dict):
return 'size=%s' % valobj.GetNumChildren()
class UESparseArraySynthProvider:
def __init__(self, valobj, dict):
logger = lldb.formatters.Logger.Logger()
self.valobj = valobj
def num_children(self):
logger = lldb.formatters.Logger.Logger()
try:
NumBitsVal = self.NumFreeIndices.GetValueAsSigned(0)
ArrayNumVal = self.Data.GetChildMemberWithName('ArrayNum').GetValueAsSigned(0)
return ArrayNumVal - NumBitsVal;
except:
return 0;
def get_child_index(self,name):
logger = lldb.formatters.Logger.Logger()
try:
return int(name.lstrip('[').rstrip(']'))
except:
return None
def get_child_at_index(self,index):
logger = lldb.formatters.Logger.Logger()
logger >> "Retrieving child %s" % index
if index < 0:
return None;
if index >= self.num_children():
return None;
Val = None
if self.SecondaryDataDataVal > 0:
Expr = '(bool)((((int*)%s)[%s/32] >> %s) & 1)' % (self.SecondaryDataData.GetAddress(), index, index)
Val = self.SecondaryDataData.CreateValueFromExpression('[%s]' % index, Expr)
else:
Expr = '(bool)((((int*)%s)[%s/32] >> %s) & 1)' % (self.InlineData.GetAddress(), index, index)
Val = self.InlineData.CreateValueFromExpression('[5s]' % index, Expr)
if Val.GetValueAsSigned(0) != 0:
offset = index * self.ElementTypeSize
return self.AllocatorData.CreateChildAtOffset('[%s]' % index, offset, self.ElementType)
else:
return None
def extract_type(self):
logger = lldb.formatters.Logger.Logger()
ArrayType = self.valobj.GetType().GetUnqualifiedType()
if ArrayType.IsReferenceType():
ArrayType = ArrayType.GetDereferencedType()
elif ArrayType.IsPointerType():
ArrayType = ArrayType.GetPointeeType()
if ArrayType.GetNumberOfTemplateArguments() > 0:
ElementType = ArrayType.GetTemplateArgumentType(0)
else:
ElementType = None
return ElementType
def update(self):
logger = lldb.formatters.Logger.Logger()
try:
self.ElementType = self.extract_type()
self.ElementTypeSize = self.ElementType.GetByteSize()
assert self.ElementTypeSize != 0
self.NumFreeIndices = self.valobj.GetChildMemberWithName('NumFreeIndices')
self.Data = self.valobj.GetChildMemberWithName('Data')
self.AllocatorInstance = self.Data.GetChildMemberWithName('AllocatorInstance')
self.AllocatorData = self.AllocatorInstance.GetChildMemberWithName('Data')
self.AllocationFlags = self.valobj.GetChildMemberWithName('AllocationFlags')
self.InlineData = self.AllocationFlags.GetChildMemberWithName('InlineData')
self.SecondaryData = self.AllocationFlags.GetChildMemberWithName('SecondaryData')
self.SecondaryDataData = self.SecondaryData.GetChildMemberWithName('Data')
self.SecondaryDataDataVal = self.SecondaryDataData.GetValueAsSigned(0)
except:
pass
def has_children(self):
return True
def UESparseArraySummaryProvider(valobj,dict):
return 'size=%s' % valobj.GetNumChildren()
class UEBitArraySynthProvider:
def __init__(self, valobj, dict):
logger = lldb.formatters.Logger.Logger()
self.valobj = valobj
def num_children(self):
logger = lldb.formatters.Logger.Logger()
try:
NumBitsVal = self.NumBits.GetValueAsSigned(0)
return NumBitsVal;
except:
return 0;
def get_child_index(self,name):
logger = lldb.formatters.Logger.Logger()
try:
return int(name.lstrip('[').rstrip(']'))
except:
return None
def get_child_at_index(self,index):
logger = lldb.formatters.Logger.Logger()
logger >> "Retrieving child %s" % index
if index < 0:
return None;
if index >= self.num_children():
return None;
if self.SecondaryDataDataVal > 0:
Expr = '(bool)((((int*)%s)[%s/32] >> %s) & 1)' % (self.SecondaryDataData.GetAddress(), index, index)
return self.SecondaryDataData.CreateValueFromExpression('[%s]' % index, Expr)
else:
Expr = '(bool)((((int*)%s)[%s/32] >> %s) & 1)' % (self.InlineData.GetAddress(), index, index)
return self.InlineData.CreateValueFromExpression('[%s]' % index, Expr)
return None
def update(self):
logger = lldb.formatters.Logger.Logger()
try:
self.SecondaryDataData = None
self.SecondaryDataDataVal = 0
self.NumBits = self.valobj.GetChildMemberWithName('NumBits')
self.MaxBits = self.valobj.GetChildMemberWithName('MaxBits')
self.InlineData = self.valobj.GetChildMemberWithName('InlineData')
self.SecondaryData = self.valobj.GetChildMemberWithName('SecondaryData')
if self.SecondaryData != None:
self.SecondaryDataData = self.SecondaryData.GetChildMemberWithName('Data')
self.SecondaryDataDataVal = self.SecondaryDataData.GetValueAsUnsigned(0)
except:
pass
def has_children(self):
return True
def UEBitArraySummaryProvider(valobj,dict):
return 'size=%s' % valobj.GetNumChildren()
class UEArraySynthProvider:
def __init__(self, valobj, dict):
logger = lldb.formatters.Logger.Logger()
self.valobj = valobj
def num_children(self):
logger = lldb.formatters.Logger.Logger()
try:
ArrayNumVal = self.ArrayNum.GetValueAsSigned(0)
return ArrayNumVal + self.NumChildren;
except:
return 0;
def get_child_index(self,name):
logger = lldb.formatters.Logger.Logger()
try:
return self.NumChildren + int(name.lstrip('[').rstrip(']'))
except:
return self.valobj.GetIndexOfChildWithName(name)
def get_child_at_index(self,index):
logger = lldb.formatters.Logger.Logger()
logger >> "Retrieving child %s" % index
if index < 0:
return None;
if index < self.NumChildren:
return self.valobj.GetChildAtIndex(index)
else:
index -= self.NumChildren
if index >= self.num_children():
return None;
try:
offset = index * self.ElementTypeSize
if self.Data != None:
return self.Data.CreateChildAtOffset('[%s]' % index, offset, self.ElementType)
elif self.SecondaryDataDataVal > 0:
return self.SecondaryDataData.CreateChildAtOffset('[%s]' % index, offset, self.ElementType)
else:
return self.InlineData.CreateChildAtOffset('[%s]' % index, offset, self.ElementType)
except:
return None
def extract_type(self):
logger = lldb.formatters.Logger.Logger()
ArrayType = self.valobj.GetType().GetUnqualifiedType()
if ArrayType.IsReferenceType():
ArrayType = ArrayType.GetDereferencedType()
elif ArrayType.IsPointerType():
ArrayType = ArrayType.GetPointeeType()
if ArrayType.GetNumberOfTemplateArguments() > 0:
ElementType = ArrayType.GetTemplateArgumentType(0)
else:
ElementType = None
return ElementType
def update(self):
logger = lldb.formatters.Logger.Logger()
try:
self.NumChildren = self.valobj.GetNumChildren()
self.ArrayNum = self.valobj.GetChildMemberWithName('ArrayNum')
self.ArrayMax = self.valobj.GetChildMemberWithName('ArrayMax')
self.AllocatorInstance = self.valobj.GetChildMemberWithName('AllocatorInstance')
if self.AllocatorInstance.GetType().IsReferenceType():
self.AllocatorInstance = self.AllocatorInstance.Dereference()
self.Data = self.AllocatorInstance.GetChildMemberWithName('Data')
self.InlineData = self.AllocatorInstance.GetChildMemberWithName('InlineData')
self.SecondaryData = self.AllocatorInstance.GetChildMemberWithName('SecondaryData')
if self.SecondaryData != None:
self.SecondaryDataData = self.SecondaryData.GetChildMemberWithName('Data')
self.SecondaryDataDataVal = self.SecondaryDataData.GetValueAsUnsigned(0)
else:
self.SecondaryDataData = None
self.SecondaryDataDataVal = 0
except:
logger >> "UEArraySynthProvider::update failed accessing members"
pass
try:
self.ElementType = self.extract_type()
self.ElementTypeSize = self.ElementType.GetByteSize()
assert self.ElementTypeSize != 0
except:
logger >> "UEArraySynthProvider::update failed accessing element type"
pass
def has_children(self):
return True
def UEArraySummaryProvider(valobj,dict):
return 'size=%s' % valobj.GetChildMemberWithName('ArrayNum').GetValueAsSigned(0)
class UESetSynthProvider:
def __init__(self, valobj, dict):
logger = lldb.formatters.Logger.Logger()
self.valobj = valobj
# Can't cast to TSetElement - the template instantiation won't allow it
Expr = '(size_t)sizeof(FSetElementId) + sizeof(int32)'
self.TSetElement = self.valobj.CreateValueFromExpression('TSetElement', Expr)
def num_children(self):
logger = lldb.formatters.Logger.Logger()
try:
ArrayNumVal = self.ArrayNum.GetValueAsUnsigned(0)
NumFreeIndicesVal = self.NumFreeIndices.GetValueAsUnsigned(0)
return ArrayNumVal - NumFreeIndicesVal;
except:
return 0;
def get_child_index(self,name):
logger = lldb.formatters.Logger.Logger()
try:
return int(name.lstrip('[').rstrip(']'))
except:
return 0
def get_child_at_index(self,index):
logger = lldb.formatters.Logger.Logger()
logger >> "Retrieving child %s" % index
if index < 0:
return None;
if index >= self.num_children():
return None;
try:
offset = index * self.ElementTypeSize
HasObject = 0
if self.SecondaryDataDataVal > 0:
Expr = '(bool)((((int*)%s)[%s/32] >> %s) & 1)' % (self.SecondaryDataDataVal, index, index)
HasObject = 1 ##self.AllocationFlagsSecondaryDataData.CreateValueFromExpression('[%s]' % index, Expr).GetValueAsUnsigned(0)
else:
Expr = '(bool)((((int*)%s)[%s/32] >> %s) & 1)' % (self.AllocationFlagsInlineDataAddr, index, index)
HasObject = 1 ##self.AllocationFlagsInlineData.CreateValueFromExpression('[%s]' % index, Expr).GetValueAsUnsigned(0)
if HasObject == 1:
return self.AllocatorInstanceData.CreateChildAtOffset('[%s]' % index, offset, self.ElementType)
else:
return self.valobj.CreateValueFromExpression('[%s]' % index, '(void*)0xDEADBEEF')
except:
return None
def extract_type(self):
logger = lldb.formatters.Logger.Logger()
ArrayType = self.valobj.GetType().GetUnqualifiedType()
if ArrayType.IsReferenceType():
ArrayType = ArrayType.GetDereferencedType()
elif ArrayType.IsPointerType():
ArrayType = ArrayType.GetPointeeType()
if ArrayType.GetNumberOfTemplateArguments() > 0:
ElementType = ArrayType.GetTemplateArgumentType(0)
else:
ElementType = None
return ElementType
def update(self):
logger = lldb.formatters.Logger.Logger()
try:
self.Elements = self.valobj.GetChildMemberWithName('Elements')
self.ElementsData = self.Elements.GetChildMemberWithName('Data')
self.ArrayNum = self.ElementsData.GetChildMemberWithName('ArrayNum')
self.NumFreeIndices = self.Elements.GetChildMemberWithName('NumFreeIndices')
self.AllocationFlags = self.Elements.GetChildMemberWithName('AllocationFlags')
self.AllocationFlagsAllocatorInstance = self.AllocationFlags.GetChildMemberWithName('AllocatorInstance')
self.AllocatorInstance = self.ElementsData.GetChildMemberWithName('AllocatorInstance')
self.AllocatorInstanceData = self.AllocatorInstance.GetChildMemberWithName('Data')
self.AllocationFlagsInlineData = self.AllocationFlagsAllocatorInstance.GetChildMemberWithName('InlineData')
self.AllocationFlagsInlineDataAddr = self.AllocationFlagsInlineData.AddressOf().GetValueAsUnsigned(0)
self.AllocationFlagsSecondaryData = self.AllocationFlagsAllocatorInstance.GetChildMemberWithName('SecondaryData')
self.AllocationFlagsSecondaryDataData = self.AllocationFlagsSecondaryData.GetChildMemberWithName('Data')
self.SecondaryDataDataVal = self.AllocationFlagsSecondaryDataData.GetValueAsUnsigned(0)
self.ElementType = self.extract_type()
# This may fail due to C++ struct padding - will have to check
self.ElementTypeSize = self.ElementType.GetByteSize() + self.TSetElement.GetValueAsUnsigned(0)
assert self.ElementTypeSize != 0
except:
pass
def has_children(self):
return True
def UESetSummaryProvider(valobj,dict):
return 'size=%s' % valobj.GetNumChildren()
class UEMapSynthProvider:
def __init__(self, valobj, dict):
logger = lldb.formatters.Logger.Logger()
self.valobj = valobj
# Can't cast to TSetElement - the template instantiation won't allow it
Expr = '(size_t)sizeof(FSetElementId) + sizeof(int32)'
self.TSetElement = self.valobj.CreateValueFromExpression('TSetElement', Expr)
def num_children(self):
logger = lldb.formatters.Logger.Logger()
try:
ArrayNumVal = self.ArrayNum.GetValueAsUnsigned(0)
NumFreeIndicesVal = self.NumFreeIndices.GetValueAsUnsigned(0)
return ArrayNumVal - NumFreeIndicesVal;
except:
return 0;
def get_child_index(self,name):
logger = lldb.formatters.Logger.Logger()
try:
return int(name.lstrip('[').rstrip(']'))
except:
return 0
def get_child_at_index(self,index):
logger = lldb.formatters.Logger.Logger()
logger >> "Retrieving child %s" % index
if index < 0:
return None;
if index >= self.num_children():
return None;
try:
offset = index * self.ElementTypeSize
HasObject = 0
if self.SecondaryDataDataVal != 0:
Expr = '(bool)((((unsigned int*)%s)[%s/32] >> %s) & 1)' % (self.SecondaryDataDataVal, index, index)
HasObject = 1 ##self.AllocationFlagsSecondaryDataData.CreateValueFromExpression('[%s]' % index, Expr).GetValueAsUnsigned(0)
else:
Expr = '(bool)((((unsigned int*)%s)[%s/32] >> %s) & 1)' % (self.AllocationFlagsInlineDataAddr, index, index)
HasObject = 1 ##self.AllocationFlagsInlineData.CreateValueFromExpression('[%s]' % index, Expr).GetValueAsUnsigned(0)
if HasObject == 1:
return self.AllocatorInstanceData.CreateChildAtOffset('[%s]' % index,offset,self.ElementType)
else:
return self.valobj.CreateValueFromExpression('[%s]' % index, '(void*)0xDEADBEEF')
except:
return None
def extract_type(self):
logger = lldb.formatters.Logger.Logger()
ArrayType = self.Pairs.GetType().GetUnqualifiedType()
if ArrayType.IsReferenceType():
ArrayType = ArrayType.GetDereferencedType()
elif ArrayType.IsPointerType():
ArrayType = ArrayType.GetPointeeType()
if ArrayType.GetNumberOfTemplateArguments() > 0:
ElementType = ArrayType.GetTemplateArgumentType(0)
else:
ElementType = None
return ElementType
def update(self):
logger = lldb.formatters.Logger.Logger()
try:
self.Pairs = self.valobj.GetChildMemberWithName('Pairs')
self.Elements = self.Pairs.GetChildMemberWithName('Elements')
self.ElementsData = self.Elements.GetChildMemberWithName('Data')
self.ArrayNum = self.ElementsData.GetChildMemberWithName('ArrayNum')
self.NumFreeIndices = self.Elements.GetChildMemberWithName('NumFreeIndices')
self.AllocationFlags = self.Elements.GetChildMemberWithName('AllocationFlags')
self.AllocationFlagsAllocatorInstance = self.AllocationFlags.GetChildMemberWithName('AllocatorInstance')
self.AllocatorInstance = self.ElementsData.GetChildMemberWithName('AllocatorInstance')
self.AllocatorInstanceData = self.AllocatorInstance.GetChildMemberWithName('Data')
self.AllocationFlagsInlineData = self.AllocationFlagsAllocatorInstance.GetChildMemberWithName('InlineData')
self.AllocationFlagsInlineDataAddr = self.AllocationFlagsInlineData.AddressOf().GetValueAsUnsigned(0)
self.AllocationFlagsSecondaryData = self.AllocationFlagsAllocatorInstance.GetChildMemberWithName('SecondaryData')
self.AllocationFlagsSecondaryDataData = self.AllocationFlagsSecondaryData.GetChildMemberWithName('Data')
self.SecondaryDataDataVal = self.AllocationFlagsSecondaryDataData.GetValueAsUnsigned(0)
self.ElementType = self.extract_type()
# This may fail due to C++ struct padding - will have to check
self.ElementTypeSize = self.ElementType.GetByteSize() + self.TSetElement.GetValueAsUnsigned(0)
assert self.ElementTypeSize != 0
except:
pass
def has_children(self):
return True
def UEMapSummaryProvider(valobj,dict):
return 'size=%s' % valobj.GetNumChildren()
def __lldb_init_module(debugger,dict):
debugger.HandleCommand('type summary add -F UEDataFormatter.UETCharSummaryProvider -e TCHAR -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UESignedCharSummaryProvider -e "signed char *" -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEFStringSummaryProvider -e -x "FString$" -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEFNameEntrySummaryProvider -e -x "FNameEntry$" -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEFNameSummaryProvider -e -x "FName$" -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEFNameSummaryProvider -e -x "FMinimalName$" -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEUObjectBaseSummaryProvider -e UObject -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEUObjectBaseSummaryProvider -e UObjectBase -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEUObjectBaseSummaryProvider -e UObjectBaseUtility -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEFFieldClassSummaryProvider -e FFieldClass -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEFFieldSummaryProvider -e FField -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEFWeakObjectPtrSummaryProvider -e FWeakObjectPtr -w UEDataFormatters')
debugger.HandleCommand('type synthetic add -l UEDataFormatter.UETWeakObjectPtrSynthProvider -x "TWeakObjectPtr<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type synthetic add -l UEDataFormatter.UETWeakObjectPtrSynthProvider -x "TAutoWeakObjectPtr<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type synthetic add -l UEDataFormatter.UEArraySynthProvider -x "TArray<.+,.+>$" -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEArraySummaryProvider -e -x "TArray<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type synthetic add -l UEDataFormatter.UEBitArraySynthProvider -x "TBitArray<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEBitArraySummaryProvider -e -x "TBitArray<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type synthetic add -l UEDataFormatter.UESparseArraySynthProvider -x "TSparseArray<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UESparseArraySummaryProvider -e -x "TSparseArray<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type synthetic add -l UEDataFormatter.UEChunkedArraySynthProvider -x "TChunkedArray<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEChunkedArraySummaryProvider -e -x "TChunkedArray<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type synthetic add -l UEDataFormatter.UESetSynthProvider -x "TSet<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UESetSummaryProvider -e -x "TSet<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type synthetic add -l UEDataFormatter.UEMapSynthProvider -x "TMap<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEMapSummaryProvider -e -x "TMap<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type synthetic add -l UEDataFormatter.UEMapSynthProvider -x "TMapBase<.+>$" -w UEDataFormatters')
debugger.HandleCommand('type summary add -F UEDataFormatter.UEMapSummaryProvider -e -x "TMapBase<.+>$" -w UEDataFormatters')
debugger.HandleCommand("type category enable UEDataFormatters")
def fv(path):
"""Walk a dotted path through an SBValue tree, auto-dereffing pointers.
Usage in CodeLLDB Watch pane: /py fv("Widget.Object.SCompoundWidget.SWidget")
Uses GetChildMemberWithName, same as the Variables pane, so real members,
base classes, and synthetic children all work uniformly.
"""
frame = lldb.debugger.GetSelectedTarget().GetProcess().GetSelectedThread().GetSelectedFrame()
parts = path.split('.')
val = frame.FindVariable(parts[0])
if not val.IsValid():
return "<no variable '%s' in frame>" % parts[0]
for p in parts[1:]:
if val.GetType().IsPointerType():
val = val.Dereference()
child = val.GetChildMemberWithName(p)
if not child.IsValid():
for i in range(val.GetNumChildren()):
c = val.GetChildAtIndex(i)
if c.GetName() == p:
child = c
break
if not child.IsValid():
return "<no child '%s' in %s (%s)>" % (p, val.GetName(), val.GetType().GetName())
val = child
return val
import builtins
builtins.fv = fv