[lldb/script] Add scripting extension template generator (#209647)

This patch adds a `scripting extension generate <ExtensionType>...`
command that introspects a Python extension base class and emits a
skeleton subclass with `# TODO: Implement` stubs for its abstract
methods (or, with `-a`, every method), then opens the result in an
editor.

Generated imports use `from <module> import <class>`, and the generated
`__init__` forwards its arguments to `super().__init__(...)` since every
base class relies on its constructor to set up attributes
(`self.target`, `self.process`, ...) that inherited, non-overridden
methods depend on. When the host can't open an external editor (e.g.
non-macOS), the command reports that as a message rather than an error,
since the file is already written.

To catch regressions in the generator itself instead of just checking
that a file was produced, this patch adds
`TestScriptingExtensionGenerate.py`, which generates a template for
every extension kind on the fly and drives it through a live lldb
session (`process launch -C`, `breakpoint set -P`, `target stop-hook add
-P`, etc.).

Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
(cherry picked from commit c4962f5721a36e198234019b846d3788738f766a)
This commit is contained in:
Med Ismail Bennani authored and Tobias Hieta committed 2026-07-23 08:09:06 +02:00
1 parent e28ea20ce0
commit 3df0bbdc25
13 files changed
+961 -12

No files matched your search

+3 -2
View File
@@ -687,8 +687,9 @@ public:
static ScriptedInterfaceUsages
GetScriptedInterfaceUsagesAtIndex(uint32_t idx);
static void AutoCompleteScriptedExtension(llvm::StringRef partial_name,
CompletionRequest &request);
static void AutoCompleteScriptedExtension(
llvm::StringRef partial_name, CompletionRequest &request,
lldb::ScriptLanguage language = lldb::eScriptLanguageUnknown);
// REPL
static bool RegisterPlugin(llvm::StringRef name, llvm::StringRef description,
@@ -39,6 +39,7 @@
#include "lldb/Utility/Broadcaster.h"
#include "lldb/Utility/Status.h"
#include "lldb/Utility/StructuredData.h"
#include "lldb/Utility/UnimplementedError.h"
#include "lldb/lldb-private.h"
#include <optional>
@@ -170,6 +171,25 @@ public:
virtual StructuredData::DictionarySP GetInterpreterInfo();
/// Describes one extension to emit into the generated template file.
///
/// `full_name` is the dotted extension name as the user typed it (e.g.
/// `"ScriptedProcess"` or a nested `"...ChildKind"`), used to name the
/// generated class. `path` is the same name split on `.`; its last element
/// selects the ScriptedExtension kind and everything before it is treated
/// as a subclass discriminator. Both StringRefs must outlive the call to
/// GenerateExtensionTemplate; the implementation stores them, not copies.
struct ExtensionTemplateRequest {
llvm::StringRef full_name;
llvm::SmallVector<llvm::StringRef> path;
};
virtual llvm::Expected<FileSpec>
GenerateExtensionTemplate(const std::string &name,
std::vector<ExtensionTemplateRequest> &extensions,
bool generate_non_abstract_methods,
std::string output_file);
~ScriptInterpreter() override = default;
virtual bool Interrupt() { return false; }
@@ -510,6 +530,11 @@ public:
static lldb::ScriptLanguage StringToLanguage(const llvm::StringRef &string);
virtual llvm::Expected<std::string>
ExtensionToImportPath(lldb::ScriptedExtension extension) {
return llvm::make_error<UnimplementedError>();
}
static llvm::StringLiteral
ExtensionToString(lldb::ScriptedExtension extension);
@@ -11,6 +11,7 @@
#include "lldb/Core/PluginManager.h"
#include "lldb/DataFormatters/DataVisualization.h"
#include "lldb/Host/Config.h"
#include "lldb/Host/Host.h"
#include "lldb/Host/OptionParser.h"
#include "lldb/Interpreter/CommandInterpreter.h"
#include "lldb/Interpreter/CommandOptionArgumentTable.h"
@@ -20,6 +21,8 @@
#include "lldb/Interpreter/ScriptInterpreter.h"
#include "lldb/Utility/AnsiTerminal.h"
#include "lldb/Utility/Args.h"
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "llvm/ADT/StringMap.h"
@@ -371,6 +374,188 @@ private:
CommandOptions m_options;
};
#define LLDB_OPTIONS_scripting_extension_generate
#include "CommandOptions.inc"
class CommandObjectScriptingExtensionGenerate : public CommandObjectParsed {
public:
CommandObjectScriptingExtensionGenerate(CommandInterpreter &interpreter)
: CommandObjectParsed(interpreter, "scripting extension generate",
"Generate a scripting extension template. ",
"scripting extension generate") {
AddSimpleArgumentList(eArgTypeScriptedExtension, eArgRepeatPlus);
}
~CommandObjectScriptingExtensionGenerate() override = default;
Options *GetOptions() override { return &m_options; }
class CommandOptions : public Options {
public:
CommandOptions() = default;
~CommandOptions() override = default;
Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
ExecutionContext *execution_context) override {
Status error;
const char short_option =
g_scripting_extension_generate_options[option_idx].short_option;
switch (short_option) {
case 'a':
m_generate_non_abstract_methods = true;
break;
case 'l':
m_language = (lldb::ScriptLanguage)OptionArgParser::ToOptionEnum(
option_arg, GetDefinitions()[option_idx].enum_values,
eScriptLanguageNone, error);
if (!error.Success())
error = Status::FromErrorStringWithFormatv(
"unrecognized value for language '{0}'", option_arg);
break;
case 'n':
m_generated_class_prefix = option_arg.str();
break;
case 'o':
m_output_filepath = option_arg.str();
break;
case 'e': {
bool success;
m_open_editor = OptionArgParser::ToBoolean(option_arg, true, &success)
? eLazyBoolYes
: eLazyBoolNo;
if (!success)
error = Status::FromErrorStringWithFormatv(
"invalid boolean value for -e: '{0}'", option_arg);
} break;
default:
llvm_unreachable("Unimplemented option");
}
return error;
}
void OptionParsingStarting(ExecutionContext *execution_context) override {
m_generate_non_abstract_methods = false;
m_language = lldb::eScriptLanguageNone;
m_generated_class_prefix.clear();
m_output_filepath.clear();
m_open_editor = eLazyBoolCalculate;
}
llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
return llvm::ArrayRef(g_scripting_extension_generate_options);
}
bool m_generate_non_abstract_methods = false;
lldb::ScriptLanguage m_language = lldb::eScriptLanguageNone;
std::string m_generated_class_prefix;
std::string m_output_filepath;
LazyBool m_open_editor = eLazyBoolCalculate;
};
void
HandleArgumentCompletion(CompletionRequest &request,
OptionElementVector &opt_element_vector) override {
// If `-l <lang>` was already given, complete only extensions available
// in that language. Bare `-l` (no value yet) and any parse failures
// fall back to language-agnostic completion.
lldb::ScriptLanguage language = lldb::eScriptLanguageUnknown;
llvm::ArrayRef<OptionDefinition> defs = m_options.GetDefinitions();
for (const OptionArgElement &elem : opt_element_vector) {
if (elem.opt_defs_index < 0 ||
static_cast<size_t>(elem.opt_defs_index) >= defs.size())
continue;
if (defs[elem.opt_defs_index].short_option != 'l' ||
elem.opt_arg_pos <= 0)
continue;
llvm::StringRef value =
request.GetParsedLine().GetArgumentAtIndex(elem.opt_arg_pos);
Status error;
auto candidate =
static_cast<lldb::ScriptLanguage>(OptionArgParser::ToOptionEnum(
value, defs[elem.opt_defs_index].enum_values,
lldb::eScriptLanguageUnknown, error));
if (error.Success())
language = candidate;
break;
}
PluginManager::AutoCompleteScriptedExtension(
request.GetCursorArgumentPrefix(), request, language);
lldb_private::CommandCompletions::InvokeCommonCompletionCallbacks(
GetCommandInterpreter(), lldb::eDiskFileCompletion, request, nullptr);
}
protected:
void DoExecute(Args &command, CommandReturnObject &result) override {
if (command.GetArgumentCount() == 0) {
result.SetError(
Status::FromErrorString("specify extension name to generate"));
return;
}
std::vector<ScriptInterpreter::ExtensionTemplateRequest> extension_requests;
for (size_t i = 0; i < command.GetArgumentCount(); i++) {
llvm::StringRef extension_name = command.GetArgumentAtIndex(i);
llvm::SmallVector<llvm::StringRef> extension_components;
extension_name.split(extension_components, ".");
lldb::ScriptedExtension extension =
ScriptInterpreter::StringToExtension(extension_components.back());
if (extension == eScriptedExtensionInvalid) {
result.SetError(Status::FromErrorStringWithFormatv(
"unknown scripted extension: '{0}'", extension_name));
return;
}
extension_requests.push_back({extension_name, extension_components});
}
lldb::ScriptLanguage language =
(m_options.m_language == lldb::eScriptLanguageNone)
? m_interpreter.GetDebugger().GetScriptLanguage()
: m_options.m_language;
if (language == lldb::eScriptLanguageNone) {
result.AppendError(
"the script-lang setting is set to none - scripting not available");
return;
}
ScriptInterpreter *script_interpreter =
GetDebugger().GetScriptInterpreter(true, language);
if (script_interpreter == nullptr) {
result.AppendError("no script interpreter");
return;
}
auto generated_file_or_err = script_interpreter->GenerateExtensionTemplate(
m_options.m_generated_class_prefix, extension_requests,
m_options.m_generate_non_abstract_methods, m_options.m_output_filepath);
if (!generated_file_or_err) {
result.SetError(generated_file_or_err.takeError());
return;
}
if (m_options.m_open_editor != eLazyBoolNo) {
if (llvm::Error err = Host::OpenFileInExternalEditor(
"", *generated_file_or_err, 1, true)) {
// The template is on disk; failing to launch an editor (e.g. no
// external editor available on this platform) shouldn't fail the
// whole command.
LLDB_LOG_ERROR(GetLog(LLDBLog::Host), std::move(err),
"OpenFileInExternalEditor failed: {0}");
}
}
result.AppendMessageWithFormatv(
"Generated scripting extension template: {0}",
generated_file_or_err->GetPath());
result.SetStatus(eReturnStatusSuccessFinishNoResult);
}
private:
CommandOptions m_options;
};
class CommandObjectMultiwordScriptingExtension : public CommandObjectMultiword {
public:
CommandObjectMultiwordScriptingExtension(CommandInterpreter &interpreter)
@@ -381,6 +566,9 @@ public:
LoadSubCommand(
"list",
CommandObjectSP(new CommandObjectScriptingExtensionList(interpreter)));
LoadSubCommand("generate",
CommandObjectSP(new CommandObjectScriptingExtensionGenerate(
interpreter)));
}
~CommandObjectMultiwordScriptingExtension() override = default;
+28
View File
@@ -1587,6 +1587,34 @@ let Command = "scripting extension list" in {
Desc<"Output the scripted extension list in json format.">;
}
let Command = "scripting extension generate" in {
def scripting_extension_generate_all_methods
: Option<"all", "a">,
Desc<"Generate all the extension methods, not only the abstract ones.">;
def scripting_extension_generate_language
: Option<"language", "l">,
EnumArg<"ScriptLang">,
Desc<"Specify the scripting language. If none is specified the "
"default scripting language is used.">;
def scripting_extension_generate_class_prefix
: Option<"name", "n">,
Arg<"ClassName">,
Required,
Desc<"Specify the class prefix name to include in the generated class "
"definitions.">;
def scripting_extension_generate_output_file
: Option<"output", "o">,
Arg<"Filename">,
Desc<"File location where the extension is generated.">;
def scripting_extension_generate_open_editor
: Option<"edit", "e">,
Arg<"Boolean">,
Desc<"Open the generated file in an external editor after writing "
"it to disk. Defaults to true; pass `-e false` to skip "
"opening the editor (e.g. under SSH without X forwarding, "
"or in scripted runs).">;
}
let Command = "source info" in {
def source_info_count : Option<"count", "c">,
Arg<"Count">,
+21 -9
View File
@@ -20,6 +20,7 @@
#include "lldb/Utility/Status.h"
#include "lldb/Utility/StringList.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/ADT/Twine.h"
#include "llvm/Support/DynamicLibrary.h"
#include "llvm/Support/ErrorExtras.h"
@@ -2126,16 +2127,27 @@ PluginManager::GetScriptedInterfaceUsagesAtIndex(uint32_t idx) {
return {};
}
void PluginManager::AutoCompleteScriptedExtension(llvm::StringRef name,
CompletionRequest &request) {
void PluginManager::AutoCompleteScriptedExtension(
llvm::StringRef name, CompletionRequest &request,
lldb::ScriptLanguage language) {
llvm::StringSet<> emitted;
for (size_t idx = 0; idx < GetNumScriptedInterfaces(); idx++) {
if (auto instance =
GetScriptedInterfaceInstances().GetInstanceAtIndex(idx)) {
llvm::StringLiteral extension_name =
ScriptInterpreter::ExtensionToString(instance->extension);
if (extension_name.starts_with(name))
request.AddCompletion(extension_name);
}
auto instance = GetScriptedInterfaceInstances().GetInstanceAtIndex(idx);
if (!instance)
continue;
// Filter to the requested language when the caller pinned one via
// `-l`. `eScriptLanguageUnknown` means "no filter".
if (language != lldb::eScriptLanguageUnknown &&
instance->language != language)
continue;
llvm::StringLiteral extension_name =
ScriptInterpreter::ExtensionToString(instance->extension);
if (!extension_name.starts_with(name))
continue;
// A single extension can back multiple languages, so dedup entries
// we've already surfaced.
if (emitted.insert(extension_name).second)
request.AddCompletion(extension_name);
}
}
@@ -15,6 +15,7 @@
#include "lldb/Utility/Status.h"
#include "lldb/Utility/Stream.h"
#include "lldb/Utility/StringList.h"
#include "lldb/Utility/UnimplementedError.h"
#include "lldb/ValueObject/ValueObject.h"
#include "llvm/ADT/StringSwitch.h"
#if defined(_WIN32)
@@ -50,6 +51,12 @@ StructuredData::DictionarySP ScriptInterpreter::GetInterpreterInfo() {
return nullptr;
}
llvm::Expected<FileSpec> ScriptInterpreter::GenerateExtensionTemplate(
const std::string &name, std::vector<ExtensionTemplateRequest> &extensions,
bool generate_non_abstract_methods, std::string output_file) {
return llvm::make_error<UnimplementedError>();
}
bool ScriptInterpreter::LoadScriptingModule(
const char *filename, const LoadScriptOptions &options,
lldb_private::Status &error, StructuredData::ObjectSP *module_sp,
@@ -77,6 +77,131 @@ def run_python_interpreter(local_dict):
if e.code:
print("Script exited with code %s" % e.code)
def generate_extension_schema(cls):
"""Introspect a scripting extension base class and return a JSON schema
describing its members. Used by `scripting extension generate` (via
`ScriptInterpreterPython::GetExtensionSchema`) to emit a skeleton
subclass with `# TODO: Implement` stubs for each method the base class
defines. Each method entry carries the signature, type hints,
docstring, and whether it's `@abstractmethod`, so the generator can
decide which methods to stub out (all of them with `-a`, otherwise
just the abstract ones). The schema also lists non-callable
attributes the base class exposes -- class-level values plus
class-body type annotations -- so the generator can advertise them in
the derived class' docstring. A `typing_imports` field enumerates
every `typing` generic (`Optional`, `Union`, ...) referenced by the
signatures or attribute types, so the generator can add the right
`from typing import` line without having to re-scan strings."""
import inspect, json, typing
used_typing = set()
def _record_typing(s):
# Anything from `typing.__all__` referenced as a generic
# (`Optional[...]`) gets picked up. Keying off the `[` avoids
# matching identifiers that merely embed the name.
if not s:
return
for name in typing.__all__:
if f"{name}[" in s:
used_typing.add(name)
def _fmt_type(t):
# `type(None)` stringifies as `NoneType`; render it as the
# literal `None` so the annotation stays valid Python.
if t is type(None):
return "None"
if isinstance(t, type):
if t.__module__ == "builtins":
return t.__name__
return f"{t.__module__}.{t.__name__}"
# `typing` generic aliases stringify with a leading `typing.`
# (`typing.Optional[list]`); the module prefix is noise for a
# docstring. `Union[int, str, None]` also renders its `None`
# component as `NoneType`, so fix that up too.
formatted = str(t).replace("typing.", "").replace("NoneType", "None")
_record_typing(formatted)
return formatted
def _build_signature(func):
# Reconstruct the signature from resolved type hints so forward
# refs (`"ScriptedFrame"`) come out as their real class -- what
# `inspect.signature(...)`'s own `str` would render as
# `ForwardRef('ScriptedFrame')`.
try:
hints = typing.get_type_hints(func)
except Exception:
hints = {}
sig = inspect.signature(func)
parts = []
for name, param in sig.parameters.items():
piece = name
if name in hints:
piece += f": {_fmt_type(hints[name])}"
if param.default is not inspect.Parameter.empty:
piece += f" = {param.default!r}"
parts.append(piece)
rendered = "(" + ", ".join(parts) + ")"
if "return" in hints:
rendered += f" -> {_fmt_type(hints['return'])}"
return rendered
def _get_function_metadata(func):
try:
hints = typing.get_type_hints(func)
type_hints = {k: str(v) for k, v in hints.items()}
except Exception:
type_hints = {}
return {
"signature": _build_signature(func),
"type_hints": type_hints,
"is_abstract": getattr(func, "__isabstractmethod__", False),
"doc": inspect.getdoc(func),
}
try:
class_hints = typing.get_type_hints(cls)
except Exception:
class_hints = {}
members = []
attributes = []
seen_attrs = set()
for name, member in inspect.getmembers(cls):
if inspect.isfunction(member):
members.append({"name": name, **_get_function_metadata(member)})
continue
if name.startswith("_"):
continue
entry = {"name": name}
if name in class_hints:
entry["type"] = _fmt_type(class_hints[name])
attributes.append(entry)
seen_attrs.add(name)
# Class-body type annotations without a runtime value
# (`target: SBTarget`) don't show up in `inspect.getmembers`, so pick
# them up from the hint map directly.
for name in class_hints:
if name.startswith("_") or name in seen_attrs:
continue
attributes.append({"name": name, "type": _fmt_type(class_hints[name])})
seen_attrs.add(name)
return json.dumps(
{
"class": cls.__name__,
"module": cls.__module__,
"doc": inspect.getdoc(cls),
"members": members,
"attributes": attributes,
"typing_imports": sorted(used_typing),
},
separators=(",", ":"),
)
def run_one_line(local_dict, input_string):
global g_run_one_line_str
try:
@@ -251,10 +251,15 @@ public:
size_t num_args = sizeof...(Args);
if (arg_info->max_positional_args != PythonCallable::ArgInfo::UNBOUNDED &&
num_args != arg_info->max_positional_args) {
if (num_args != arg_info->max_positional_args - 1)
if (num_args != arg_info->max_positional_args - 1) {
// `expected_return_object` starts in an error state; consume it
// before we return with a different error, or its destructor
// will abort.
llvm::consumeError(expected_return_object.takeError());
return create_error("Passed arguments ({0}) doesn't match the number "
"of expected arguments ({1}).",
num_args, arg_info->max_positional_args);
}
std::apply(
[&init, &expected_return_object](auto &&...args) {
@@ -36,11 +36,13 @@
#include "lldb/Target/ThreadPlan.h"
#include "lldb/Utility/Instrumentation.h"
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/StructuredData.h"
#include "lldb/Utility/Timer.h"
#include "lldb/ValueObject/ValueObject.h"
#include "lldb/lldb-enumerations.h"
#include "lldb/lldb-forward.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/ErrorExtras.h"
@@ -265,6 +267,363 @@ StructuredData::DictionarySP ScriptInterpreterPython::GetInterpreterInfo() {
return info_json.CreateStructuredDictionary();
}
llvm::Expected<std::string> ScriptInterpreterPython::ExtensionToImportPath(
lldb::ScriptedExtension extension) {
switch (extension) {
case eScriptedExtensionOperatingSystem:
return "lldb.plugins.operating_system";
case eScriptedExtensionScriptedPlatform:
return "lldb.plugins.scripted_platform";
case eScriptedExtensionScriptedProcess:
return "lldb.plugins.scripted_process";
case eScriptedExtensionScriptedHook:
return "lldb.plugins.scripted_hook";
case eScriptedExtensionScriptedBreakpointResolver:
return "lldb.plugins.scripted_breakpoint";
case eScriptedExtensionScriptedThreadPlan:
return "lldb.plugins.scripted_thread_plan";
case eScriptedExtensionScriptedFrameProvider:
return "lldb.plugins.scripted_frame_provider";
case eScriptedExtensionScriptedThread:
case eScriptedExtensionScriptedFrame:
return "lldb.plugins.scripted_process";
case eScriptedExtensionScriptedStackFrameRecognizer:
return "lldb.plugins.scripted_stackframe_recognizer";
case eScriptedExtensionInvalid:
return llvm::createStringError("invalid extension name");
}
return llvm::createStringError("invalid extension name");
}
llvm::Expected<StructuredData::ObjectSP>
ScriptInterpreterPython::GetExtensionSchema(
const llvm::SmallVector<llvm::StringRef> &extension_path) {
lldb::ScriptedExtension extension =
ScriptInterpreter::StringToExtension(extension_path.back());
auto import_path_or_err = ExtensionToImportPath(extension);
if (!import_path_or_err)
return import_path_or_err.takeError();
StreamString command_stream;
// __import__(path, fromlist=['']) imports the submodule and returns it
// directly (rather than the top-level package), as a single expression --
// this keeps the whole call eval-able in one line while guaranteeing the
// module is imported first; referencing "<import_path>.<ClassName>"
// directly would only work if something else had already imported
// <import_path> as a side effect.
command_stream.Printf("lldb.embedded_interpreter.generate_extension_schema("
"__import__('%s', fromlist=['']).%s)",
import_path_or_err->c_str(),
ScriptInterpreter::ExtensionToString(extension).data());
// Use eScriptReturnTypeOpaqueObject: it transfers a real owned reference
// we can safely extract the string from. eScriptReturnTypeCharStrOrNone
// instead hands back a pointer to a temporary Python object's buffer
// that gets destroyed (and, for a freshly created string like this one,
// deallocated) as soon as ExecuteOneLineWithReturn returns -- reading
// it afterwards is a use-after-free.
void *result_obj = nullptr;
if (!ExecuteOneLineWithReturn(
command_stream.GetData(),
ScriptInterpreter::eScriptReturnTypeOpaqueObject, &result_obj,
ExecuteScriptOptions().SetEnableIO(false)))
return llvm::createStringError("invalid extension schema format");
// ExecuteOneLineWithReturn releases the GIL before returning, so touching
// the returned object (Str() below can execute arbitrary Python code) must
// re-acquire it first. py_result is scoped so its destructor (a DECREF)
// also runs before the GIL is released below, not after.
std::string schema_str;
{
PyGILState_STATE gil_state = PyGILState_Ensure();
{
PythonObject py_result(PyRefType::Owned,
static_cast<PyObject *>(result_obj));
if (py_result.IsAllocated() && py_result.get() != Py_None)
schema_str = py_result.Str().GetString().str();
}
PyGILState_Release(gil_state);
}
if (schema_str.empty())
return llvm::createStringError("empty extension schema");
return StructuredData::ParseJSON(schema_str);
}
llvm::Error ScriptInterpreterPython::ParseExtensionSchema(
Stream &s, llvm::StringRef output_script_prefix,
const llvm::SmallVector<llvm::StringRef> &extension_path,
bool generate_non_abstract_methods, std::set<std::string> &typing_imports) {
auto schema_or_err = GetExtensionSchema(extension_path);
if (!schema_or_err)
return schema_or_err.takeError();
StructuredData::ObjectSP schema = *schema_or_err;
if (!schema)
return llvm::createStringError("empty extension schema");
StructuredData::Dictionary *dict = schema->GetAsDictionary();
if (!dict)
return llvm::createStringError("extension schema is not a JSON object");
// Merge each class' typing imports into the caller-owned set so the
// final `from typing import ...` line covers every class we emit.
StructuredData::Array *schema_typing;
if (dict->GetValueForKeyAsArray("typing_imports", schema_typing))
schema_typing->ForEach([&](StructuredData::Object *entry) {
if (auto *str = entry->GetAsString())
typing_imports.insert(str->GetValue().str());
return true;
});
llvm::StringRef base_class, import_path;
if (!dict->GetValueForKeyAsString("class", base_class))
return llvm::createStringError(
llvm::formatv("extension schema dictionary is missing 'class' key")
.str());
if (!dict->GetValueForKeyAsString("module", import_path))
return llvm::createStringError(
llvm::formatv("extension schema dictionary is missing 'module' key")
.str());
// imports
s.Printf("from %s import %s\n", import_path.data(), base_class.data());
s.EOL();
// class definition
s.Printf("class %s%s(%s):\n", output_script_prefix.data(), base_class.data(),
base_class.data());
s.IndentMore();
// Class docstring: list the non-callable members the base class exposes
// so the user sees what's available without having to hop back to the
// base class definition.
bool has_body = false;
StructuredData::Array *attributes;
if (dict->GetValueForKeyAsArray("attributes", attributes) &&
attributes->GetSize()) {
s.Indent();
s.PutCString("\"\"\"\n");
s.Indent();
s.Printf("Attributes inherited from %s:\n", base_class.data());
for (size_t i = 0; i < attributes->GetSize(); i++) {
auto maybe_dict = attributes->GetItemAtIndexAsDictionary(i);
if (!maybe_dict)
continue;
StructuredData::Dictionary *attr_dict = *maybe_dict;
llvm::StringRef attr_name;
if (!attr_dict->GetValueForKeyAsString("name", attr_name))
continue;
llvm::StringRef attr_type;
bool has_type = attr_dict->GetValueForKeyAsString("type", attr_type);
s.Indent();
s.Printf("- %s", attr_name.data());
if (has_type)
s.Printf(": %s", attr_type.data());
s.EOL();
}
s.Indent();
s.PutCString("\"\"\"\n\n");
has_body = true;
}
// members
StructuredData::Array *members;
if (!dict->GetValueForKeyAsArray("members", members))
return llvm::createStringError("missing 'members' key in extension schema");
// If the base class doesn't mark anything `@abstractmethod`, the filter
// "only stub abstract methods" would leave the derived class empty --
// which isn't a useful starting point. Fall back to emitting every
// method in that case so the user has actual code to edit.
bool any_abstract = false;
for (size_t i = 0; i < members->GetSize(); i++) {
auto maybe_dict = members->GetItemAtIndexAsDictionary(i);
if (!maybe_dict)
continue;
bool is_abstract = false;
if ((*maybe_dict)->GetValueForKeyAsBoolean("is_abstract", is_abstract) &&
is_abstract) {
any_abstract = true;
break;
}
}
bool emit_all_methods = generate_non_abstract_methods || !any_abstract;
for (size_t i = 0; i < members->GetSize(); i++) {
auto maybe_dict = members->GetItemAtIndexAsDictionary(i);
if (!maybe_dict)
return llvm::createStringError(
llvm::formatv(
"member at index {0} in extension schema isn't a dictionary")
.str());
StructuredData::Dictionary *member_dict = *maybe_dict;
llvm::StringRef symbol, args;
if (!member_dict->GetValueForKeyAsString("name", symbol))
return llvm::createStringError(
llvm::formatv(
"member at index {0} in extension schema is missing 'name' key")
.str());
if (!member_dict->GetValueForKeyAsString("signature", args))
return llvm::createStringError(
llvm::formatv("member at index {0} in extension schema is missing "
"'signature' key")
.str());
bool is_abstract = false;
bool has_is_abstract =
member_dict->GetValueForKeyAsBoolean("is_abstract", is_abstract);
if (!emit_all_methods)
if (!has_is_abstract || !is_abstract)
continue;
s.Indent();
s.Printf("def %s%s:\n", symbol.data(), args.data());
s.IndentMore();
llvm::StringRef documentation;
if (member_dict->GetValueForKeyAsString("doc", documentation)) {
s.Indent();
s.PutCString("\"\"\"\n");
llvm::SmallVector<llvm::StringRef> lines;
documentation.split(lines, "\n");
for (llvm::StringRef line : lines) {
s.Indent();
s.PutCString(line);
s.EOL();
}
s.Indent();
s.PutCString("\"\"\"");
s.EOL();
}
if (symbol == "__init__") {
// The base class' constructor sets up attributes (e.g. self.target,
// self.process) that the inherited, non-overridden methods rely on.
// Forward the same arguments so that state is still initialized.
// Splitting the param list on `,` requires bracket-depth awareness
// because annotations like `Union[X, Y]` also contain commas.
llvm::StringRef params = args.trim("()");
std::vector<std::string> forwarded_args;
int depth = 0;
size_t start = 0;
auto flush = [&](size_t end) {
llvm::StringRef param = params.slice(start, end);
param = param.split(':').first.split('=').first.trim();
if (!param.empty() && param != "self")
forwarded_args.push_back(param.str());
};
for (size_t i = 0; i < params.size(); ++i) {
char c = params[i];
if (c == '[' || c == '(' || c == '{')
++depth;
else if (c == ']' || c == ')' || c == '}')
--depth;
else if (c == ',' && depth == 0) {
flush(i);
start = i + 1;
}
}
flush(params.size());
s.Indent();
s.Printf("super().__init__(%s)\n",
llvm::join(forwarded_args, ", ").c_str());
}
s.Indent();
s.PutCString("# TODO: Implement\n");
s.Indent();
s.PutCString("pass\n\n");
s.IndentLess();
has_body = true;
}
// A class with no body is a Python syntax error, so emit `pass` when the
// base class has nothing to stub out (no methods and no attributes to
// document).
if (!has_body) {
s.Indent();
s.PutCString("pass\n");
}
return llvm::Error::success();
}
llvm::Expected<FileSpec> ScriptInterpreterPython::GenerateExtensionTemplate(
const std::string &name, std::vector<ExtensionTemplateRequest> &extensions,
bool generate_non_abstract_methods, std::string output_file) {
// `ParseExtensionSchema` accumulates every `typing` generic it sees
// (`Optional`, `Union`, `List`, ...) into this set so we can emit a
// targeted `from typing import ...` line only for what's actually
// referenced. The Python schema does the detection so we don't have
// to re-scan strings here.
std::set<std::string> typing_imports;
StreamString bodies;
for (const ExtensionTemplateRequest &extension : extensions) {
if (llvm::Error err =
ParseExtensionSchema(bodies, name, extension.path,
generate_non_abstract_methods, typing_imports))
return std::move(err);
bodies.PutCString("\n\n");
}
StreamString generated_file_stream;
generated_file_stream.PutCString("import lldb\n");
if (!typing_imports.empty()) {
std::vector<std::string> sorted_imports(typing_imports.begin(),
typing_imports.end());
generated_file_stream.Format("from typing import {0}\n",
llvm::join(sorted_imports, ", "));
}
generated_file_stream.PutCString("\n");
generated_file_stream.PutCString(bodies.GetString());
FileSpec save_location;
if (output_file.empty()) {
// Sanitize the caller-supplied class prefix so it can't escape the
// temp directory (`../`, path separators, ...). Only keep ASCII
// alphanumerics; everything else collapses to `_`, and an all-junk
// name falls back to a fixed default.
std::string sanitized;
sanitized.reserve(name.size());
for (char c : name)
sanitized.push_back(llvm::isAlnum(c) ? static_cast<char>(llvm::toLower(c))
: '_');
if (sanitized.find_first_not_of('_') == std::string::npos)
sanitized = "extension";
const std::string file_name = "lldb_" + sanitized + "_extension.py";
save_location = HostInfo::GetGlobalTempDir();
FileSystem::Instance().Resolve(save_location);
save_location.AppendPathComponent(file_name);
} else {
save_location = FileSpec(output_file);
FileSystem::Instance().Resolve(save_location);
}
File::OpenOptions flags = File::eOpenOptionWriteOnly |
File::eOpenOptionCanCreate |
File::eOpenOptionTruncate;
auto opened_file = FileSystem::Instance().Open(save_location, flags);
if (!opened_file)
return opened_file.takeError();
FileUP file = std::move(opened_file.get());
size_t byte_size = generated_file_stream.GetSize();
Status error = file->Write(generated_file_stream.GetData(), byte_size);
if (error.Fail() || byte_size != generated_file_stream.GetSize())
return llvm::createStringError("Unable to write to destination file. Bytes "
"written do not match generated file size.");
return save_location;
}
void ScriptInterpreterPython::SharedLibraryDirectoryHelper(
FileSpec &this_file) {
// When we're loaded from python, this_file will point to the file inside the
@@ -16,6 +16,7 @@
#include "lldb/lldb-private.h"
#include <memory>
#include <set>
#include <string>
#include <vector>
@@ -41,7 +42,15 @@ public:
: ScriptInterpreter(debugger, lldb::eScriptLanguagePython),
IOHandlerDelegateMultiline("DONE") {}
llvm::Expected<std::string>
ExtensionToImportPath(lldb::ScriptedExtension extension) override;
StructuredData::DictionarySP GetInterpreterInfo() override;
llvm::Expected<FileSpec>
GenerateExtensionTemplate(const std::string &name,
std::vector<ExtensionTemplateRequest> &extensions,
bool generate_non_abstract_methods,
std::string output_file) override;
static void Initialize();
static void Terminate();
static llvm::StringRef GetPluginNameStatic() { return "script-python"; }
@@ -50,6 +59,14 @@ public:
static void SharedLibraryDirectoryHelper(FileSpec &this_file);
protected:
llvm::Error
ParseExtensionSchema(Stream &s, llvm::StringRef output_script_prefix,
const llvm::SmallVector<llvm::StringRef> &extension_path,
bool generate_non_abstract_methods,
std::set<std::string> &typing_imports);
llvm::Expected<StructuredData::ObjectSP>
GetExtensionSchema(const llvm::SmallVector<llvm::StringRef> &extension_path);
static void ComputePythonDirForApple(llvm::SmallVectorImpl<char> &path);
static void ComputePythonDir(llvm::SmallVectorImpl<char> &path);
};
@@ -0,0 +1,3 @@
C_SOURCES := main.c
include Makefile.rules
@@ -0,0 +1,167 @@
"""
Meta-test: generate every kind of scripting extension template on the fly via
`scripting extension generate`, then actually register and drive each one
through a live lldb debug session, to catch regressions in the generator
itself (e.g. broken imports, wrong base-class name, missing __init__
chaining) rather than just checking that a Python file was produced.
The stub methods lldb calls into may legitimately report failure (e.g. an
`is_alive` stub that returns None can make `process launch` report an
error), so success/failure of the driving command is not asserted. What is
asserted is that lldb never surfaces an unhandled Python traceback while
running the generated code.
"""
import os
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
class TestScriptingExtensionGenerate(TestBase):
NO_DEBUG_INFO_TESTCASE = True
def generate_extension(self, kind, generate_all=False):
"""Run `scripting extension generate` for `kind`, returning the path
to the generated file. Fails the test if generation errors out."""
output_path = self.getBuildArtifact(f"generated_{kind}.py")
if os.path.exists(output_path):
os.remove(output_path)
all_flag = " -a" if generate_all else ""
self.runCmd(
f"scripting extension generate{all_flag} -n Test -o {output_path} {kind}"
)
self.assertTrue(
os.path.exists(output_path),
f"scripting extension generate did not create {output_path}",
)
return output_path
def run_and_assert_no_traceback(self, cmd):
"""Run `cmd` and fail the test if lldb surfaced an unhandled Python
traceback while executing it. The command itself may still report
failure (e.g. a stub method returning None) without that being a
generator bug."""
self.runCmd(cmd, check=False)
output = self.res.GetOutput() + self.res.GetError()
self.assertNotIn(
"Traceback (most recent call last)",
output,
f"command {cmd!r} raised a Python exception:\n{output}",
)
def test_generate_operating_system(self):
"""OperatingSystem: generate, import, and activate via the
target.process.python-os-plugin-path setting."""
self.build()
path = self.generate_extension("OperatingSystem")
self.run_and_assert_no_traceback(f"command script import {path}")
lldbutil.run_to_source_breakpoint(self, "break here", lldb.SBFileSpec("main.c"))
self.run_and_assert_no_traceback(
f"settings set target.process.python-os-plugin-path {path}"
)
self.run_and_assert_no_traceback("thread list")
self.runCmd("settings clear target.process.python-os-plugin-path", check=False)
def test_generate_scripted_platform(self):
"""ScriptedPlatform: generate, import, and select via `platform
select`."""
path = self.generate_extension("ScriptedPlatform")
self.run_and_assert_no_traceback(f"command script import {path}")
self.run_and_assert_no_traceback(
"platform select scripted-platform -C "
"generated_ScriptedPlatform.TestScriptedPlatform"
)
def test_generate_scripted_process(self):
"""ScriptedProcess: generate, import, and launch via `process launch
-C`."""
self.build()
path = self.generate_extension("ScriptedProcess")
self.run_and_assert_no_traceback(f"command script import {path}")
target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
self.assertTrue(target, VALID_TARGET)
self.run_and_assert_no_traceback(
"process launch -C generated_ScriptedProcess.TestScriptedProcess"
)
def test_generate_scripted_thread_plan(self):
"""ScriptedThreadPlan: generate, import, and drive via `thread
step-scripted -C`."""
self.build()
path = self.generate_extension("ScriptedThreadPlan")
self.run_and_assert_no_traceback(f"command script import {path}")
lldbutil.run_to_source_breakpoint(self, "break here", lldb.SBFileSpec("main.c"))
self.run_and_assert_no_traceback(
"thread step-scripted -C "
"generated_ScriptedThreadPlan.TestScriptedThreadPlan"
)
def test_generate_scripted_breakpoint_resolver(self):
"""ScriptedBreakpointResolver: generate, import, and set via
`breakpoint set -P`."""
self.build()
path = self.generate_extension("ScriptedBreakpointResolver")
self.run_and_assert_no_traceback(f"command script import {path}")
target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
self.assertTrue(target, VALID_TARGET)
self.run_and_assert_no_traceback(
"breakpoint set -P "
"generated_ScriptedBreakpointResolver.TestScriptedBreakpointResolver"
)
def test_generate_scripted_hook_for_stop_hook(self):
"""ScriptedHook drives `target stop-hook add -P` (the stop-hook is a
subset of the general scripted hook interface)."""
self.build()
path = self.generate_extension("ScriptedHook")
self.run_and_assert_no_traceback(f"command script import {path}")
target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
self.assertTrue(target, VALID_TARGET)
self.run_and_assert_no_traceback(
"target stop-hook add -P generated_ScriptedHook.TestScriptedHook"
)
def test_generate_scripted_frame_provider(self):
"""ScriptedFrameProvider: generate, import, and register via `target
frame-provider register -C`. Uses -a to generate all methods since
`get_description` is a required static abstract method."""
self.build()
path = self.generate_extension("ScriptedFrameProvider", generate_all=True)
self.run_and_assert_no_traceback(f"command script import {path}")
lldbutil.run_to_source_breakpoint(self, "break here", lldb.SBFileSpec("main.c"))
self.run_and_assert_no_traceback(
"target frame-provider register -C "
"generated_ScriptedFrameProvider.TestScriptedFrameProvider"
)
def test_generate_scripted_hook(self):
"""ScriptedHook: generate, import, and register via `target hook add
-P`."""
self.build()
path = self.generate_extension("ScriptedHook")
self.run_and_assert_no_traceback(f"command script import {path}")
target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
self.assertTrue(target, VALID_TARGET)
self.run_and_assert_no_traceback(
"target hook add -P generated_ScriptedHook.TestScriptedHook"
)
@@ -0,0 +1,12 @@
#include <stdio.h>
int foo(int x) {
int y = x + 1; // break here
return y;
}
int main() {
int result = foo(41);
printf("%d\n", result);
return 0;
}