llvm-project/lldb/examples/python/templates/scripted_symbol_locator.py
rchamala 1ee03d1e09
[lldb] Add ScriptedSymbolLocator plugin for source file resolution (#181334)
## Summary                                                        
                                                                    
Based on discussion from
[RFC](https://discourse.llvm.org/t/rfc-python-callback-for-source-file-resolution/83545),
this PR adds a new `SymbolLocatorScripted` plugin that allows Python
scripts to implement custom symbol and source file resolution logic.
This enables downstream users to build custom symbol servers, source
file remapping, and build artifact resolution entirely in Python.
                                                                    
  ### Changes

- Adds `LocateSourceFile()` to the SymbolLocator plugin interface,
called during source path resolution with a fully loaded `ModuleSP`, so
the plugin has access to the module's UUID, file paths, and symbols.
- Adds `SymbolLocatorScripted` plugin that delegates all four
SymbolLocator methods (`LocateExecutableObjectFile`,
`LocateExecutableSymbolFile`, `DownloadObjectAndSymbolFile`,
`LocateSourceFile`) to a user-provided Python class.
- Adds `ScriptedSymbolLocatorPythonInterface` to bridge C++ calls to
Python, with proper GIL management and error handling.
- Results for `LocateSourceFile` are cached per (module UUID, source
file) pair.
- The Python class is configured via: `settings set
plugin.symbol-locator.scripted.script-class module.ClassName`

  ### Python class interface

  ```python
  class MyLocator:
      def __init__(self, exe_ctx, args): ...
      def locate_source_file(self, module, original_source_file):
  ...
      def locate_executable_object_file(self, module_spec): ...
      def locate_executable_symbol_file(self, module_spec,
  default_search_paths): ...
      def download_object_and_symbol_file(self, module_spec,
  force_lookup, copy_executable): ...
```

  ### Test plan
```
  Added TestScriptedSymbolLocator.py with 3 test cases:
  - test_locate_source_file — verifies the locator resolves source
  files, receives a valid SBModule with UUID, and remaps paths correctly
  - test_locate_source_file_none_fallthrough — verifies returning
None falls through to default LLDB resolution, and that having no script
  class set works normally
  - test_invalid_script_class — verifies graceful handling of
  invalid class names without crashing
```

Co-authored-by: Rahul Reddy Chamala <rachamal@fb.com>
2026-02-14 07:39:00 -08:00

221 lines
8.2 KiB
Python

from abc import ABCMeta, abstractmethod
import os
import lldb
class ScriptedSymbolLocator(metaclass=ABCMeta):
"""
The base class for a scripted symbol locator.
Most of the base class methods are optional and return ``None`` to fall
through to LLDB's default resolution. Override only the methods you need.
Configuration::
(lldb) command script import /path/to/my_locator.py
(lldb) target symbols scripted register -C my_locator.MyLocator \\
[-k key -v value ...]
"""
@abstractmethod
def __init__(self, exe_ctx, args):
"""Construct a scripted symbol locator.
Args:
exe_ctx (lldb.SBExecutionContext): The execution context for
the scripted symbol locator.
args (lldb.SBStructuredData): A Dictionary holding arbitrary
key/value pairs used by the scripted symbol locator.
"""
target = None
self.target = None
self.args = None
if isinstance(exe_ctx, lldb.SBExecutionContext):
target = exe_ctx.target
if isinstance(target, lldb.SBTarget) and target.IsValid():
self.target = target
self.dbg = target.GetDebugger()
if isinstance(args, lldb.SBStructuredData) and args.IsValid():
self.args = args
def locate_source_file(self, module, original_source_file):
"""Locate the source file for a given module.
Called when LLDB resolves source file paths during stack frame
display, breakpoint resolution, or source listing. This is the
primary method for implementing source file remapping based on
build IDs.
The module is a fully loaded ``SBModule`` (not an ``SBModuleSpec``),
so you can access its UUID, file path, platform file path,
symbol file path, sections, and symbols.
Results are cached per (module UUID, source file) pair, so this
method is called at most once per unique combination.
Args:
module (lldb.SBModule): The loaded module containing debug
info. Use ``module.GetUUIDString()`` to get the build ID
for looking up the correct source revision.
original_source_file (str): The original source file path
as recorded in the debug info.
Returns:
lldb.SBFileSpec: The resolved file spec, or None to fall
through to LLDB's default source resolution.
"""
return None
def locate_executable_object_file(self, module_spec):
"""Locate the executable (object) file for a given module.
Called when LLDB needs to find the binary for a module during
target creation or module loading. For example, when loading a
minidump, LLDB calls this for each shared library referenced
in the dump.
Args:
module_spec (lldb.SBModuleSpec): The module specification
containing the UUID, file path, architecture, and other
search criteria.
Returns:
lldb.SBFileSpec: The located executable, or None to fall
through to LLDB's default search.
"""
return None
def locate_executable_symbol_file(self, module_spec, default_search_paths):
"""Locate the symbol file for a given module.
Called when LLDB needs to find separate debug symbols (e.g.,
``.dSYM`` bundles on macOS, ``.debug`` files on Linux, ``.dwp``
files for split DWARF) for a module.
Args:
module_spec (lldb.SBModuleSpec): The module specification
containing the UUID and file path to search for.
default_search_paths (list): A list of default search paths
to check.
Returns:
lldb.SBFileSpec: The located symbol file, or None to fall
through to LLDB's default search.
"""
return None
def download_object_and_symbol_file(
self, module_spec, force_lookup, copy_executable
):
"""Download both the object file and symbol file for a module.
Called when LLDB needs to download a binary and its debug symbols
from a remote source (e.g., a symbol server, build artifact
store, or cloud storage). This is the last method called in the
resolution chain, typically as a fallback when local lookups
fail.
Args:
module_spec (lldb.SBModuleSpec): The module specification
containing the UUID and file path to download.
force_lookup (bool): If True, skip any cached results and
force a fresh lookup.
copy_executable (bool): If True, copy the executable to a
local path.
Returns:
bool: True if the download succeeded, False otherwise.
"""
return False
class LocalCacheSymbolLocator(ScriptedSymbolLocator):
"""Example locator that resolves files from a local cache directory.
Demonstrates how to subclass ``ScriptedSymbolLocator`` to implement
custom symbol and source file resolution. This locator looks up files
in a local directory structure organized by build ID (UUID)::
<cache_dir>/
<uuid>/
<binary_name>
<binary_name>.debug
src/
main.cpp
...
Usage::
(lldb) command script import scripted_symbol_locator
(lldb) target symbols scripted register \\
-C scripted_symbol_locator.LocalCacheSymbolLocator \\
-k cache_dir -v "/path/to/cache"
(lldb) target create --core /path/to/minidump.dmp
(lldb) bt
The locator searches for:
- Executables: ``<cache_dir>/<uuid>/<filename>``
- Symbol files: ``<cache_dir>/<uuid>/<filename>.debug``
- Source files: ``<cache_dir>/<uuid>/src/<basename>``
"""
cache_dir = None
def __init__(self, exe_ctx, args):
super().__init__(exe_ctx, args)
# Allow cache_dir to be set via structured data args.
if self.args:
cache_dir_val = self.args.GetValueForKey("cache_dir")
if cache_dir_val and cache_dir_val.IsValid():
val = cache_dir_val.GetStringValue(256)
if val:
LocalCacheSymbolLocator.cache_dir = val
def _get_cache_path(self, uuid_str, *components):
"""Build a path under the cache directory for a given UUID.
Args:
uuid_str (str): The module's UUID string.
*components: Additional path components (e.g., filename).
Returns:
str: The full path, or None if cache_dir is not set or the
UUID is empty.
"""
if not self.cache_dir or not uuid_str:
return None
return os.path.join(self.cache_dir, uuid_str, *components)
def locate_source_file(self, module, original_source_file):
"""Look up source files under ``<cache_dir>/<uuid>/src/``."""
uuid_str = module.GetUUIDString()
basename = os.path.basename(original_source_file)
path = self._get_cache_path(uuid_str, "src", basename)
if path and os.path.exists(path):
return lldb.SBFileSpec(path, True)
return None
def locate_executable_object_file(self, module_spec):
"""Look up executables under ``<cache_dir>/<uuid>/``."""
uuid_str = module_spec.GetUUIDString()
filename = os.path.basename(module_spec.GetFileSpec().GetFilename() or "")
if not filename:
return None
path = self._get_cache_path(uuid_str, filename)
if path and os.path.exists(path):
return lldb.SBFileSpec(path, True)
return None
def locate_executable_symbol_file(self, module_spec, default_search_paths):
"""Look up debug symbol files under ``<cache_dir>/<uuid>/``."""
uuid_str = module_spec.GetUUIDString()
filename = os.path.basename(module_spec.GetFileSpec().GetFilename() or "")
if not filename:
return None
debug_path = self._get_cache_path(uuid_str, filename + ".debug")
if debug_path and os.path.exists(debug_path):
return lldb.SBFileSpec(debug_path, True)
return None