From a9a3ebf54e337957afc9bada2fcc18447d3aa8df Mon Sep 17 00:00:00 2001 From: Felipe de Azevedo Piovezan Date: Fri, 16 May 2025 14:50:31 -0700 Subject: [PATCH 1/2] [lldb][nfc] Create helper functions in SwiftLanguageRuntimeNames These will be useful to reuse code in upcoming commits. --- .../Swift/SwiftLanguageRuntimeNames.cpp | 104 +++++++++++------- 1 file changed, 67 insertions(+), 37 deletions(-) diff --git a/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp b/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp index 1f6615001f7ff..330dc16a5500a 100644 --- a/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp +++ b/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp @@ -419,6 +419,66 @@ CreateRunThroughTaskSwitchingTrampolines(Thread &thread, return nullptr; } +/// Demangle `symbol_name` and extracts the text at the node described by +/// `node_path`, if it exists; otherwise, returns an empty string. +static std::string FindClassName(StringRef symbol_name, + llvm::ArrayRef node_path) { + swift::Demangle::Context ctx; + NodePointer demangled_node = + SwiftLanguageRuntime::DemangleSymbolAsNode(symbol_name, ctx); + + if (!demangled_node) { + std::string symbol_name_str = symbol_name.str(); + LLDB_LOGF(GetLog(LLDBLog::Step), + "SwiftLanguageRuntime: failed to demangle %s.", + symbol_name_str.c_str()); + return ""; + } + NodePointer class_node = childAtPath(demangled_node, node_path); + if (!class_node || !class_node->hasText()) { + std::string node_str = getNodeTreeAsString(demangled_node); + LLDB_LOGF(GetLog(LLDBLog::Step), + "SwiftLanguageRuntime: failed to extract name from " + "demangle node: %s", + node_str.c_str()); + return ""; + } + return class_node->getText().str(); +} + +/// If sc_list is non-empty, returns a plan that runs to any of its addresses. +/// Otherwise, returns nullptr. +static ThreadPlanSP +CreateThreadPlanRunToSCInList(Thread &thread, const SymbolContextList &sc_list, + bool stop_others) { + std::vector load_addresses; + Target &target = thread.GetProcess()->GetTarget(); + for (const SymbolContext &sc : sc_list) { + const Symbol *ctor_symbol = sc.symbol; + if (ctor_symbol) + load_addresses.push_back(ctor_symbol->GetLoadAddress(&target)); + } + + if (load_addresses.empty()) { + LLDB_LOG(GetLog(LLDBLog::Step), + "SwiftLanguageRuntime: empty sc_list found."); + return {}; + } + return std::make_shared(thread, load_addresses, + stop_others); +} + +/// Search all modules for `target_func` and creates a RunToAddress plan +/// targeting all symbols found. +static ThreadPlanSP CreateRunToAddressPlan(StringRef target_func, + Thread &thread, bool stop_others) { + ModuleList modules = thread.GetProcess()->GetTarget().GetImages(); + SymbolContextList sc_list; + modules.FindFunctionSymbols(ConstString(target_func), eFunctionNameTypeFull, + sc_list); + return CreateThreadPlanRunToSCInList(thread, sc_list, stop_others); +} + static lldb::ThreadPlanSP GetStepThroughTrampolinePlan(Thread &thread, bool stop_others) { // Here are the trampolines we have at present. @@ -475,19 +535,7 @@ static lldb::ThreadPlanSP GetStepThroughTrampolinePlan(Thread &thread, log->Printf( "Stepped to thunk \"%s\" (kind: %s) stepping to target: \"%s\".", symbol_name, GetThunkKindName(thunk_kind), thunk_target.c_str()); - - ModuleList modules = thread.GetProcess()->GetTarget().GetImages(); - SymbolContextList sc_list; - modules.FindFunctionSymbols(ConstString(thunk_target), - eFunctionNameTypeFull, sc_list); - if (sc_list.GetSize() == 1 && sc_list[0].symbol) { - Symbol &thunk_symbol = *sc_list[0].symbol; - Address target_address = thunk_symbol.GetAddress(); - if (target_address.IsValid()) - return std::make_shared(thread, target_address, - stop_others); - } - return nullptr; + return CreateRunToAddressPlan(thunk_target, thread, stop_others); } case ThunkAction::StepIntoConformance: { // The TTW symbols encode the protocol conformance requirements @@ -582,37 +630,19 @@ static lldb::ThreadPlanSP GetStepThroughTrampolinePlan(Thread &thread, } case ThunkAction::StepIntoAllocatingInit: { LLDB_LOGF(log, "Stepping into allocating init: \"%s\"", symbol_name); - swift::Demangle::Context ctx; - NodePointer demangled_node = - SwiftLanguageRuntime::DemangleSymbolAsNode(symbol_name, ctx); - using Kind = Node::Kind; - NodePointer class_node = childAtPath( - demangled_node, {Kind::Allocator, Kind::Class, Kind::Identifier}); - if (!class_node || !class_node->hasText()) { - std::string node_str = getNodeTreeAsString(demangled_node); - LLDB_LOGF(log, - "Failed to extract constructor name from demangle node: %s", - node_str.c_str()); + static constexpr auto class_path = {Kind::Allocator, Kind::Class, + Kind::Identifier}; + std::string class_name = FindClassName(symbol_name, class_path); + if (class_name.empty()) return nullptr; - } ModuleFunctionSearchOptions options{/*include_symbols*/ true, /*include_inlines*/ true}; - std::string ctor_name = llvm::formatv("{0}.init", class_node->getText()); + std::string ctor_name = llvm::formatv("{0}.init", class_name); SymbolContextList sc_list; sc.module_sp->FindFunctions(RegularExpression(ctor_name), options, sc_list); - std::vector load_addresses; - Target &target = thread.GetProcess()->GetTarget(); - for (const SymbolContext &ctor_sc : sc_list) { - const Symbol *ctor_symbol = ctor_sc.symbol; - if (ctor_symbol) - load_addresses.push_back(ctor_symbol->GetLoadAddress(&target)); - } - if (load_addresses.empty()) - return nullptr; - return std::make_shared(thread, load_addresses, - stop_others); + return CreateThreadPlanRunToSCInList(thread, sc_list, stop_others); } case ThunkAction::StepThrough: { if (log) From 3512b6a5fa709a455718f7f895940d9f803fc7e9 Mon Sep 17 00:00:00 2001 From: Felipe de Azevedo Piovezan Date: Fri, 16 May 2025 15:01:28 -0700 Subject: [PATCH 2/2] [lldb] Fix stepping into ObjcC ctor from Swift When constructing an Objective C object of type `Foo` from Swift, this sequence of function calls is used: ``` * frame #0: 0x000000010000147c test.out`-[Foo initWithString:](self=0x00006000023ec000, _cmd="initWithString:", value=@"Bar") -[Foo initWithString:] at Foo.m:9:21 frame #1: 0x00000001000012bc test.out`@nonobjc Foo.init(string:) $sSo3FooC6stringABSS_tcfcTO at :0 frame #2: 0x0000000100001170 test.out`Foo.__allocating_init(string:) $sSo3FooC6stringABSS_tcfC at Foo.h:0 frame #3: 0x0000000100000ed8 test.out`work() $s4test4workyyF at main.swift:5:18 ``` Frames 1 and 2 are common with pure Swift classes, and LLDB has a Thread Plan to go from `Foo.allocating_init` -> `Foo.init`. In the case of Objcetive C interop, `Foo.init` has no user code, and is annotated with `@nonobjc`. The debugger needs a plan to go from that code to the Objective C implementation. This is what this patch attempts to fix by creating a plan that runs to any symbol matching `Foo init` (this will match all the :withBlah suffixes). This seems to be the only possible fix for this. While Objective C constructors are not necessarily called init, the interop layer seems to assume this. The only other alternative has some obstacles that could not be easily overcome. Here's the main idea for that. The assembly for `@nonobjc Foo.init` looks like (deleted all non branches): ``` test.out`@nonobjc Foo.init(string:): ... 0x1000012a0 <+20>: bl 0x100001618 ; symbol stub for: Swift.String._bridgeToObjectiveC() -> __C.NSString ... 0x1000012b8 <+44>: bl 0x100001630 ; symbol stub for: objc_msgSend ... 0x1000012e8 <+92>: ret ``` If we had more String arguments, there would be more calls to `_bridgeToObjectiveC`. The call to `objc_msgSend` is the important one, and LLDB knows how to go from that to the target of the message, LLDB has ThreadPlans for that. However, setting a breakpoint on `objc_msgSend` would fail: the calls to `_bridgeToObjectiveC` may also call `objc_msgSend`, so LLDB would end up in the wrong `objc_msgSend`. This is not entirely bad, LLDB would step back to `Foo.init`. Here's the catch: the language runtime refuses to create other plans if PC is not at the start of the function, which makes sense, as it would not be able to distinguish if its job was already done previously or not, unless it had a stateful plan (which it doesn't today). --- .../Swift/SwiftLanguageRuntimeNames.cpp | 35 +++++++++++++ .../swift/step_into_objc_interop_init/Foo.h | 11 ++++ .../swift/step_into_objc_interop_init/Foo.m | 18 +++++++ .../step_into_objc_interop_init/Makefile | 6 +++ .../TestStepIntoObjCInteropInit.py | 51 +++++++++++++++++++ .../bridging-header.h | 1 + .../step_into_objc_interop_init/main.swift | 7 +++ 7 files changed, 129 insertions(+) create mode 100644 lldb/test/API/lang/swift/step_into_objc_interop_init/Foo.h create mode 100644 lldb/test/API/lang/swift/step_into_objc_interop_init/Foo.m create mode 100644 lldb/test/API/lang/swift/step_into_objc_interop_init/Makefile create mode 100644 lldb/test/API/lang/swift/step_into_objc_interop_init/TestStepIntoObjCInteropInit.py create mode 100644 lldb/test/API/lang/swift/step_into_objc_interop_init/bridging-header.h create mode 100644 lldb/test/API/lang/swift/step_into_objc_interop_init/main.swift diff --git a/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp b/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp index 330dc16a5500a..1433cc79021f0 100644 --- a/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp +++ b/lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp @@ -45,6 +45,7 @@ enum class ThunkKind { AllocatingInit, PartialApply, ObjCAttribute, + NonObjCAttributeOnCtor, Reabstraction, ProtocolConformance, }; @@ -55,6 +56,7 @@ enum class ThunkAction { StepIntoConformance, StepIntoAllocatingInit, StepThrough, + RunToObjcCInteropCtor, }; } // namespace @@ -313,6 +315,10 @@ static ThunkKind GetThunkKind(Symbol *symbol) { switch (main_node->getKind()) { case Node::Kind::ObjCAttribute: return ThunkKind::ObjCAttribute; + case Node::Kind::NonObjCAttribute: + if (hasChild(nodes, Node::Kind::Constructor)) + return ThunkKind::NonObjCAttributeOnCtor; + break; case Node::Kind::ProtocolWitness: if (hasChild(main_node, Node::Kind::ProtocolConformance)) return ThunkKind::ProtocolConformance; @@ -342,6 +348,8 @@ static const char *GetThunkKindName(ThunkKind kind) { return "GetThunkTarget"; case ThunkKind::ObjCAttribute: return "GetThunkTarget"; + case ThunkKind::NonObjCAttributeOnCtor: + return "RunToObjcCInteropCtor"; case ThunkKind::Reabstraction: return "GetThunkTarget"; case ThunkKind::ProtocolConformance: @@ -363,6 +371,8 @@ static ThunkAction GetThunkAction(ThunkKind kind) { return ThunkAction::StepThrough; case ThunkKind::ProtocolConformance: return ThunkAction::StepIntoConformance; + case ThunkKind::NonObjCAttributeOnCtor: + return ThunkAction::RunToObjcCInteropCtor; } } @@ -537,6 +547,31 @@ static lldb::ThreadPlanSP GetStepThroughTrampolinePlan(Thread &thread, symbol_name, GetThunkKindName(thunk_kind), thunk_target.c_str()); return CreateRunToAddressPlan(thunk_target, thread, stop_others); } + case ThunkAction::RunToObjcCInteropCtor: { + static constexpr auto class_path = { + Node::Kind::Constructor, Node::Kind::Class, Node::Kind::Identifier}; + std::string class_name = FindClassName(symbol_name, class_path); + if (class_name.empty()) { + LLDB_LOGF(log, + "SwiftLanguageRuntime: could not derive class name from symbol " + "\"%s\".", + symbol_name); + return nullptr; + } + std::string ctor_name = llvm::formatv("{0} init", class_name); + LLDB_LOGF(log, + "SwiftLanguageRuntime: running to objective C constructor \"%s\" " + "from swift.", + ctor_name.c_str()); + + SymbolContextList sc_list; + ModuleFunctionSearchOptions options{/*include_symbols*/ true, + /*include_inlines*/ true}; + ModuleList modules = thread.GetProcess()->GetTarget().GetImages(); + modules.FindFunctions(RegularExpression(ctor_name), options, sc_list); + + return CreateThreadPlanRunToSCInList(thread, sc_list, stop_others); + } case ThunkAction::StepIntoConformance: { // The TTW symbols encode the protocol conformance requirements // and it is possible to go to the AST and get it to replay the diff --git a/lldb/test/API/lang/swift/step_into_objc_interop_init/Foo.h b/lldb/test/API/lang/swift/step_into_objc_interop_init/Foo.h new file mode 100644 index 0000000000000..a1a4ffee7eb91 --- /dev/null +++ b/lldb/test/API/lang/swift/step_into_objc_interop_init/Foo.h @@ -0,0 +1,11 @@ +#import + +@interface Foo : NSObject + +@property (nonnull) NSArray *values; + +- (nonnull id)init; +- (nonnull id)initWithString:(nonnull NSString *)value; +- (nonnull id)initWithString:(nonnull NSString *)value andOtherString:(nonnull NSString *) otherValue; + +@end diff --git a/lldb/test/API/lang/swift/step_into_objc_interop_init/Foo.m b/lldb/test/API/lang/swift/step_into_objc_interop_init/Foo.m new file mode 100644 index 0000000000000..7d5252e9a4b41 --- /dev/null +++ b/lldb/test/API/lang/swift/step_into_objc_interop_init/Foo.m @@ -0,0 +1,18 @@ +#import "Foo.h" + +@implementation Foo + +- (id)init { +} + +- (id)initWithString:(nonnull NSString *)value { + self->_values = @[value]; + return self; +} + +- (nonnull id)initWithString:(nonnull NSString *)value andOtherString:(nonnull NSString *) otherValue { + self->_values = @[value, otherValue]; + return self; +} + +@end diff --git a/lldb/test/API/lang/swift/step_into_objc_interop_init/Makefile b/lldb/test/API/lang/swift/step_into_objc_interop_init/Makefile new file mode 100644 index 0000000000000..e13e2a9f7bd44 --- /dev/null +++ b/lldb/test/API/lang/swift/step_into_objc_interop_init/Makefile @@ -0,0 +1,6 @@ +SWIFT_SOURCES := main.swift +SWIFT_BRIDGING_HEADER := bridging-header.h +OBJC_SOURCES := Foo.m +SWIFT_OBJC_INTEROP := 1 + +include Makefile.rules diff --git a/lldb/test/API/lang/swift/step_into_objc_interop_init/TestStepIntoObjCInteropInit.py b/lldb/test/API/lang/swift/step_into_objc_interop_init/TestStepIntoObjCInteropInit.py new file mode 100644 index 0000000000000..46d5fd5350517 --- /dev/null +++ b/lldb/test/API/lang/swift/step_into_objc_interop_init/TestStepIntoObjCInteropInit.py @@ -0,0 +1,51 @@ +import lldb +from lldbsuite.test.lldbtest import * +from lldbsuite.test.decorators import * +import lldbsuite.test.lldbutil as lldbutil + + +class TestSwiftObjcProtocol(TestBase): + def skip_debug_info_libraries(self): + if platform.system() == "Darwin": + lib_name = "libswiftCore.dylib" + else: + lib_name = "libswiftCore.so" + + self.dbg.HandleCommand( + "settings set " + "target.process.thread.step-avoid-libraries {}".format(lib_name) + ) + + @skipUnlessDarwin + @swiftTest + def test(self): + self.build() + (target, process, thread, breakpoint) = lldbutil.run_to_source_breakpoint( + self, "break here", lldb.SBFileSpec("main.swift") + ) + + self.skip_debug_info_libraries() + # Go to the first constructor, assert we can step into it. + thread.StepInto() + self.assertEqual(thread.stop_reason, lldb.eStopReasonPlanComplete) + self.assertIn("-[Foo init]", thread.frames[0].GetFunctionName()) + + # Go back to "work" function + thread.StepOut() + self.assertEqual(thread.stop_reason, lldb.eStopReasonPlanComplete) + self.assertIn("work", thread.frames[0].GetFunctionName()) + + # Go to the next constructor call. + thread.StepOver() + self.assertEqual(thread.stop_reason, lldb.eStopReasonPlanComplete) + self.assertIn("work", thread.frames[0].GetFunctionName()) + + # Assert we can step into it. + thread.StepInto() + self.assertEqual(thread.stop_reason, lldb.eStopReasonPlanComplete) + self.assertIn("-[Foo initWithString:]", thread.frames[0].GetFunctionName()) + + # Go back to "work" function + thread.StepOut() + self.assertEqual(thread.stop_reason, lldb.eStopReasonPlanComplete) + self.assertIn("work", thread.frames[0].GetFunctionName()) diff --git a/lldb/test/API/lang/swift/step_into_objc_interop_init/bridging-header.h b/lldb/test/API/lang/swift/step_into_objc_interop_init/bridging-header.h new file mode 100644 index 0000000000000..45361d3bc5b26 --- /dev/null +++ b/lldb/test/API/lang/swift/step_into_objc_interop_init/bridging-header.h @@ -0,0 +1 @@ +#import "Foo.h" diff --git a/lldb/test/API/lang/swift/step_into_objc_interop_init/main.swift b/lldb/test/API/lang/swift/step_into_objc_interop_init/main.swift new file mode 100644 index 0000000000000..286d2f30455e2 --- /dev/null +++ b/lldb/test/API/lang/swift/step_into_objc_interop_init/main.swift @@ -0,0 +1,7 @@ +func work() { + let noParams = Foo() // break here + let oneParam = Foo(string: "Bar") + print("done") +} + +work()