diff --git a/Docs/Better-Debugging-With-LLDB.md b/Docs/Better-Debugging-With-LLDB.md new file mode 100644 index 00000000..d6ea9602 --- /dev/null +++ b/Docs/Better-Debugging-With-LLDB.md @@ -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("")` 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. diff --git a/Integration.code-workspace.tpl.json b/Integration.code-workspace.tpl.json index b723443e..e7d36a69 100644 --- a/Integration.code-workspace.tpl.json +++ b/Integration.code-workspace.tpl.json @@ -123,7 +123,7 @@ "type": "lldb", "console": "integratedTerminal", "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", "process handle SIGTRAP --notify false --pass false --stop false", "target stop-hook add --one-liner \"p ::UngrabAllInputImpl()\"" @@ -146,7 +146,7 @@ "type": "lldb", "console": "integratedTerminal", "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" ] }, diff --git a/tools/UEDataFormatter.py b/tools/UEDataFormatter.py new file mode 100644 index 00000000..98096672 --- /dev/null +++ b/tools/UEDataFormatter.py @@ -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 "" % 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 "" % (p, val.GetName(), val.GetType().GetName()) + val = child + return val + +import builtins +builtins.fv = fv +