This RFC proposes a change to LLDB’s mechanism of completing C/C++/Objective-C record types with the goal of getting rid of an entire class of bugs where Clang requires a definition, but LLDB wasn’t able to create one in time. Let us know if you have any concerns or questions about the outlined approach.
We revisit the refactor that was originally proposed with the draft patch in D101950.
Background
LLDB uses an embedded Clang compiler instance to keep track of and fulfil queries about types in the debugee. We constructs types, declarations and definitions by parsing debug information and creating corresponding Clang AST nodes. By default, we will create “forward declarations” for record types, and then create definitions (i.e., complete the types) on-demand only.
Frequently, queries about a type require LLDB to have completed it. E.g., “how many children does this type have?”, “is this type polymorphic?”, or “get me the second base-class of this type”. These queries could originate form anywhere in LLDB, including from a Python script through the SBAPI, a frame variable
command (via data formatters), formatting of the current frame status, or various parts of the expression evaluator (via the expression
command).
Current Type Completion Mechanism
The life of a record type in LLDB starts as a CompilerType
in the ResolveState::Forward
state. It’s essentially a clang::RecordType
, which depending on whether it is a DW_AT_external
(i.e., a forward declaration in DWARF) may or may not have a DefinitionData
allocated for its associated clang::CXXRecordDecl
. This happens in DWARFASTParserClang::ParseStructureLikeDIE
.
LLDB relies on someone calling the ExternalASTSource::CompleteType
API in time before we dereference getDefinition
, e.g., [1][2][3]. But there is no single mechanism by which this is guaranteed to happen. Usually Type::GetFullCompilerType
is eventually called, at which point we run DWARFASTParserClang::CompleteTypeFromDWARF
, parsing the record’s children (as “forward declarations”), and completing the record’s definition.
LLDB allocates the clang types/decls it creates from DWARF into a clang::ASTContext
(one context dedicated per LLDB module). Then it moves these AST nodes into a new AST that lives for the duration of an expression (aka the Expression AST). Moving the decls between ASTContext
’s is done via the clang::ASTImporter
. To avoid redundantly importing the definition (and all children) of a DeclContext
, LLDB uses the ASTImporter
in a dedicated MinimalImport
mode. This mode only imports the “shell” of a DeclContext
without a definition [4], relying on LLDB to call ImportDefinition
to pull it in when it needs to [5].
A clang::DeclContext
maintains information about whether it contains any undeserialized
declarations (see DeclContextBitfields). These are checked in following places:
- In Clang performs a lookup into a DeclContext, it checks whether these bits have been set, and if so, will call into LLDB’s implementation of
ExternalASTSource::FindExternalVisibleDeclsByName
. - In TypeSystemClang::GetCompleteDecl to determine whether we need to complete the type or not.
- Iterating over decls of a
clang::DeclContext
will callLoadFieldsFromExternalStorage
.
Shortcomings of Current Mechanism
While the current technique of completing types makes sure we do as little work as possible, it suffers from a few correctness issues, which has led to crashes that are hard to debug and workaround over the years [6][7][8].
- If the type we’re parsing from DWARF is not a forward declaration, we allocate a default
DefinitionData
structure for it but don’t parse/complete any of its children, meaning we leave the declaration in a sort of invalid transient state, relying on something to complete it later when necessary. - If it is a forward declaration according to DWARF, we leave the
DefinitionData
unallocated, relying on something pulling in the definition later (we try to callFindDefinitionTypeForDWARFDeclContext
but that isn’t guaranteed to succeed).
In both cases, if we don’t actually complete the type by the time the type’s definition is queried, we will most likely violate Clang invariants.
(see also previous discussion of type completion mechanism benefits/drawbacks)
Proposed Solution
The ExternalASTSource
API that Clang will consistently call on every access to a definition is ExternalASTSource::CompleteRedeclChain
(see DeclLink callback initiated by a call to dataPtr
). The core idea of this RFC is to rely on this API to do type completion in LLDB, instead of the previously less well-defined ExternalASTSource::CompleteType
API.
We propose implementing the ClangASTSource::CompleteRedeclChain
in terms of TypeSystemClang::CompleteTagDecl
(which calls into DWARFASTParserClang::CompleteTypeFromDWARF
). Then whenever anyone asks for getDefinition
on a RecordDecl
, we would ensure that LLDB had the chance to actually fetch the definition. Since CompleteRedeclChain
only gets called when a declaration chain is deemed to be outdated (see generation counter check in LazyGenerationalUpdatePtr) we create redeclaration chains when parsing DWARF and increment the owning AST’s generation counter.
Changes
We merged a prototype of this RFC behind an experimental setting into a apple/llvm-project
branch (see 0261a39) to gather feedback. Here’s a high-level summary of the core changes:
DWARFASTParserClang
:- Parsing a structure will now neither start (i.e., allocate) a definition nor set the
hasExternalLexicalStorage
/hasExternalVisibleStorage
flags. CompleteRecordType
will now start and complete the definition- This addresses the “we allocated by didn’t complete a definition” problem.
- Parsing a structure will now neither start (i.e., allocate) a definition nor set the
TypeSystemClang
:- We can now implement completion in terms of
getDefinition
. CompleteTagDeclarationDefinition
no longer unsets thehasExternalLexicalStorage
/hasExternalVisibleStorage
flags.
- We can now implement completion in terms of
ASTImporter
- Will import entire
DeclContext
for structure instead of minimally importing.- Now doesn’t require manual
ImportDefinition
calls in LLDB.
- Now doesn’t require manual
- This will allow us to potentially get rid of the
MinimalImport
mode entirely (or at least remove some of the workarounds for minimal import of record types).
- Will import entire
- Since we’re now dealing with redeclaration chains, we need to make sure that wherever LLDB caches a decl, it does so using the canonical decl (i.e., the first decl of the chain). In our implementation, the first decl is also the one that holds the definition (i.e., the one we ultimately complete).
Caveats
Performance
The prototype as-is currently significantly regresses performance of any part of LLDB that performs type completion. This stems from the fact that we are now completing types more aggressively, resulting in more work for the ASTImporter and also more DWARF parsing (particularly we would be searching through more object files than we currently do on top-of-tree). We benchmarked this extensively so let me know if you want a breakdown of the regression.
The main remaining problem we identified is that we’re currently fully resolving C++ class methods, completing parameters and return types in the process whenever we complete a type. This vastly expands the set of types we need to complete. The following two remaining tasks will get us back to top-of-tree performance:
- Stop resolving member function DIEs in
DWARFASTParserClang::CompleteRecordType
(we really only want to stop resovling non-virtual methods, since virtual methods contribute to class layouts) - Stop importing method parameters in the
ASTImporter
inVisitFunctionDecl
on a class method
If we stop fully resolving member function DIEs, we will need a way to lazily complete them when needed. We could introduce a mechanism similar to LoadFieldsFromExternalStorage
to load the methods when asked for. We would have to add back logic to set the “has external storage” flags, but that should be much less problematic for methods because their absence is less critical than fields are, e.g., it wouldn’t break the layout builder.
Summary
To eliminate the crashes that stem from overly lazy type completion, we plan to refactor the LLDB’s type completion mechanism such that we guarantee to always get a type’s definition when Clang or LLDB ask for it. This will also help simplify a lot of the MinimalImport
infrastructure in the ASTImporter
and LLDB.
We’ll be monitoring how effectively our prototype helps eliminate LLDB crashes in the field over the next couple of weeks. Meanwhile we will also be addressing the above two performance issues and will then explore how to integrate the patch series into the main branch.
Looking forward to hearing your feedback!
CCing some people who might be interested in this (@jingham @clayborg @dblaikie @adrian.prantl @Teemperor @pogo59 @Felipe)