-
Notifications
You must be signed in to change notification settings - Fork 340
[lldb] Fix stepping into Objective-C interop ctors #10697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: swift/release/6.2
Are you sure you want to change the base?
[lldb] Fix stepping into Objective-C interop ctors #10697
Conversation
75bd162
to
6d84e65
Compare
@swift-ci test |
modules.FindFunctionSymbols(ConstString(target_func), eFunctionNameTypeFull, | ||
sc_list); | ||
if (sc_list.GetSize() != 1 || sc_list[0].symbol == nullptr) | ||
return nullptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return nullptr; | |
return {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit confused by this suggestion, the function returns a pointer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It returns a ThreadPlanSP (the success return returns make_shared). Apparently C++ is smart enough to turn nullptr into a default constructed shared pointer, but I agree with Adrian, that's a little magical for my taste.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, I will change, but note my dissent here in case it persuades you.
- A reader looking at
{}
will not get any of the mental warnings a nullptr brings. - Share pointers are, for all intents and purposes, pointers. Therefore there is nothing magical about the
nullptr
constructor: it's just a pointer object being constructed from a pointer value, akin to returningnullopt
in a function producingstd::optional<T>
. - Reading
return {}
is more likely to make the reader think: "wait, what is the return type of this function again?" (plus it requires the reader knowing what a default-constructedThreadPlanSP
does) whereas readingreturn nullptr
is explicit. - The final reason to keep it as nullptr is that all other places in this file are doing that, so this would stand out.
If either of those convince you, I'll happily switch back to nullptr :)
I didn't mean to write an essay, but I do believe this is the wrong direction, so it is worth talking through the decision.
|
||
/// Demangle `symbol_name` and extracts the text at the node described by | ||
/// `node_path`, if it exists. | ||
static std::optional<std::string> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any value in the optional or can you just use the empty string as null value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can change this to implicitly use the empty string as a "failed" value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Swift doesn't have any "anonymous" entities like C & C++ do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hopefully nobody has anonymous classes!
From the test failure:
Standard library with debug info strikes again. |
SymbolContextList sc_list; | ||
modules.FindFunctionSymbols(ConstString(target_func), eFunctionNameTypeFull, | ||
sc_list); | ||
if (sc_list.GetSize() != 1 || sc_list[0].symbol == nullptr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we care that there's only one? For instance, if this is an ObjC method, two shared libraries can have implementations of the same ObjC class. The runtime will pick which one to use, but we don't actually know from symbols which one that is.
And since this is a thread specific breakpoint, the only way setting a breakpoint on the "wrong" function as well as the "right" would cause trouble is if running from the thunk to the target function called the wrong function, which seems unlikely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good points, I'll change this. This way we can also delegate part of the functionality to the other function introduced in this commit
NodePointer demangled_node = | ||
SwiftLanguageRuntime::DemangleSymbolAsNode(symbol_name, ctx); | ||
|
||
NodePointer class_node = childAtPath(demangled_node, node_path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if childAtPath handles null NodePointer arguments, still seem easier to follow if you checked the return here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do.
If we're taking this approach, it would be worth being consistent everywhere (in a separate patch) and changing childAtPath
to take a reference instead, so that it no longer accepts nullptrs by definition.
While I believe the current approach leads to more succinct code, either one is fine as long as we're consistent. The only undesirable outcome is the middle ground where the callee handles nullptrs but callers are also expected to be checking. Either we trust the API to do things correctly (the API said it would when it declared itself taking a pointer), or we change the API to take a reference. APIs that accept pointers but don't handle null are bad APIs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ops, I forgot to do this. Will push another patch shortly
edit: done
|
||
/// If sc_list is non-empty, returns a plan that runs to any of its addresses. | ||
/// Otherwise, returns nullptr. | ||
static ThreadPlanSP CreateThreadPlanRunToAnySc(Thread &thread, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd abbreviate SymbolContext to SC
not Sc
, the latter looks weird.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, this is a generally useful, not swift specific bit of functionality, so it's wrong to have it in a Swift-specific file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RunToAnySc is also a really ambitious function name. Maybe RunToSCInList?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RunToAnySc is also a really ambitious function name. Maybe RunToSCInList?
Good idea!
Also, this is a generally useful, not swift specific bit of functionality, so it's wrong to have it in a Swift-specific file.
I agree we should do that, but I think it is important to take this first step -- where we identified a useful piece of logic in the middle of a big function -- in a self-contained commit. Then we can look upstream, find other places that require this logic, create the same function there, and then cherry-pick it here and delete this code.
bool stop_others) { | ||
std::vector<addr_t> load_addresses; | ||
Target &target = thread.GetProcess()->GetTarget(); | ||
for (const SymbolContext &ctor_sc : sc_list) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't call this ctor_sc
, nowhere in this function do you assume the symbol is a ctor, so that's just confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, good catch, an artifact of earlier iterations of this patch
return CreateRunToAddressPlan(thunk_target, thread, stop_others); | ||
} | ||
case ThunkAction::RunToObjcCInteropCtor: { | ||
LLDB_LOG(log, "SwiftLanguageRuntime: running to " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you wait the log output till you've got the class name? It seems useful to know what class we thought we were supposed to run to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could also then log the case where somebody passes you a demangled name you couldn't find the class it should have targeted, which would also be nice to see.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great points, I'll improve the logging here
This seems like a fine strategy, I had some nit picks but nothing serious. |
43d107a
to
c19dd8d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed all review comments, though I did push back on a couple of them, so please let me know if you have follow-up thoughts. While I pushed back, I did what was asked, but I'm interested in the discussion :)
Also applied the usual fix for standard libraries containing debug info. Separately, we should create a helper function for all tests to use when doing this.
@swift-ci test
modules.FindFunctionSymbols(ConstString(target_func), eFunctionNameTypeFull, | ||
sc_list); | ||
if (sc_list.GetSize() != 1 || sc_list[0].symbol == nullptr) | ||
return nullptr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, I will change, but note my dissent here in case it persuades you.
- A reader looking at
{}
will not get any of the mental warnings a nullptr brings. - Share pointers are, for all intents and purposes, pointers. Therefore there is nothing magical about the
nullptr
constructor: it's just a pointer object being constructed from a pointer value, akin to returningnullopt
in a function producingstd::optional<T>
. - Reading
return {}
is more likely to make the reader think: "wait, what is the return type of this function again?" (plus it requires the reader knowing what a default-constructedThreadPlanSP
does) whereas readingreturn nullptr
is explicit. - The final reason to keep it as nullptr is that all other places in this file are doing that, so this would stand out.
If either of those convince you, I'll happily switch back to nullptr :)
I didn't mean to write an essay, but I do believe this is the wrong direction, so it is worth talking through the decision.
NodePointer demangled_node = | ||
SwiftLanguageRuntime::DemangleSymbolAsNode(symbol_name, ctx); | ||
|
||
NodePointer class_node = childAtPath(demangled_node, node_path); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do.
If we're taking this approach, it would be worth being consistent everywhere (in a separate patch) and changing childAtPath
to take a reference instead, so that it no longer accepts nullptrs by definition.
While I believe the current approach leads to more succinct code, either one is fine as long as we're consistent. The only undesirable outcome is the middle ground where the callee handles nullptrs but callers are also expected to be checking. Either we trust the API to do things correctly (the API said it would when it declared itself taking a pointer), or we change the API to take a reference. APIs that accept pointers but don't handle null are bad APIs.
|
||
/// If sc_list is non-empty, returns a plan that runs to any of its addresses. | ||
/// Otherwise, returns nullptr. | ||
static ThreadPlanSP CreateThreadPlanRunToAnySc(Thread &thread, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RunToAnySc is also a really ambitious function name. Maybe RunToSCInList?
Good idea!
Also, this is a generally useful, not swift specific bit of functionality, so it's wrong to have it in a Swift-specific file.
I agree we should do that, but I think it is important to take this first step -- where we identified a useful piece of logic in the middle of a big function -- in a self-contained commit. Then we can look upstream, find other places that require this logic, create the same function there, and then cherry-pick it here and delete this code.
bool stop_others) { | ||
std::vector<addr_t> load_addresses; | ||
Target &target = thread.GetProcess()->GetTarget(); | ||
for (const SymbolContext &ctor_sc : sc_list) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, good catch, an artifact of earlier iterations of this patch
SymbolContextList sc_list; | ||
modules.FindFunctionSymbols(ConstString(target_func), eFunctionNameTypeFull, | ||
sc_list); | ||
if (sc_list.GetSize() != 1 || sc_list[0].symbol == nullptr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good points, I'll change this. This way we can also delegate part of the functionality to the other function introduced in this commit
return CreateRunToAddressPlan(thunk_target, thread, stop_others); | ||
} | ||
case ThunkAction::RunToObjcCInteropCtor: { | ||
LLDB_LOG(log, "SwiftLanguageRuntime: running to " |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great points, I'll improve the logging here
@swift-ci test |
These will be useful to reuse code in upcoming commits.
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 swiftlang#1: 0x00000001000012bc test.out`@nonobjc Foo.init(string:) $sSo3FooC6stringABSS_tcfcTO at <compiler-generated>:0 frame swiftlang#2: 0x0000000100001170 test.out`Foo.__allocating_init(string:) $sSo3FooC6stringABSS_tcfC at Foo.h:0 frame swiftlang#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).
c19dd8d
to
3512b6a
Compare
addressed one piece of feedback that had escaped me. |
@swift-ci test |
The first commit is just hoisting helper functions for re-use. The second commit actually solves the problem.
Please read each commit in isolation, especially the message on the second commit.
rdar://146886271