Feature #18910
closedImprove maintainability of LLDB helpers
Description
Summary¶
lldb_cruby.py
manages lldb custom commands using functions. The file is a large list of Python functions, and an init handler to map some of the Python functions into the debugger, to enable execution of custom logic during a debugging session.
Since LLDB 3.7 (September 2015) there has also been support for using python classes rather than bare functions, as long as those classes implement a specific interface.
This PR Introduces some more defined structure to the LLDB helper functions by switching from the function based implementation to the class based one, and providing an auto-loading mechanism by which new functions can be loaded.
The intention behind this change is to make working with the LLDB helpers easier, by reducing code duplication, providing a consistent structure and a clearer API for developers.
Background¶
The current function based approach has some advantages and disadvantages
Advantages:
- Adding new code is easy.
- All the code is self contained and searchable.
Disadvantages:
- No visible organisation of the file contents. This means
- Hard to tell which functions are utility functions and which are available to you in a debugging session
- Lots of code duplication within lldb functions
- Large files quickly become intimidating to work with - for example,
lldb_disasm.py
was implemented as a seperate Python module because it was easier to start with a clean slate than add significant amounts of code tolldb_cruby.py
What are we doing here?¶
This PR attempts, to fix the disadvantages of the current approach and maintain, or enhance, the benefits. The new structure of a command looks like this;
# Our class inherits from RbBaseCommand which provides access to Ruby globals
# and utility helpers
class TestCommand(RbBaseCommand):
# program is the keyword the user will type in lldb to execute this command
program = "test"
# help_string will be displayed in lldb when the user uses the help functions
help_string = "This is a test command to show how to implement lldb commands"
# Init is required by the API but we don't need to do anything here
def __init__(self, debugger, _internal_dict):
pass
# call is where our command logic will be implemented
def __call__(self, debugger, command, exe_ctx, result):
# this call to super sets up the Ruby globals, ie. everything that currently
# done by the following code in the current lldb helpers
# if not ('RUBY_Qfalse' in globals()):
# lldb_init(debugger)
#
super().__call__(self, debugger, command, exe_ctx, result)
# Set up some commonly used variables. Nearly every existing lldb command
# currently does this by hand.
target, process, thread, frame = self.build_environment(debugger)
# rest of our code goes here
If the command fulfils the following criteria it will then be auto-loaded when an lldb session is started:
- The package file must exist inside the
commands
directory and the filename must end in_command.py
- The package must implement a class whose name ends in
Command
- The class must implement the
lldb
API (at minimum this means defining__init__
and__call__
, but also optionallyget_short_help
andget_long_help
) - The class must have a class variable
package
that is a String. This is the name of the command you'll call in thelldb
debugger.
Advantages/Disadvantages of this approach¶
Advantages:
- Less code duplication.
- We can now use inheritance and mix-ins to provide shared functionality between commands.
- Commands are easier to read and reason about, only code relevant to the command behaviour is in
__call__
most everything else can be abstracted away.
- Easier to see at a glance what functions are available, what they do, and what commands they're mapped to, rather than jumping around a large file
Disadvantages
- Slightly more boilerplate code required to add new commands - although I've attempted to mitigate this by providing a template -
commands/command_template.py
that can be copied and renamed to use as a starting point.
Notes¶
- Only two smaller commands have been ported to this new structure at the moment:
heap_page
, andrclass_ext
. I've kept these commits seperate in order to show what work is required to port existing commands over to the new style. - More commands will be ported over individually in seperate PR's, until eventually we'll have no more function based commands at all and can deprecate them.
- The debugger instance passed to
__call__
is different to the one passed to__init__
- and attempting to store one in an ivar and use it later doesn't work for me. This means I can't call super in the initialiser to setup the thread variables, and have them exist and work by the time the function is called. I'd like to find a way around this issue and callsuper
in the__init__
to clean up the__call__
a little more.
Updated by ianks (Ian Ker-Seymer) about 2 years ago
On a related note, I’ve often found myself wanting access to these helpers when developing native extensions. I’ve copy-pasta’d them in the past, but I wonder if Ruby could support including them for debug builds?
Updated by eightbitraptor (Matthew Valentine-House) about 2 years ago
ianks (Ian Ker-Seymer) wrote in #note-1:
On a related note, I’ve often found myself wanting access to these helpers when developing native extensions. I’ve copy-pasta’d them in the past, but I wonder if Ruby could support including them for debug builds?
One thing that I'd like to do in a future PR is experiment with having these LLDB helpers inside a gem. Despite the slightly odd experience of having a Ruby gem that contained only Python code, we'd get a couple of benefits. 1) We'd be able to tie the helpers to a specific version of Ruby - they're very closely coupled to Ruby internals, and can be brittle when implementation details in the VM change. and 2) It would then be more easily usable for folks writing native extensions. What do you think?
Updated by ianks (Ian Ker-Seymer) about 2 years ago
eightbitraptor (Matthew Valentine-House) wrote in #note-2:
ianks (Ian Ker-Seymer) wrote in #note-1:
On a related note, I’ve often found myself wanting access to these helpers when developing native extensions. I’ve copy-pasta’d them in the past, but I wonder if Ruby could support including them for debug builds?
One thing that I'd like to do in a future PR is experiment with having these LLDB helpers inside a gem. Despite the slightly odd experience of having a Ruby gem that contained only Python code, we'd get a couple of benefits. 1) We'd be able to tie the helpers to a specific version of Ruby - they're very closely coupled to Ruby internals, and can be brittle when implementation details in the VM change. and 2) It would then be more easily usable for folks writing native extensions. What do you think?
👍🏻
Updated by eightbitraptor (Matthew Valentine-House) almost 2 years ago
- Status changed from Open to Closed
Applied in changeset git|f1ccfa0c2c200c9443fbfc3f1ac3acbdd3e35559.
[ci-skip][Feature #18910][lldb] Provide class framework for lldb commands
lldb_cruby.py
manages lldb custom commands using functions. The file
is a large list of Python functions, and an init handler to map some of
the Python functions into the debugger, to enable execution of custom
logic during a debugging session.
Since LLDB 3.7 (September 2015) there has also been support for using
python classes rather than bare functions, as long as those classes
implement a specific interface.
This PR Introduces some more defined structure to the LLDB helper
functions by switching from the function based implementation to the
class based one, and providing an auto-loading mechanism by which new
functions can be loaded.
The intention behind this change is to make working with the LLDB
helpers easier, by reducing code duplication, providing a consistent
structure and a clearer API for developers.
The current function based approach has some advantages and
disadvantages
Advantages:
- Adding new code is easy.
- All the code is self contained and searchable.
Disadvantages:
- No visible organisation of the file contents. This means
- Hard to tell which functions are utility functions and which are
available to you in a debugging session - Lots of code duplication within lldb functions
- Hard to tell which functions are utility functions and which are
- Large files quickly become intimidating to work with - for example,
lldb_disasm.py
was implemented as a seperate Python module because
it was easier to start with a clean slate than add significant amounts
of code tolldb_cruby.py
This PR attempts, to fix the disadvantages of the current approach and
maintain, or enhance, the benefits. The new structure of a command looks
like this;
class TestCommand(RbBaseCommand):
# program is the keyword the user will type in lldb to execute this command
program = "test"
# help_string will be displayed in lldb when the user uses the help functions
help_string = "This is a test command to show how to implement lldb commands"
# call is where our command logic will be implemented
def call(self, debugger, command, exe_ctx, result):
pass
If the command fulfils the following criteria it will then be
auto-loaded when an lldb session is started:
- The package file must exist inside the
commands
directory and the
filename must end in_command.py
- The package must implement a class whose name ends in
Command
- The class inherits from
RbBaseCommand
or at minimum a class that
shares the same interface asRbBaseCommand
(at minimum this means
defining__init__
and__call__
, and using__call__
to call
call
which is defined in the subclasses). - The class must have a class variable
package
that is a String. This
is the name of the command you'll call in thelldb
debugger.