From: "eightbitraptor (Matthew Valentine-House)" Date: 2022-07-13T13:33:40+00:00 Subject: [ruby-core:109195] [Ruby master Feature#18910] Improve maintainability of LLDB helpers Issue #18910 has been reported by eightbitraptor (Matthew Valentine-House). ---------------------------------------- Feature #18910: Improve maintainability of LLDB helpers https://bugs.ruby-lang.org/issues/18910 * Author: eightbitraptor (Matthew Valentine-House) * Status: Open * Priority: Normal ---------------------------------------- **[Github PR: #6129](https://github.com/ruby/ruby/pull/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. -- https://bugs.ruby-lang.org/ Unsubscribe: