Skip to content

Commit 75bd162

Browse files
[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 <compiler-generated>: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).
1 parent 5358515 commit 75bd162

File tree

8 files changed

+127
-0
lines changed

8 files changed

+127
-0
lines changed

lldb/source/Plugins/LanguageRuntime/Swift/SwiftLanguageRuntimeNames.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ enum class ThunkKind {
4545
AllocatingInit,
4646
PartialApply,
4747
ObjCAttribute,
48+
NonObjCAttributeOnCtor,
4849
Reabstraction,
4950
ProtocolConformance,
5051
};
@@ -55,6 +56,7 @@ enum class ThunkAction {
5556
StepIntoConformance,
5657
StepIntoAllocatingInit,
5758
StepThrough,
59+
RunToObjcCInteropCtor,
5860
};
5961

6062
} // namespace
@@ -313,6 +315,10 @@ static ThunkKind GetThunkKind(Symbol *symbol) {
313315
switch (main_node->getKind()) {
314316
case Node::Kind::ObjCAttribute:
315317
return ThunkKind::ObjCAttribute;
318+
case Node::Kind::NonObjCAttribute:
319+
if (hasChild(nodes, Node::Kind::Constructor))
320+
return ThunkKind::NonObjCAttributeOnCtor;
321+
break;
316322
case Node::Kind::ProtocolWitness:
317323
if (hasChild(main_node, Node::Kind::ProtocolConformance))
318324
return ThunkKind::ProtocolConformance;
@@ -342,6 +348,8 @@ static const char *GetThunkKindName(ThunkKind kind) {
342348
return "GetThunkTarget";
343349
case ThunkKind::ObjCAttribute:
344350
return "GetThunkTarget";
351+
case ThunkKind::NonObjCAttributeOnCtor:
352+
return "RunToObjcCInteropCtor";
345353
case ThunkKind::Reabstraction:
346354
return "GetThunkTarget";
347355
case ThunkKind::ProtocolConformance:
@@ -363,6 +371,8 @@ static ThunkAction GetThunkAction(ThunkKind kind) {
363371
return ThunkAction::StepThrough;
364372
case ThunkKind::ProtocolConformance:
365373
return ThunkAction::StepIntoConformance;
374+
case ThunkKind::NonObjCAttributeOnCtor:
375+
return ThunkAction::RunToObjcCInteropCtor;
366376
}
367377
}
368378

@@ -537,6 +547,25 @@ static lldb::ThreadPlanSP GetStepThroughTrampolinePlan(Thread &thread,
537547
symbol_name, GetThunkKindName(thunk_kind), thunk_target.c_str());
538548
return CreateRunToAddressPlan(thunk_target, thread, stop_others);
539549
}
550+
case ThunkAction::RunToObjcCInteropCtor: {
551+
LLDB_LOG(log, "SwiftLanguageRuntime: running to "
552+
"objective C constructor from swift.");
553+
static constexpr auto class_path = {
554+
Node::Kind::Constructor, Node::Kind::Class, Node::Kind::Identifier};
555+
std::optional<std::string> class_name = FindClassName(symbol_name, class_path);
556+
if (!class_name)
557+
return nullptr;
558+
std::string ctor_name = llvm::formatv("{0} init", *class_name);
559+
560+
SymbolContextList sc_list;
561+
ModuleFunctionSearchOptions options{/*include_symbols*/ true,
562+
/*include_inlines*/ true};
563+
ModuleList modules = thread.GetProcess()->GetTarget().GetImages();
564+
modules.FindFunctions(RegularExpression(ctor_name), options, sc_list);
565+
566+
ThreadPlanSP plan = CreateThreadPlanRunToAnySc(thread, sc_list, stop_others);
567+
return plan;
568+
}
540569
case ThunkAction::StepIntoConformance: {
541570
// The TTW symbols encode the protocol conformance requirements
542571
// and it is possible to go to the AST and get it to replay the
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#import <Foundation/Foundation.h>
2+
3+
@interface Foo : NSObject
4+
5+
@property (nonnull) NSArray<NSString *> *values;
6+
7+
- (nonnull id)init;
8+
- (nonnull id)initWithString:(nonnull NSString *)value;
9+
- (nonnull id)initWithString:(nonnull NSString *)value andOtherString:(nonnull NSString *) otherValue;
10+
11+
@end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#import "Foo.h"
2+
3+
@implementation Foo
4+
5+
- (id)init {
6+
}
7+
8+
- (id)initWithString:(nonnull NSString *)value {
9+
self->_values = @[value];
10+
return self;
11+
}
12+
13+
- (nonnull id)initWithString:(nonnull NSString *)value andOtherString:(nonnull NSString *) otherValue {
14+
self->_values = @[value, otherValue];
15+
return self;
16+
}
17+
18+
@end
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
SWIFT_SOURCES := main.swift
2+
SWIFT_BRIDGING_HEADER := bridging-header.h
3+
OBJC_SOURCES := Foo.m
4+
SWIFT_OBJC_INTEROP := 1
5+
6+
include Makefile.rules
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import lldb
2+
from lldbsuite.test.lldbtest import *
3+
from lldbsuite.test.decorators import *
4+
import lldbsuite.test.lldbutil as lldbutil
5+
6+
7+
class TestSwiftObjcProtocol(TestBase):
8+
@skipUnlessDarwin
9+
@swiftTest
10+
def test(self):
11+
self.build()
12+
(target, process, thread, breakpoint) = lldbutil.run_to_source_breakpoint(
13+
self, "break here", lldb.SBFileSpec("main.swift")
14+
)
15+
16+
# Go to the first constructor, assert we can step into it.
17+
thread.StepInto()
18+
self.assertEqual(thread.stop_reason, lldb.eStopReasonPlanComplete)
19+
self.assertIn("-[Foo init]", thread.frames[0].GetFunctionName())
20+
21+
# Go back to "work" function
22+
thread.StepOut()
23+
self.assertEqual(thread.stop_reason, lldb.eStopReasonPlanComplete)
24+
self.assertIn("work", thread.frames[0].GetFunctionName())
25+
26+
# Go to the next constructor call.
27+
thread.StepOver()
28+
self.assertEqual(thread.stop_reason, lldb.eStopReasonPlanComplete)
29+
self.assertIn("work", thread.frames[0].GetFunctionName())
30+
31+
# Assert we can step into it.
32+
thread.StepInto()
33+
self.assertEqual(thread.stop_reason, lldb.eStopReasonPlanComplete)
34+
self.assertIn("-[Foo initWithString:]", thread.frames[0].GetFunctionName())
35+
36+
# Go back to "work" function
37+
thread.StepOut()
38+
self.assertEqual(thread.stop_reason, lldb.eStopReasonPlanComplete)
39+
self.assertIn("work", thread.frames[0].GetFunctionName())
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#import "Foo.h"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
func work() {
2+
let noParams = Foo() // break here
3+
let oneParam = Foo(string: "Bar")
4+
print("done")
5+
}
6+
7+
work()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#include "bridging-header.h"
2+
3+
@implementation ObjcClass
4+
5+
- (instancetype)init {
6+
self = [super init];
7+
if (self) {
8+
self.someString = @"The objc string";
9+
}
10+
return self;
11+
}
12+
13+
+ (id<ObjcProtocol>)getP {
14+
return [ObjcClass new];
15+
}
16+
@end

0 commit comments

Comments
 (0)