Project

General

Profile

Actions

Feature #18910

closed

Improve maintainability of LLDB helpers

Added by eightbitraptor (Matt V-H) over 2 years ago. Updated over 2 years ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:109195]

Description

Github PR: #6129

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 to lldb_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 optionally get_short_help and get_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 the lldb 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, and rclass_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 call super in the __init__ to clean up the __call__ a little more.
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0