Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lldb/include/lldb/Symbol/CompilerDeclContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ class CompilerDeclContext {

ConstString GetScopeQualifiedName() const;

CompilerDecl GetDecl() const;

private:
TypeSystem *m_type_system = nullptr;
void *m_opaque_decl_ctx = nullptr;
Expand Down
4 changes: 4 additions & 0 deletions lldb/include/lldb/Symbol/TypeSystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ class TypeSystem : public PluginInterface,

virtual lldb::LanguageType DeclContextGetLanguage(void *opaque_decl_ctx) = 0;

virtual CompilerDecl DeclContextGetDecl(void *opaque_decl_ctx) {
return CompilerDecl();
}

/// Returns the direct parent context of specified type
virtual CompilerDeclContext
GetCompilerDeclContextForType(const CompilerType &type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,233 @@ CPPLanguageRuntime::FindLibCppStdFunctionCallableInfo(
if (!valobj_sp)
return optional_info;

// std::function has many variants, try to disambiguate
ValueObjectSP func_as_base_ptr;
{
ValueObjectSP outer_f = valobj_sp->GetChildMemberWithName("__f_");

if (!outer_f)
return optional_info; // Unrecognized implementation

if (outer_f->IsPointerType()) {
// git: 3e519524c118651123eecf60c2bbc5d65ad9bac3
//
// class function<_Rp()> {
// aligned_storage<3*sizeof(void*)>::type __buf_;
// __base<_Rp>* __f_;
// }

func_as_base_ptr = std::move(outer_f);
} else if (auto inner_f = outer_f->GetChildMemberWithName("__f_")) {
// git: 050b064f15ee56ee0b42c9b957a3dd0f32532394
//
// class function<_Rp(_ArgTypes...)> {
// __value_func<_Rp(_ArgTypes...)> __f_;
// }
//
// class __value_func<_Rp(_ArgTypes...)> {
// aligned_storage<3 * sizeof(void*)>::type __buf_;
// __base<_Rp(_ArgTypes...)>* __f_;
// }

func_as_base_ptr = std::move(inner_f);
} else
return optional_info; // Unrecognized implementation
}

// __base<...> is a pure virtual class with an interface to create/copy/destroy/invoke
// the underlying value. This interface is implemented by partial specializations of the
// __func<_Fp, _Alloc, ...> template where _Fp is the wrapped functor object
Status status;
ValueObjectSP func_as_base = func_as_base_ptr->Dereference(status);
if (status.Fail())
return optional_info;

// First we'll try to extract the __func<...> template instantiation's type by looking up
// the declarations of the member function pointers in it's vtable
CompilerType func_type;
Address func_method_addr;
{
ValueObjectSP vtable = func_as_base->GetVTable();

llvm::Expected<uint32_t> num_entries = vtable->GetNumChildren();
if (num_entries.takeError())
return optional_info;

// __base is pure virtual, __func is final. All member function pointers are equally
// good candidates to find the enclosing class.
//
// In practice the first two vtable entries point to artificial destructors which the
// type system refuses to elaborate as their artificial specifications are not added
// to the enclosing class' declaration context. This causes various warnings, and dont
// get us any closer to the concrete type thus we skip them.
for (uint32_t idx = 2; idx < *num_entries; idx++) {
ValueObjectSP entry = vtable->GetChildAtIndex(idx);

// Points to a potentially interesting member function
addr_t mfunc_load_addr = entry->GetValueAsUnsigned(0);
if (!mfunc_load_addr)
continue;

if (!valobj_sp->GetTargetSP()->ResolveLoadAddress(mfunc_load_addr, func_method_addr))
continue;

Function* func = func_method_addr.CalculateSymbolContextFunction();
if (!func)
continue;

CompilerDeclContext mfunc_decl_ctx = func->GetDeclContext();
if (!mfunc_decl_ctx.IsClassMethod())
continue;

// Member functions are contained in their enclosing class' decl context
CompilerDeclContext mfunc_parent = mfunc_decl_ctx.GetDecl().GetDeclContext();
if (!mfunc_parent.IsValid())
continue;

func_type = mfunc_parent.GetDecl().GetType();
break;
}
}

CompilerType callable_type = func_type.GetTypeTemplateArgument(0);
if (!callable_type)
return optional_info;

// Now that the __func is a known type we can dig for the wrapped callable
ValueObjectSP callable;
{
// class __func<_Fp, _Alloc, _Rp(_ArgTypes...)> : __base<_Rp(_ArgTypes...)> {
// __alloc_func<_Fp, _Alloc, _Rp(_ArgTypes...)> __f_;
// }
//
// class __alloc_func<_Fp, _Ap, _Rp(_ArgTypes...)> {
// __compressed_pair<_Fp, _Ap> __f_;
// }
//
// class __compressed_pair : __compressed_pair_elem<_T1, 0>,
// __compressed_pair_elem<_T2, 1> {
// }
//
// struct __compressed_pair_elem {
// _Tp __value_;
// }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, this is not the compressed pair representation of libc++ anymore. Nothing really that would affect the code here, but the comment could use an update.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am developing this using the SDK shipped with Xcode 16 - that's where I mostly intend to use the improvements too. Perhaps it isn't worth putting library code excerpts into comments if they are doomed to get out of sync eventually?

ValueObjectSP alloc_func = func_as_base->Cast(func_type);
if (!alloc_func)
return optional_info;

ValueObjectSP pair = alloc_func->GetChildAtNamePath({"__f_", "__f_"});
if (!pair)
return optional_info;

if (callable_type.IsRecordType() && callable_type.GetNumFields() == 0) {
// callable_type is an empty class, and has been optimized away! Serve a dummy
callable = valobj_sp->CreateValueObjectFromAddress("__value_",
pair->GetLoadAddress(),
pair->GetExecutionContextRef(),
callable_type);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean optimized away here? What situation do we land here in? I assume this is not optimized code you're talking about. Why do 0 fields indicate such situation?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what you were seeing actually is that libc++ removed the __compressed_pair structure. So __f_ gets you exactly the callable, not a wrapper template anymore. See e.g., what we did for the other data-formatters to support the new layout: #96538

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code snippets above are from the SDK shipped with Xcode 16. There __compressed_pair_elem has a specialization for storing classes which are completely empty which is an empty class.

In that case empty base class elision kicks in, and leaves me with a __compressed_pair with no base classes and no fields.

Eventually I would like to make a synthetic children provider for std::function which would show the wrapped object. I figured even if it is empty it would be worthy to show a dummy of it so the UI can immediately show the underlying C++ type as well.


As far as I am aware Apple's libc++ implementation is slightly different than the one on this repo. What is the policy for such situations? Are you OK with me also supporting their layout or is that something more fitting of contributing to https://github.com/swiftlang/llvm-project?

} else {
ValueObjectSP elem0 = pair->GetChildAtIndex(0);
if (!elem0)
return optional_info;

callable = elem0->GetChildMemberWithName("__value_");
if (!callable)
return optional_info;
}
}

if (callable_type.IsFunctionPointerType()) {
addr_t target_load_addr = callable->GetValueAsUnsigned(LLDB_INVALID_ADDRESS);

ModuleSP mod = func_method_addr.CalculateSymbolContextModule();

Address callable_addr;
if (!mod->ResolveFileAddress(target_load_addr, callable_addr))
return optional_info;

SymbolContext sc;
mod->ResolveSymbolContextForAddress(callable_addr, eSymbolContextSymbol | eSymbolContextLineEntry, sc);

if (!sc.symbol)
return optional_info;

return LibCppStdFunctionCallableInfo {
.callable_symbol = *sc.symbol,
.callable_address = sc.symbol->GetAddress(),
.callable_line_entry = sc.line_entry,
.callable_case = LibCppStdFunctionCallableCase::FreeOrMemberFunction
};
} else if (callable_type.IsMemberFunctionPointerType()) {
// TODO: Member function's unsigned value comes back as invalid! I am guessing
// the ValueObject wants to let me know that this is not necessarily as simple
// as that.. I remember reading something in the Itanium ABI about member
// pointers taking up two pointers of space. Perhaps that's why it is not legal
// to read their underlying value raw?

printf("Curious!");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, yes, it's represented as a pointer and possible offset: https://itanium-cxx-abi.github.io/cxx-abi/abi.html#member-function-pointers

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the ABI agnostic way of extracting the underlying function pointer? Or is it fine to assume that the target is using the Itanium ABI?

} else if (callable_type.IsRecordType()) {
// Target is a lambda, or a generic callable. Search for a single operator() overload
std::optional<ConstString> mangled_func_name;

for (uint32_t idx = 0; idx < callable_type.GetNumMemberFunctions(); idx++) {
TypeMemberFunctionImpl mfunc = callable_type.GetMemberFunctionAtIndex(idx);

if (mfunc.GetKind() != eMemberFunctionKindInstanceMethod)
continue;

if (mfunc.GetName() != "operator()")
continue;

if (mangled_func_name)
return optional_info; // Cannot resolve ambiguous target

mangled_func_name = mfunc.GetMangledName();
}

// Locate the symbol context corresponding to the target function
SymbolContext sc;
{
// We'll assume that callable_type is in the same module as the vtable
ModuleSP mod = func_method_addr.CalculateSymbolContextModule();

// Limit our lookup to callable_type
CompilerDeclContext decl_ctx = callable_type.GetTypeSystem()->GetCompilerDeclContextForType(callable_type);

SymbolContextList list;
mod->FindFunctions(*mangled_func_name, decl_ctx, eFunctionNameTypeFull, {}, list);

if (list.GetSize() != 1)
return optional_info;

list.GetContextAtIndex(0, sc);
}

// TODO: This feels a bit clunky, I am probably misusing the API? FindFunctions returns me
// SymbolContexts with the .function set but not .symbol ... At first glance it seemed like
// if we know the function there must be a symbol too!
if (!sc.function)
return optional_info;

Symbol* symbol = sc.function->GetAddressRange().GetBaseAddress().CalculateSymbolContextSymbol();
if (!symbol)
return optional_info;

return LibCppStdFunctionCallableInfo {
.callable_symbol = *symbol,
.callable_address = symbol->GetAddress(),
.callable_line_entry = sc.GetFunctionStartLineEntry(),

// TODO: Can't tell lambdas apart from generic callables.. do we really need to?
// Is it important to have the correct qualification in the summary?
.callable_case = LibCppStdFunctionCallableCase::Lambda
};
}

// Unrecognized callable type - skip the original implementation for now
if (!callable_type.IsVoidType())
return optional_info;

// Member __f_ has type __base*, the contents of which will hold:
// 1) a vtable entry which may hold type information needed to discover the
// lambda being called
Expand Down Expand Up @@ -232,7 +459,6 @@ CPPLanguageRuntime::FindLibCppStdFunctionCallableInfo(
return optional_info;

uint32_t address_size = process->GetAddressByteSize();
Status status;

// First item pointed to by __f_ should be the pointer to the vtable for
// a __base object.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,6 @@ llvm::Error ItaniumABILanguageRuntime::TypeHasVTable(CompilerType type) {
type = pointee_type;
}

// Make sure this is a class or a struct first by checking the type class
// bitfield that gets returned.
if ((type.GetTypeClass() & (eTypeClassStruct | eTypeClassClass)) == 0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate on the removal here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have overgeneralized here a bit - TypeSystemClang::IsPolymorphicClass already checks these conditions, and seems to handle situations better where CompilerType is a typedef to a struct/class instead of being one itself.

return llvm::createStringError(std::errc::invalid_argument,
"type \"%s\" is not a class or struct or a pointer to one",
original_type.GetTypeName().AsCString("<invalid>"));
}

// Check if the type has virtual functions by asking it if it is polymorphic.
if (!type.IsPolymorphicClass()) {
return llvm::createStringError(std::errc::invalid_argument,
Expand Down
7 changes: 7 additions & 0 deletions lldb/source/Plugins/TypeSystem/Clang/TypeSystemClang.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9595,6 +9595,13 @@ TypeSystemClang::DeclContextGetLanguage(void *opaque_decl_ctx) {
return eLanguageTypeUnknown;
}

CompilerDecl TypeSystemClang::DeclContextGetDecl(void *opaque_decl_ctx) {
if (auto *decl_ctx = (clang::DeclContext *)opaque_decl_ctx)
if (auto* decl = dyn_cast_or_null<clang::Decl>(decl_ctx))
return CompilerDecl(this, decl);
return CompilerDecl();
}

static bool IsClangDeclContext(const CompilerDeclContext &dc) {
return dc.IsValid() && isa<TypeSystemClang>(dc.GetTypeSystem());
}
Expand Down
2 changes: 2 additions & 0 deletions lldb/source/Plugins/TypeSystem/Clang/TypeSystemClang.h
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,8 @@ class TypeSystemClang : public TypeSystem {

lldb::LanguageType DeclContextGetLanguage(void *opaque_decl_ctx) override;

CompilerDecl DeclContextGetDecl(void *opaque_decl_ctx) override;

std::vector<lldb_private::CompilerContext>
DeclContextGetCompilerContext(void *opaque_decl_ctx) override;

Expand Down
6 changes: 6 additions & 0 deletions lldb/source/Symbol/CompilerDeclContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ ConstString CompilerDeclContext::GetScopeQualifiedName() const {
return ConstString();
}

CompilerDecl CompilerDeclContext::GetDecl() const {
if (IsValid())
return m_type_system->DeclContextGetDecl(m_opaque_decl_ctx);
return CompilerDecl();
}

bool CompilerDeclContext::IsClassMethod() {
if (IsValid())
return m_type_system->DeclContextIsClassMethod(m_opaque_decl_ctx);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
#include <functional>

template<typename = bool, typename = int>
struct Dummy {
// Used to make lambda host function's symbol more complex
};

int foo(int x, int y) {
return x + y - 1;
}
Expand All @@ -18,7 +23,7 @@ struct Bar {
}
} ;

int foo2() {
int foo2(Dummy<> dummy = {}) {
auto f = [](int x) {
return x+1;
};
Expand Down