This is an archive of the discontinued LLVM Phabricator instance.

[lldb] Fix {break,watch}point command function stopping behaviour
ClosedPublic

Authored by mib on Feb 23 2023, 4:58 PM.

Details

Summary

In order to run a {break,watch}point command, lldb can resolve to the
script interpreter to run an arbitrary piece of code or call into a
user-provided function. To do so, we will generate a wrapping function,
where we first copy lldb's internal dictionary keys into the
interpreter's global dictionary, copied inline the user code before
resetting the global dictionary to its previous state.

However, {break,watch}point commands can optionally return a value that
would tell lldb whether we should stop or not. This feature was
only implemented for breakpoint commands and since we inlined the user
code directly into the wrapping function, introducing an early return,
that caused lldb to let the interpreter global dictionary tinted with the
internal dictionary keys.

This patch fixes that issue while also adding the stopping behaviour to
watchpoint commands.

To do so, this patch refactors the {break,watch}point command creation
method, to let the lldb wrapper function generator know if the user code is
a function call or a arbitrary expression.

Then the wrapper generator, if the user input was a function call, the
wrapper function will call the user function and save the return value into
a variable. If the user input was an arbitrary expression, the wrapper will
inline it into a nested function, call the nested function and save the
return value into the same variable. After resetting the interpreter global
dictionary to its previous state, the generated wrapper function will return
the varible containing the return value.

rdar://105461140

Signed-off-by: Med Ismail Bennani <medismail.bennani@gmail.com>

Diff Detail

Event Timeline

mib created this revision.Feb 23 2023, 4:58 PM
Herald added a project: Restricted Project. · View Herald TranscriptFeb 23 2023, 4:58 PM
mib requested review of this revision.Feb 23 2023, 4:58 PM
delcypher added inline comments.
lldb/source/Commands/CommandObjectWatchpointCommand.cpp
425–426

@mib There's a reason I didn't do it this way when I tried to fix this locally.

The python stub function we generate would look something like this with your patch.

def lldb_autogen_python_wp_callback_func__0 (frame, wp, internal_dict):
     global_dict = globals()
     new_keys = internal_dict.keys()
     old_keys = global_dict.keys()
     global_dict.update (internal_dict)
     if True:
         return call_user_function(frame, wp, internal_dict)
     for key in new_keys:
         internal_dict[key] = global_dict[key]
         if key not in old_keys:
            del global_dict[key]

with your early return the logic for updating internal_dict and global_dict will not run. I'm not entirely sure what the purpose of this is but if its important then we need to implement this patch another way.

Here's another way we could do it. You could take the patch I originally wrote but change ScriptInterpreterPythonImpl::GenerateFunction(...) to take an additional parameter bool ReturnValue that is false by default. This parameter when true would do the work my patch did to make sure we use the return value from the last user statement evaluated. For the watchpoint evaluation we can pass a true value. For the other cases ReturnValue will be false so there will be no behavior change there.

lldb/test/API/commands/watchpoints/watchpoint_commands/command/TestWatchpointCommandPython.py
159

This self.expect() fails when the bug hasn't been fixed, correct?

mib added a subscriber: jingham.Feb 24 2023, 11:03 AM
mib added inline comments.
lldb/source/Commands/CommandObjectWatchpointCommand.cpp
425–426

It's funny you're proposing this approach because we had the exact same idea when looking at this bug with @bulbazord.

I ended up going with this implementation because if you use and one-liner -o instead of a python function -F for your watchpoint callback, you'd also get the early return behavior.

I thought it would be better to stay consistant even if that implies leaving some internal_dict keys in the global_dict (because of the early return).

May be @jingham has some opinions about this.

lldb/test/API/commands/watchpoints/watchpoint_commands/command/TestWatchpointCommandPython.py
159

Yup

bulbazord accepted this revision.Feb 24 2023, 11:15 AM

I'm alright with this patch since it at least makes watchpoint commands consistent with breakpoint commands (w.r.t the -F flag).

lldb/source/Commands/CommandObjectWatchpointCommand.cpp
425–426

The behavior implemented in this patch makes the -F flag consistent with the way it behaves with breakpoint commands. If we want to make sure that internal_dict and global_dict are appropriately updated instead of doing an early return, then I think that should be done in a follow-up that would apply to both breakpoints and watchpoints (because they shouldn't really be behaving differently w.r.t. commands). A simple idea for that would be to store the result of the function call in a variable and return it at the very end.

lldb/test/API/commands/watchpoints/watchpoint_commands/command/TestWatchpointCommandPython.py
1

I checked, the difference here is a byte order mark: https://en.wikipedia.org/wiki/Byte_order_mark

This just removes 3 bytes: ef bb bf

This revision is now accepted and ready to land.Feb 24 2023, 11:15 AM
mib added inline comments.Feb 24 2023, 11:16 AM
lldb/source/Commands/CommandObjectWatchpointCommand.cpp
425–426

+1

jingham requested changes to this revision.Feb 24 2023, 11:27 AM

IIUC, not removing elements from the internal dict will mean cross-contamination between the python states when you have multiple concurrent debuggers, which for instance Xcode does a lot. New variables would appear in the other session, but not linked to this debugger. That could cause subtle problems, which it's worth doing some extra work to avoid.

We should also make sure in the test that a watchpoint command that returns nothing still works. That's suppose to be the equivalent of returning True.

This revision now requires changes to proceed.Feb 24 2023, 11:27 AM
mib added a comment.Feb 24 2023, 11:34 AM

IIUC, not removing elements from the internal dict will mean cross-contamination between the python states when you have multiple concurrent debuggers, which for instance Xcode does a lot. New variables would appear in the other session, but not linked to this debugger. That could cause subtle problems, which it's worth doing some extra work to avoid.

We've discuss offline with @jingham, and I'll fix this on a new diff to separate the 2 issues.

We should also make sure in the test that a watchpoint command that returns nothing still works. That's suppose to be the equivalent of returning True.

I'll update this PR to test this.

mib updated this revision to Diff 500317.Feb 24 2023, 3:15 PM
mib retitled this revision from [lldb] Fix watchpoint command function stopping behavior to [lldb] Fix {break,watch}point command function stopping behavior.
mib edited the summary of this revision. (Show Details)

Address @jingham & @delcypher feedbacks.

bulbazord requested changes to this revision.Feb 24 2023, 3:41 PM
bulbazord added inline comments.
lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
1310–1320

I feel like this is subtly wrong. For example, given the user provided function:

def func(frame, bp_loc, internal_dict):
    if frame.name == "main":
        return True
    return False

This will be transformed into:

def __user_callback():
    nonlocal __return_val
    if frame.name == "main":
        __return_val = True
    __return_val = False

Assume frame.name == "main": In the first one, we will return True. In the second one, __return_val will be False.

Maybe we can insert the input into __user_callback as-is and do __return_val = __user_callback()?

This revision now requires changes to proceed.Feb 24 2023, 3:41 PM
bulbazord added inline comments.Feb 24 2023, 3:44 PM
lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
1310–1320

To be clear, I think this will work with -F because there's just one return statement inserted a few frames up.

This also may fail with -o if you concatenate multiple lines in one. For example, -o "print('foo'); return frame.name == 'main'". This is one line, so we'll never set __return_val to frame.name == 'main'. You might need to do further processing on the input to break it into multiple lines.

mib marked 4 inline comments as done.Feb 24 2023, 8:33 PM
mib updated this revision to Diff 500349.Feb 24 2023, 8:39 PM
mib retitled this revision from [lldb] Fix {break,watch}point command function stopping behavior to [lldb] Fix {break,watch}point command function stopping behaviour.
mib edited the summary of this revision. (Show Details)

The third implementation is the charm 😝:

  • If the user input is a oneliner, we wrap it into a nested function.
  • Then we call the function (either the nested function or the user provided python function) and capture the return value in a variable.
  • Finally, after resetting the global dictionary to its previous state, we return the variable that contains the user's return value.
mib marked an inline comment as done.Feb 24 2023, 8:40 PM
delcypher added inline comments.Feb 27 2023, 11:46 AM
lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
1317

Why do we need to loop over multiple lines in this is a "oneliner"?

Is num_lines == 1 equivalent to is_oneliner?

If yes, then the is_oneliner parameter is not needed.
If no, then is_oneliner should probably be renamed to make sure this confusion cannot be made.

1331

Why are we erroring here? It looks like this is something that the old code supported. I would is expect is_oneliner to imply there are multiple lines in which case shouldn't we support that here?

bulbazord added inline comments.Feb 27 2023, 1:05 PM
lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
1317

+1

1331

+1

jingham added inline comments.Feb 27 2023, 2:14 PM
lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
1317

You can do:

(lldb) breakpoint command add -s python -o "first_python_line" -o "second_python_line"

which give you a "one-line" command with two lines.

mib updated this revision to Diff 500975.Feb 27 2023, 5:09 PM

Rename is_oneliner flag to is_callback and swap the logic.

delcypher accepted this revision.Feb 27 2023, 6:31 PM

@mib Thanks for working on this. LGTM (with very minor nits), but you should probably wait for someone who works on LLDB more frequently than me to give you the ok.

lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
1310–1340

Nit: the space between .update and (internal_dict) is probably an old mistake. We can probably tidy that up seeing as we're already touching the line.

1328

Nit: Might be worth leaving a comment here on why we're wrapping the code in a nested function.

mib updated this revision to Diff 501240.Feb 28 2023, 11:36 AM
mib marked an inline comment as done.

Address @delcypher comment.

bulbazord accepted this revision.Feb 28 2023, 11:38 AM

LGTM thanks!

This revision was not accepted when it landed; it landed in state Needs Review.Feb 28 2023, 11:40 AM
This revision was landed with ongoing or failed builds.
This revision was automatically updated to reflect the committed changes.

You're passing is_callback by value so the const is close to redundant. I think there's an "Effective C++" chapter dedicated to this. LLVM is pretty conservative in marking this as const and while LLDB uses it a bit more this isn't common at all. The current code now makes you wonder why this is different than has_extra_args. Even if that does get modified in some of the function's implementation, that's an implementation detail that shouldn't be exposed through the interface.

This broke Lua support:

In file included from /w/src/llvm.org/lldb/source/Plugins/ScriptInterpreter/Lua/ScriptInterpreterLua.cpp:9:
/w/src/llvm.org/lldb/source/Plugins/ScriptInterpreter/Lua/ScriptInterpreterLua.h:95:68: error: non-virtual member function marked 'override' hides virtual member function
                                    const char *command_body_text) override;
                                                                   ^
/w/src/llvm.org/lldb/include/lldb/Interpreter/ScriptInterpreter.h:415:16: note: hidden overloaded virtual function 'lldb_private::ScriptInterpreter::SetWatchpointCommandCallback' declared here: different number of parameters (3 vs 2)
  virtual void SetWatchpointCommandCallback(WatchpointOptions *wp_options,
               ^

This broke Lua support:

In file included from /w/src/llvm.org/lldb/source/Plugins/ScriptInterpreter/Lua/ScriptInterpreterLua.cpp:9:
/w/src/llvm.org/lldb/source/Plugins/ScriptInterpreter/Lua/ScriptInterpreterLua.h:95:68: error: non-virtual member function marked 'override' hides virtual member function
                                    const char *command_body_text) override;
                                                                   ^
/w/src/llvm.org/lldb/include/lldb/Interpreter/ScriptInterpreter.h:415:16: note: hidden overloaded virtual function 'lldb_private::ScriptInterpreter::SetWatchpointCommandCallback' declared here: different number of parameters (3 vs 2)
  virtual void SetWatchpointCommandCallback(WatchpointOptions *wp_options,
               ^

@mib 👆

mib added a comment.Mar 2 2023, 10:58 AM

This broke Lua support:

In file included from /w/src/llvm.org/lldb/source/Plugins/ScriptInterpreter/Lua/ScriptInterpreterLua.cpp:9:
/w/src/llvm.org/lldb/source/Plugins/ScriptInterpreter/Lua/ScriptInterpreterLua.h:95:68: error: non-virtual member function marked 'override' hides virtual member function
                                    const char *command_body_text) override;
                                                                   ^
/w/src/llvm.org/lldb/include/lldb/Interpreter/ScriptInterpreter.h:415:16: note: hidden overloaded virtual function 'lldb_private::ScriptInterpreter::SetWatchpointCommandCallback' declared here: different number of parameters (3 vs 2)
  virtual void SetWatchpointCommandCallback(WatchpointOptions *wp_options,
               ^

@mib 👆

Yup, saw that. We don't have any bot that tests LUA so this went unnoticed ... I'm rebuilding lldb now with lua, will come up with a fix in a few :)

mib added a comment.Mar 2 2023, 12:47 PM

@kparzysz I landed the fix in ab81fc29d9f7a8f8807293fc987f68f9ccf42259. Let me know if you still have some issues. Thanks!

Looks good. Thanks for the quick fix @mib!