[Lldb-commits] [lldb] [lldb] Add `scripting extension instances` command (PR #226043)
Med Ismail Bennani via lldb-commits
lldb-commits at lists.llvm.org
Thu Sep 24 00:13:40 PDT 2026
https://github.com/medismailben created https://github.com/llvm/llvm-project/pull/226043
There is currently no way to tell which scripted extensions are alive in a debug session, or which script file a given extension class came from.
This patch addresses that by recording the file of every module imported by the script interpreter, and attributing it to the ScriptedMetadata of each extension object created from one of its classes. It tracks every successfully created extension object in a registry owned by the script interpreter, and lists them with the new `scripting extension instances` command, grouped by class. Each instance shows a UUID, the address of its script object and the arguments it was created with:
```
(lldb) scripting extension instances
Instantiated scripted extensions:
--------------------------------------------------------------------------------
Class: resolver.Resolver
Extension: ScriptedBreakpointResolver
Path: /tmp/resolver.py
Instance: 430F093E-89C6-EEF1-9602-E951A3A523DF (0x90000759e54f1a0)
Args:
- symbol: break_on_me
--------------------------------------------------------------------------------
Class: stop_hook.stop_handler
Extension: ScriptedHook
Path: /tmp/stop_hook.py
Instances:
[0] 7576EB2F-4E4D-1931-D006-F632A51696BD (0x50000784d2584a0)
Args:
- increment: 5
[1] C95B90A2-E199-343F-B7F7-AC2E360BFF2F (0x50000759e54dfa0)
Args:
- increment: 1
- return_false: 1
```
>From 307afc7be788a470bf2cfb07d62f5aa370a2d40c Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Thu, 24 Sep 2026 00:10:17 -0700
Subject: [PATCH] [lldb] Add `scripting extension instances` command
There is currently no way to tell which scripted extensions are alive
in a debug session, or which script file a given extension class came
from.
This patch addresses that by recording the file of every module imported
by the script interpreter, and attributing it to the ScriptedMetadata of
each extension object created from one of its classes. It tracks every
successfully created extension object in a registry owned by the script
interpreter, and lists them with the new `scripting extension instances`
command, grouped by class. Each instance shows a UUID, the address of its
script object and the arguments it was created with:
```
(lldb) scripting extension instances
Instantiated scripted extensions:
--------------------------------------------------------------------------------
Class: resolver.Resolver
Extension: ScriptedBreakpointResolver
Path: /tmp/resolver.py
Instance: 430F093E-89C6-EEF1-9602-E951A3A523DF (0x90000759e54f1a0)
Args:
- symbol: break_on_me
--------------------------------------------------------------------------------
Class: stop_hook.stop_handler
Extension: ScriptedHook
Path: /tmp/stop_hook.py
Instances:
[0] 7576EB2F-4E4D-1931-D006-F632A51696BD (0x50000784d2584a0)
Args:
- increment: 5
[1] C95B90A2-E199-343F-B7F7-AC2E360BFF2F (0x50000759e54dfa0)
Args:
- increment: 1
- return_false: 1
```
Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
---
.../Interfaces/ScriptedInterface.h | 40 ++++-
.../lldb/Interpreter/ScriptInterpreter.h | 19 +++
.../Interpreter/ScriptedInstanceRegistry.h | 97 +++++++++++
lldb/include/lldb/Utility/ScriptedMetadata.h | 8 +
lldb/include/lldb/lldb-forward.h | 5 +
.../Commands/CommandObjectScripting.cpp | 115 +++++++++++++
lldb/source/Commands/Options.td | 6 +
lldb/source/Interpreter/CMakeLists.txt | 1 +
lldb/source/Interpreter/ScriptInterpreter.cpp | 20 +++
.../Interpreter/ScriptedInstanceRegistry.cpp | 153 +++++++++++++++++
.../Interfaces/ScriptedPythonInterface.h | 17 ++
.../Python/ScriptInterpreterPython.cpp | 15 ++
.../TestScriptingExtensionInstances.py | 154 ++++++++++++++++++
.../scripting/extension/instance_cmds.py | 17 ++
.../Interpreter/TestScriptedInterface.cpp | 45 +++++
15 files changed, 711 insertions(+), 1 deletion(-)
create mode 100644 lldb/include/lldb/Interpreter/ScriptedInstanceRegistry.h
create mode 100644 lldb/source/Interpreter/ScriptedInstanceRegistry.cpp
create mode 100644 lldb/test/API/commands/scripting/extension/TestScriptingExtensionInstances.py
create mode 100644 lldb/test/API/commands/scripting/extension/instance_cmds.py
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
index 79c2f08a1820b..d01699fe27022 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
@@ -10,6 +10,7 @@
#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDINTERFACE_H
#include "ScriptedInterfaceUsages.h"
+#include "lldb/Interpreter/ScriptedInstanceRegistry.h"
#include "lldb/Core/StructuredDataImpl.h"
#include "lldb/Utility/LLDBLog.h"
@@ -27,7 +28,12 @@ namespace lldb_private {
class ScriptedInterface {
public:
ScriptedInterface() = default;
- virtual ~ScriptedInterface() = default;
+ virtual ~ScriptedInterface() { UnregisterInstance(); }
+
+ // Copies would share the registry entry, and the first one destroyed would
+ // drop it while the other is still alive.
+ ScriptedInterface(const ScriptedInterface &) = delete;
+ ScriptedInterface &operator=(const ScriptedInterface &) = delete;
StructuredData::GenericSP GetScriptObjectInstance() {
return m_object_instance_sp;
@@ -37,6 +43,8 @@ class ScriptedInterface {
return m_scripted_metadata;
}
+ virtual llvm::StringRef GetPluginName() { return {}; }
+
/// Whether the user can invoke this extension directly, the way a scripted
/// command can. Those never introduce the target's API mutex bypass, so at
/// top level they serialize like any other command; nested inside an
@@ -101,8 +109,38 @@ class ScriptedInterface {
}
protected:
+ void RegisterInstance(const lldb::ScriptedInstanceRegistrySP ®istry_sp,
+ llvm::StringRef class_name) {
+ UnregisterInstance();
+ if (!registry_sp)
+ return;
+ ScriptedInstanceInfo info;
+ info.plugin_name = GetPluginName();
+ info.class_name = class_name.str();
+ if (m_object_instance_sp)
+ info.object_address =
+ reinterpret_cast<uintptr_t>(m_object_instance_sp->GetValue());
+ if (m_scripted_metadata) {
+ info.source_path = m_scripted_metadata->GetSourcePath();
+ info.args_sp = m_scripted_metadata->GetArgsSP();
+ }
+ m_registry_id = registry_sp->Add(std::move(info));
+ m_registry_wp = registry_sp;
+ }
+
StructuredData::GenericSP m_object_instance_sp;
std::optional<ScriptedMetadata> m_scripted_metadata;
+
+private:
+ void UnregisterInstance() {
+ // The interpreter owning the registry may already be gone.
+ if (auto registry_sp = m_registry_wp.lock())
+ registry_sp->Remove(m_registry_id);
+ m_registry_wp.reset();
+ }
+
+ lldb::ScriptedInstanceRegistryWP m_registry_wp;
+ uint64_t m_registry_id = 0;
};
} // namespace lldb_private
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 7ad530b3233f2..eb25337e58194 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -22,11 +22,15 @@
#include "lldb/Interpreter/Interfaces/ScriptedProcessInterface.h"
#include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
#include "lldb/Interpreter/ScriptObject.h"
+#include "lldb/Interpreter/ScriptedInstanceRegistry.h"
#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 "llvm/ADT/StringMap.h"
+#include <memory>
+#include <mutex>
#include <optional>
namespace lldb_private {
@@ -379,6 +383,15 @@ class ScriptInterpreter : public PluginInterface {
FileSpec extra_search_dir = {},
lldb::TargetSP loaded_into_target_sp = {});
+ void SetImportedModulePath(llvm::StringRef module_name, const FileSpec &path);
+
+ /// Falls back to the enclosing package for submodules of an imported one.
+ FileSpec GetImportedModulePath(llvm::StringRef module_name) const;
+
+ const lldb::ScriptedInstanceRegistrySP &GetScriptedInstanceRegistry() const {
+ return m_instance_registry_sp;
+ }
+
virtual bool IsReservedWord(const char *word) { return false; }
virtual std::unique_ptr<ScriptInterpreterLocker> AcquireInterpreterLock();
@@ -522,6 +535,12 @@ class ScriptInterpreter : public PluginInterface {
protected:
Debugger &m_debugger;
lldb::ScriptLanguage m_script_lang;
+
+private:
+ mutable std::mutex m_imported_modules_mutex;
+ llvm::StringMap<FileSpec> m_imported_modules;
+ lldb::ScriptedInstanceRegistrySP m_instance_registry_sp =
+ std::make_shared<ScriptedInstanceRegistry>();
};
} // namespace lldb_private
diff --git a/lldb/include/lldb/Interpreter/ScriptedInstanceRegistry.h b/lldb/include/lldb/Interpreter/ScriptedInstanceRegistry.h
new file mode 100644
index 0000000000000..f0c0fd1c9554d
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/ScriptedInstanceRegistry.h
@@ -0,0 +1,97 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_INTERPRETER_SCRIPTEDINSTANCEREGISTRY_H
+#define LLDB_INTERPRETER_SCRIPTEDINSTANCEREGISTRY_H
+
+#include "lldb/Utility/FileSpec.h"
+#include "lldb/Utility/StructuredData.h"
+#include "lldb/Utility/UUID.h"
+#include "lldb/lldb-defines.h"
+#include "lldb/lldb-enumerations.h"
+#include "lldb/lldb-forward.h"
+#include "lldb/lldb-types.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/StringRef.h"
+
+#include <cstdint>
+#include <mutex>
+#include <string>
+#include <vector>
+
+namespace lldb_private {
+
+struct ScriptedInstanceInfo {
+ uint64_t id = 0;
+ UUID uuid;
+ lldb::addr_t object_address = LLDB_INVALID_ADDRESS;
+ /// Plugin names are string literals, so this never dangles.
+ llvm::StringRef plugin_name;
+ std::string class_name;
+ FileSpec source_path;
+ StructuredData::DictionarySP args_sp;
+};
+
+struct ScriptedInstanceGroup {
+ std::string class_name;
+ lldb::ScriptedExtension extension = lldb::eScriptedExtensionInvalid;
+ FileSpec source_path;
+ std::vector<ScriptedInstanceInfo> instances;
+
+ StructuredData::DictionarySP ToStructuredData() const;
+
+ void Dump(Stream &s, bool use_color) const;
+};
+
+/// Entries are snapshots taken when the object is created, so listing them
+/// never calls back into an interface that might be mid-destruction on
+/// another thread.
+class ScriptedInstanceRegistry {
+public:
+ uint64_t Add(ScriptedInstanceInfo info) {
+ std::lock_guard<std::mutex> guard(m_mutex);
+ const uint64_t id = m_next_id++;
+ info.id = id;
+ info.uuid = UUID::Generate();
+ m_instances[id] = std::move(info);
+ return id;
+ }
+
+ void Remove(uint64_t id) {
+ std::lock_guard<std::mutex> guard(m_mutex);
+ m_instances.erase(id);
+ }
+
+ std::vector<ScriptedInstanceInfo> GetInstances() const {
+ std::vector<ScriptedInstanceInfo> instances;
+ {
+ std::lock_guard<std::mutex> guard(m_mutex);
+ instances.reserve(m_instances.size());
+ for (const auto &entry : m_instances)
+ instances.push_back(entry.second);
+ }
+ llvm::sort(instances,
+ [](const ScriptedInstanceInfo &lhs,
+ const ScriptedInstanceInfo &rhs) { return lhs.id < rhs.id; });
+ return instances;
+ }
+
+ std::vector<ScriptedInstanceGroup> GetInstanceGroups(
+ llvm::ArrayRef<lldb::ScriptedExtension> extensions = {}) const;
+
+private:
+ mutable std::mutex m_mutex;
+ uint64_t m_next_id = 1;
+ llvm::DenseMap<uint64_t, ScriptedInstanceInfo> m_instances;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_SCRIPTEDINSTANCEREGISTRY_H
diff --git a/lldb/include/lldb/Utility/ScriptedMetadata.h b/lldb/include/lldb/Utility/ScriptedMetadata.h
index fcbcc70bc36e3..38dad813070c5 100644
--- a/lldb/include/lldb/Utility/ScriptedMetadata.h
+++ b/lldb/include/lldb/Utility/ScriptedMetadata.h
@@ -9,6 +9,7 @@
#ifndef LLDB_UTILITY_SCRIPTEDMETADATA_H
#define LLDB_UTILITY_SCRIPTEDMETADATA_H
+#include "lldb/Utility/FileSpec.h"
#include "lldb/Utility/ProcessInfo.h"
#include "lldb/Utility/StreamString.h"
#include "lldb/Utility/StructuredData.h"
@@ -26,6 +27,7 @@ class ScriptedMetadata {
if (metadata_sp) {
m_class_name = metadata_sp->GetClassName();
m_args_sp = metadata_sp->GetArgsSP();
+ m_source_path = metadata_sp->GetSourcePath();
}
}
@@ -37,6 +39,11 @@ class ScriptedMetadata {
llvm::StringRef GetClassName() const { return m_class_name; }
StructuredData::DictionarySP GetArgsSP() const { return m_args_sp; }
+ const FileSpec &GetSourcePath() const { return m_source_path; }
+ void SetSourcePath(const FileSpec &source_path) {
+ m_source_path = source_path;
+ }
+
/// Get a unique identifier for this metadata based on its contents.
/// The ID is computed from the class name and arguments dictionary,
/// not from the pointer address, so two metadata objects with the same
@@ -62,6 +69,7 @@ class ScriptedMetadata {
private:
std::string m_class_name;
StructuredData::DictionarySP m_args_sp;
+ FileSpec m_source_path;
};
} // namespace lldb_private
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index f48f077e9b5ee..649f7b704d68e 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -197,6 +197,7 @@ class ScriptedMetadata;
class ScriptedBreakpointInterface;
class ScriptedCommandInterface;
class ScriptedHookInterface;
+class ScriptedInstanceRegistry;
class ScriptedPlatformInterface;
class ScriptedProcessInterface;
class ScriptedThreadInterface;
@@ -436,6 +437,10 @@ typedef std::unique_ptr<lldb_private::ScriptedProcessInterface>
ScriptedProcessInterfaceUP;
typedef std::shared_ptr<lldb_private::ScriptedHookInterface>
ScriptedHookInterfaceSP;
+typedef std::shared_ptr<lldb_private::ScriptedInstanceRegistry>
+ ScriptedInstanceRegistrySP;
+typedef std::weak_ptr<lldb_private::ScriptedInstanceRegistry>
+ ScriptedInstanceRegistryWP;
typedef std::shared_ptr<lldb_private::ScriptedThreadInterface>
ScriptedThreadInterfaceSP;
typedef std::shared_ptr<lldb_private::ScriptedThreadPlanInterface>
diff --git a/lldb/source/Commands/CommandObjectScripting.cpp b/lldb/source/Commands/CommandObjectScripting.cpp
index 02c19ed4ef0cd..28eb1a8b80240 100644
--- a/lldb/source/Commands/CommandObjectScripting.cpp
+++ b/lldb/source/Commands/CommandObjectScripting.cpp
@@ -374,6 +374,118 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed {
CommandOptions m_options;
};
+#define LLDB_OPTIONS_scripting_extension_instances
+#include "CommandOptions.inc"
+
+class CommandObjectScriptingExtensionInstances : public CommandObjectParsed {
+public:
+ CommandObjectScriptingExtensionInstances(CommandInterpreter &interpreter)
+ : CommandObjectParsed(
+ interpreter, "scripting extension instances",
+ "List the instantiated scripting extensions, grouped by class, "
+ "along with the script file each class was imported from.",
+ "scripting extension instances [--json] [<extension-name> ...]") {
+ AddSimpleArgumentList(eArgTypeScriptedExtension, eArgRepeatStar);
+ }
+
+ ~CommandObjectScriptingExtensionInstances() override = default;
+
+ Options *GetOptions() override { return &m_options; }
+
+ void
+ HandleArgumentCompletion(CompletionRequest &request,
+ OptionElementVector &opt_element_vector) override {
+ lldb_private::CommandCompletions::InvokeCommonCompletionCallbacks(
+ GetCommandInterpreter(), lldb::eScriptedExtensionCompletion, request,
+ nullptr);
+ }
+
+ class CommandOptions : public Options {
+ public:
+ CommandOptions() = default;
+ ~CommandOptions() override = default;
+ Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
+ ExecutionContext *execution_context) override {
+ const int short_option = m_getopt_table[option_idx].val;
+
+ switch (short_option) {
+ case 'j':
+ m_json_format = true;
+ break;
+ default:
+ llvm_unreachable("Unimplemented option");
+ }
+
+ return {};
+ }
+
+ void OptionParsingStarting(ExecutionContext *execution_context) override {
+ m_json_format = false;
+ }
+
+ llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
+ return llvm::ArrayRef(g_scripting_extension_instances_options);
+ }
+
+ bool m_json_format = false;
+ };
+
+protected:
+ void DoExecute(Args &command, CommandReturnObject &result) override {
+ std::vector<lldb::ScriptedExtension> extensions;
+ for (const Args::ArgEntry &arg : command.entries()) {
+ lldb::ScriptedExtension extension =
+ ScriptInterpreter::StringToExtension(arg.ref());
+ if (extension == eScriptedExtensionInvalid) {
+ result.AppendErrorWithFormat("no scripted extension named '%s'",
+ arg.c_str());
+ return;
+ }
+ extensions.push_back(extension);
+ }
+
+ std::vector<ScriptedInstanceGroup> groups;
+ if (ScriptInterpreter *script_interpreter =
+ GetDebugger().GetScriptInterpreter(/*can_create=*/false))
+ groups =
+ script_interpreter->GetScriptedInstanceRegistry()->GetInstanceGroups(
+ extensions);
+
+ if (m_options.m_json_format) {
+ StructuredData::Array groups_array;
+ for (const ScriptedInstanceGroup &group : groups)
+ groups_array.AddItem(group.ToStructuredData());
+ groups_array.Dump(result.GetOutputStream());
+ result.GetOutputStream().EOL();
+ result.SetStatus(eReturnStatusSuccessFinishResult);
+ return;
+ }
+
+ Stream &s = result.GetOutputStream();
+ const bool use_color = s.AsRawOstream().colors_enabled();
+ constexpr llvm::StringLiteral faint_code = ANSI_ESCAPE1(ANSI_CTRL_FAINT);
+ constexpr llvm::StringLiteral reset_code = ANSI_ESCAPE1(ANSI_CTRL_NORMAL);
+ const std::string separator(
+ std::min<uint64_t>(GetDebugger().GetTerminalWidth(), 80), '-');
+
+ s.PutCString("Instantiated scripted extensions:");
+ if (groups.empty())
+ s << " None";
+ s.EOL();
+ for (const ScriptedInstanceGroup &group : groups) {
+ if (use_color)
+ s << faint_code << separator << reset_code << '\n';
+ else
+ s << separator << '\n';
+ group.Dump(s, use_color);
+ }
+ result.SetStatus(eReturnStatusSuccessFinishResult);
+ }
+
+private:
+ CommandOptions m_options;
+};
+
#define LLDB_OPTIONS_scripting_extension_generate
#include "CommandOptions.inc"
@@ -576,6 +688,9 @@ class CommandObjectMultiwordScriptingExtension : public CommandObjectMultiword {
LoadSubCommand(
"list",
CommandObjectSP(new CommandObjectScriptingExtensionList(interpreter)));
+ LoadSubCommand("instances",
+ CommandObjectSP(new CommandObjectScriptingExtensionInstances(
+ interpreter)));
LoadSubCommand("generate",
CommandObjectSP(new CommandObjectScriptingExtensionGenerate(
interpreter)));
diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td
index ab851725979ef..9809d193ac258 100644
--- a/lldb/source/Commands/Options.td
+++ b/lldb/source/Commands/Options.td
@@ -1587,6 +1587,12 @@ let Command = "scripting extension list" in {
Desc<"Output the scripted extension list in json format.">;
}
+let Command = "scripting extension instances" in {
+ def scripting_extension_instances_json
+ : Option<"json", "j">,
+ Desc<"Output the instantiated scripted extensions in json format.">;
+}
+
let Command = "scripting extension generate" in {
def scripting_extension_generate_all_methods
: Option<"all", "a">,
diff --git a/lldb/source/Interpreter/CMakeLists.txt b/lldb/source/Interpreter/CMakeLists.txt
index 8a9605b568865..17290df85e2ec 100644
--- a/lldb/source/Interpreter/CMakeLists.txt
+++ b/lldb/source/Interpreter/CMakeLists.txt
@@ -58,6 +58,7 @@ add_lldb_library(lldbInterpreter NO_PLUGIN_DEPENDENCIES
Options.cpp
Property.cpp
ScriptInterpreter.cpp
+ ScriptedInstanceRegistry.cpp
ADDITIONAL_HEADER_DIRS
${LLDB_INCLUDE_DIR}/lldb/Interpreter
diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp
index a53f3b744f02e..0e402381b3a4e 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -65,6 +65,26 @@ bool ScriptInterpreter::LoadScriptingModule(
return false;
}
+void ScriptInterpreter::SetImportedModulePath(llvm::StringRef module_name,
+ const FileSpec &path) {
+ std::lock_guard<std::mutex> guard(m_imported_modules_mutex);
+ m_imported_modules[module_name] = path;
+}
+
+FileSpec
+ScriptInterpreter::GetImportedModulePath(llvm::StringRef module_name) const {
+ std::lock_guard<std::mutex> guard(m_imported_modules_mutex);
+ while (true) {
+ auto it = m_imported_modules.find(module_name);
+ if (it != m_imported_modules.end())
+ return it->second;
+ size_t dot_pos = module_name.rfind('.');
+ if (dot_pos == llvm::StringRef::npos)
+ return {};
+ module_name = module_name.take_front(dot_pos);
+ }
+}
+
std::string ScriptInterpreter::LanguageToString(lldb::ScriptLanguage language) {
switch (language) {
case eScriptLanguageNone:
diff --git a/lldb/source/Interpreter/ScriptedInstanceRegistry.cpp b/lldb/source/Interpreter/ScriptedInstanceRegistry.cpp
new file mode 100644
index 0000000000000..2ecfb1a0401be
--- /dev/null
+++ b/lldb/source/Interpreter/ScriptedInstanceRegistry.cpp
@@ -0,0 +1,153 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Interpreter/ScriptedInstanceRegistry.h"
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
+#include "lldb/Utility/AnsiTerminal.h"
+#include "lldb/Utility/Stream.h"
+
+#include "llvm/ADT/StringMap.h"
+
+#include <tuple>
+
+using namespace lldb;
+using namespace lldb_private;
+
+static bool HasArgs(const ScriptedInstanceInfo &info) {
+ return info.args_sp && info.args_sp->GetSize();
+}
+
+std::vector<ScriptedInstanceGroup> ScriptedInstanceRegistry::GetInstanceGroups(
+ llvm::ArrayRef<ScriptedExtension> extensions) const {
+ // Unknown names map to eScriptedExtensionInvalid, the zero value.
+ llvm::StringMap<ScriptedExtension> extension_by_plugin;
+ for (uint32_t i = 0; i < PluginManager::GetNumScriptedInterfaces(); i++)
+ extension_by_plugin[PluginManager::GetScriptedInterfaceNameAtIndex(i)] =
+ PluginManager::GetScriptedInterfaceExtensionAtIndex(i);
+
+ std::vector<ScriptedInstanceGroup> groups;
+ for (ScriptedInstanceInfo &info : GetInstances()) {
+ ScriptedExtension extension = extension_by_plugin.lookup(info.plugin_name);
+ if (!extensions.empty() && !llvm::is_contained(extensions, extension))
+ continue;
+
+ auto it = llvm::find_if(groups, [&](const ScriptedInstanceGroup &group) {
+ return group.class_name == info.class_name &&
+ group.source_path == info.source_path &&
+ group.extension == extension;
+ });
+ if (it == groups.end()) {
+ it = groups.insert(groups.end(), ScriptedInstanceGroup());
+ it->class_name = info.class_name;
+ it->extension = extension;
+ it->source_path = info.source_path;
+ }
+ it->instances.push_back(std::move(info));
+ }
+
+ llvm::sort(groups, [](const ScriptedInstanceGroup &lhs,
+ const ScriptedInstanceGroup &rhs) {
+ return std::make_tuple(llvm::StringRef(lhs.class_name),
+ lhs.source_path.GetPath(), lhs.extension) <
+ std::make_tuple(llvm::StringRef(rhs.class_name),
+ rhs.source_path.GetPath(), rhs.extension);
+ });
+ return groups;
+}
+
+StructuredData::DictionarySP ScriptedInstanceGroup::ToStructuredData() const {
+ auto group_sp = std::make_shared<StructuredData::Dictionary>();
+ group_sp->AddStringItem("class_name", class_name);
+ group_sp->AddStringItem("extension",
+ ScriptInterpreter::ExtensionToString(extension));
+ if (source_path)
+ group_sp->AddStringItem("source_path", source_path.GetPath());
+
+ auto instances_sp = std::make_shared<StructuredData::Array>();
+ for (const ScriptedInstanceInfo &info : instances) {
+ auto instance_sp = std::make_shared<StructuredData::Dictionary>();
+ instance_sp->AddStringItem("uuid", info.uuid.GetAsString());
+ if (info.object_address != LLDB_INVALID_ADDRESS)
+ instance_sp->AddIntegerItem("address", info.object_address);
+ if (HasArgs(info))
+ instance_sp->AddItem("args", info.args_sp);
+ instances_sp->AddItem(instance_sp);
+ }
+ group_sp->AddItem("instances", instances_sp);
+ return group_sp;
+}
+
+void ScriptedInstanceGroup::Dump(Stream &s, bool use_color) const {
+ constexpr llvm::StringLiteral label_code =
+ ANSI_ESCAPE1(ANSI_FG_COLOR_GREEN) ANSI_ESCAPE1(ANSI_CTRL_BOLD);
+ constexpr llvm::StringLiteral name_code =
+ ANSI_ESCAPE1(ANSI_FG_COLOR_CYAN) ANSI_ESCAPE1(ANSI_CTRL_BOLD);
+ constexpr llvm::StringLiteral reset_code = ANSI_ESCAPE1(ANSI_CTRL_NORMAL);
+ const llvm::StringRef label_color = use_color ? label_code : "";
+ const llvm::StringRef name_color = use_color ? name_code : "";
+ const llvm::StringRef reset = use_color ? reset_code : "";
+
+ auto print_label = [&](llvm::StringRef indent, llvm::StringRef label) {
+ s << indent << label_color << label << ':' << reset;
+ };
+ auto print_instance = [&](const ScriptedInstanceInfo &info) {
+ s << info.uuid.GetAsString();
+ if (info.object_address != LLDB_INVALID_ADDRESS)
+ s.Format(" ({0:x})", info.object_address);
+ s << '\n';
+ };
+ auto print_args = [&](const ScriptedInstanceInfo &info,
+ llvm::StringRef indent) {
+ if (!HasArgs(info))
+ return;
+ print_label(indent, "Args");
+ s << '\n';
+ // Dictionaries don't keep insertion order, so sort for stable output.
+ std::vector<std::pair<llvm::StringRef, StructuredData::Object *>> args;
+ info.args_sp->ForEach(
+ [&](llvm::StringRef key, StructuredData::Object *value) {
+ args.emplace_back(key, value);
+ return true;
+ });
+ llvm::sort(args, llvm::less_first());
+ for (const auto &[key, value] : args) {
+ s << indent << " - " << key << ": ";
+ if (StructuredData::String *str = value->GetAsString())
+ s << str->GetValue();
+ else
+ value->Dump(s, /*pretty_print=*/false);
+ s << '\n';
+ }
+ };
+
+ print_label(" ", "Class");
+ s << ' ' << name_color << class_name << reset << '\n';
+ print_label(" ", "Extension");
+ s << ' ' << ScriptInterpreter::ExtensionToString(extension) << '\n';
+ if (source_path) {
+ print_label(" ", "Path");
+ s << ' ' << source_path.GetPath() << '\n';
+ }
+
+ if (instances.size() == 1) {
+ print_label(" ", "Instance");
+ s << ' ';
+ print_instance(instances.front());
+ print_args(instances.front(), " ");
+ return;
+ }
+
+ print_label(" ", "Instances");
+ s << '\n';
+ for (auto [idx, info] : llvm::enumerate(instances)) {
+ s.Format(" [{0}] ", idx);
+ print_instance(info);
+ print_args(info, " ");
+ }
+}
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index eb74e8caea40e..54b91001ed657 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -416,6 +416,23 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
m_object_instance_sp = StructuredData::GenericSP(
new StructuredPythonObject(std::move(result)));
+
+ std::string qualified_class_name = class_name.str();
+ PythonString obj_module_name =
+ obj_class.GetAttributeValue("__module__").AsType<PythonString>();
+ if (obj_module_name.IsValid()) {
+ if (qualified_class_name.empty())
+ qualified_class_name =
+ llvm::formatv("{0}.{1}", obj_module_name.GetString(),
+ obj_class_name.GetString())
+ .str();
+ if (!m_scripted_metadata->GetSourcePath())
+ m_scripted_metadata->SetSourcePath(
+ m_interpreter.GetImportedModulePath(obj_module_name.GetString()));
+ }
+ RegisterInstance(m_interpreter.GetScriptedInstanceRegistry(),
+ qualified_class_name);
+
return m_object_instance_sp;
}
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index bb6d96be97d10..044f3f56c16bd 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -2672,6 +2672,21 @@ bool ScriptInterpreterPythonImpl::LoadScriptingModule(
if (error.Fail())
return false;
+ // __lldb_init_module may instantiate extensions from the module, so the
+ // path must be known before it runs.
+ if (llvm::Expected<PythonModule> py_module =
+ PythonModule::Import(module_name)) {
+ PythonObject py_file = py_module->GetAttributeValue("__file__");
+ if (PythonString::Check(py_file.get())) {
+ FileSpec module_path(
+ PythonString(PyRefType::Borrowed, py_file.get()).GetString());
+ FileSystem::Instance().Resolve(module_path);
+ SetImportedModulePath(module_name, module_path);
+ }
+ } else {
+ llvm::consumeError(py_module.takeError());
+ }
+
// if we are here, everything worked
// call __lldb_init_module(debugger,dict)
if (!SWIGBridge::LLDBSwigPythonCallModuleInit(
diff --git a/lldb/test/API/commands/scripting/extension/TestScriptingExtensionInstances.py b/lldb/test/API/commands/scripting/extension/TestScriptingExtensionInstances.py
new file mode 100644
index 0000000000000..ba96843d19bd1
--- /dev/null
+++ b/lldb/test/API/commands/scripting/extension/TestScriptingExtensionInstances.py
@@ -0,0 +1,154 @@
+"""
+Verify that `scripting extension instances` reports the live scripted
+extension objects along with the script file their class was imported from.
+"""
+
+import os
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+
+
+class TestScriptingExtensionInstances(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ def list_instances(self, args=""):
+ result = lldb.SBCommandReturnObject()
+ self.dbg.GetCommandInterpreter().HandleCommand(
+ "scripting extension instances --json " + args, result
+ )
+ self.assertTrue(result.Succeeded(), result.GetError())
+ data = lldb.SBStructuredData()
+ self.assertSuccess(data.SetFromJSON(result.GetOutput()))
+ self.assertEqual(data.GetType(), lldb.eStructuredDataTypeArray)
+ return [data.GetItemAtIndex(i) for i in range(data.GetSize())]
+
+ def find_group(self, class_name, args=""):
+ for group in self.list_instances(args):
+ if group.GetValueForKey("class_name").GetStringValue(256) == class_name:
+ return group
+ return None
+
+ def get_instances(self, group):
+ instances = group.GetValueForKey("instances")
+ return [instances.GetItemAtIndex(i) for i in range(instances.GetSize())]
+
+ def test_instances(self):
+ script_path = os.path.join(self.getSourceDir(), "instance_cmds.py")
+ self.runCmd("command script import " + script_path)
+ self.runCmd("command script add -c instance_cmds.EchoCommand echo-cmd")
+ self.addTearDownHook(
+ lambda: self.runCmd("command script delete echo-cmd", check=False)
+ )
+
+ group = self.find_group("instance_cmds.EchoCommand")
+ self.assertIsNotNone(group, "scripted command instance is not listed")
+ self.assertEqual(
+ group.GetValueForKey("extension").GetStringValue(256), "ScriptedCommand"
+ )
+ self.assertEqual(
+ os.path.realpath(
+ group.GetValueForKey("source_path").GetStringValue(4096)
+ ),
+ os.path.realpath(script_path),
+ )
+ (instance,) = self.get_instances(group)
+ self.assertTrue(instance.GetValueForKey("uuid").GetStringValue(64))
+ self.assertNotEqual(instance.GetValueForKey("address").GetUnsignedIntegerValue(), 0)
+ # Scripted commands take no extra arguments.
+ self.assertFalse(instance.GetValueForKey("args").IsValid())
+
+ self.assertIsNotNone(
+ self.find_group("instance_cmds.EchoCommand", "ScriptedCommand")
+ )
+ self.assertIsNone(
+ self.find_group("instance_cmds.EchoCommand", "ScriptedProcess")
+ )
+
+ self.expect(
+ "scripting extension instances",
+ substrs=[
+ "instance_cmds.EchoCommand",
+ "Extension: ScriptedCommand",
+ "Path: " + script_path,
+ "Instance: ",
+ ],
+ )
+
+ self.runCmd("command script delete echo-cmd")
+ self.assertIsNone(self.find_group("instance_cmds.EchoCommand"))
+
+ def test_instances_grouped_by_class(self):
+ script_path = os.path.join(self.getSourceDir(), "instance_cmds.py")
+ self.runCmd("command script import " + script_path)
+ self.assertTrue(self.dbg.CreateTarget(""), VALID_TARGET)
+ self.runCmd(
+ "target stop-hook add -P instance_cmds.StopHook -k answer -v 42"
+ )
+ self.runCmd(
+ "target stop-hook add -P instance_cmds.StopHook -k answer -v 42"
+ )
+
+ group = self.find_group("instance_cmds.StopHook", "ScriptedHook")
+ self.assertIsNotNone(group, "stop hook instances are not listed")
+ self.assertEqual(
+ os.path.realpath(
+ group.GetValueForKey("source_path").GetStringValue(4096)
+ ),
+ os.path.realpath(script_path),
+ )
+
+ # Identical class and arguments, but still two distinct instances.
+ instances = self.get_instances(group)
+ self.assertEqual(len(instances), 2)
+ uuids = {i.GetValueForKey("uuid").GetStringValue(64) for i in instances}
+ self.assertEqual(len(uuids), 2)
+ # Both objects are alive at once, so they can't share an address.
+ addresses = {
+ i.GetValueForKey("address").GetUnsignedIntegerValue() for i in instances
+ }
+ self.assertEqual(len(addresses), 2)
+ for instance in instances:
+ args = instance.GetValueForKey("args")
+ # Stop hooks store numeric values as integers.
+ self.assertEqual(
+ args.GetValueForKey("answer").GetUnsignedIntegerValue(), 42
+ )
+
+ self.expect(
+ "scripting extension instances ScriptedHook",
+ substrs=["instance_cmds.StopHook", "Instances:", "[0] ", "[1] ", "- answer: 42"],
+ )
+
+ def test_destroyed_instances_are_removed(self):
+ script_path = os.path.join(self.getSourceDir(), "instance_cmds.py")
+ self.runCmd("command script import " + script_path)
+ target = self.dbg.CreateTarget("")
+ self.assertTrue(target, VALID_TARGET)
+ self.runCmd("target stop-hook add -P instance_cmds.StopHook -k name -v first")
+ self.runCmd("target stop-hook add -P instance_cmds.StopHook -k name -v second")
+
+ def names():
+ group = self.find_group("instance_cmds.StopHook")
+ if not group:
+ return []
+ return [
+ i.GetValueForKey("args").GetValueForKey("name").GetStringValue(64)
+ for i in self.get_instances(group)
+ ]
+
+ self.assertEqual(names(), ["first", "second"])
+
+ self.runCmd("target stop-hook delete 1")
+ self.assertEqual(names(), ["second"])
+
+ self.assertTrue(self.dbg.DeleteTarget(target))
+ self.assertEqual(names(), [])
+
+ def test_invalid_extension_name(self):
+ self.expect(
+ "scripting extension instances NotAnExtension",
+ error=True,
+ substrs=["no scripted extension named 'NotAnExtension'"],
+ )
diff --git a/lldb/test/API/commands/scripting/extension/instance_cmds.py b/lldb/test/API/commands/scripting/extension/instance_cmds.py
new file mode 100644
index 0000000000000..0063a989dad98
--- /dev/null
+++ b/lldb/test/API/commands/scripting/extension/instance_cmds.py
@@ -0,0 +1,17 @@
+import lldb
+
+
+class EchoCommand:
+ def __init__(self, debugger, internal_dict):
+ pass
+
+ def __call__(self, debugger, command, exe_ctx, result):
+ result.AppendMessage(command)
+
+
+class StopHook:
+ def __init__(self, target, extra_args, internal_dict):
+ pass
+
+ def handle_stop(self, exe_ctx, stream):
+ return True
diff --git a/lldb/unittests/Interpreter/TestScriptedInterface.cpp b/lldb/unittests/Interpreter/TestScriptedInterface.cpp
index 48fd98e601e0e..1f74ae217c37d 100644
--- a/lldb/unittests/Interpreter/TestScriptedInterface.cpp
+++ b/lldb/unittests/Interpreter/TestScriptedInterface.cpp
@@ -8,6 +8,7 @@
#include "lldb/Interpreter/Interfaces/ScriptedCommandInterface.h"
#include "lldb/Interpreter/Interfaces/ScriptedInterface.h"
+#include "lldb/Interpreter/ScriptedInstanceRegistry.h"
#include "gtest/gtest.h"
using namespace lldb_private;
@@ -36,8 +37,52 @@ class DummyScriptedCommandInterface : public ScriptedCommandInterface {
}
};
+class RegisteredScriptedInterface : public DummyScriptedInterface {
+public:
+ RegisteredScriptedInterface(
+ const lldb::ScriptedInstanceRegistrySP ®istry_sp,
+ llvm::StringRef class_name) {
+ RegisterInstance(registry_sp, class_name);
+ }
+
+ void Reregister(const lldb::ScriptedInstanceRegistrySP ®istry_sp) {
+ RegisterInstance(registry_sp, "module.Reregistered");
+ }
+};
+
} // namespace
+TEST(ScriptedInterfaceTest, DestroyedInstanceIsUnregistered) {
+ auto registry_sp = std::make_shared<ScriptedInstanceRegistry>();
+ auto first = std::make_unique<RegisteredScriptedInterface>(registry_sp,
+ "module.First");
+ RegisteredScriptedInterface second(registry_sp, "module.Second");
+ ASSERT_EQ(registry_sp->GetInstances().size(), 2u);
+
+ first.reset();
+ std::vector<ScriptedInstanceInfo> instances = registry_sp->GetInstances();
+ ASSERT_EQ(instances.size(), 1u);
+ EXPECT_EQ(instances[0].class_name, "module.Second");
+}
+
+TEST(ScriptedInterfaceTest, ReregisteringReplacesEntry) {
+ auto registry_sp = std::make_shared<ScriptedInstanceRegistry>();
+ RegisteredScriptedInterface interface(registry_sp, "module.First");
+ interface.Reregister(registry_sp);
+
+ std::vector<ScriptedInstanceInfo> instances = registry_sp->GetInstances();
+ ASSERT_EQ(instances.size(), 1u);
+ EXPECT_EQ(instances[0].class_name, "module.Reregistered");
+}
+
+TEST(ScriptedInterfaceTest, InterfaceCanOutliveRegistry) {
+ auto registry_sp = std::make_shared<ScriptedInstanceRegistry>();
+ auto interface =
+ std::make_unique<RegisteredScriptedInterface>(registry_sp, "module.A");
+ registry_sp.reset();
+ interface.reset();
+}
+
TEST(ScriptedInterfaceTest, ExtensionsCannotBeRunDirectly) {
DummyScriptedInterface interface;
EXPECT_FALSE(interface.UserCanRunDirectly());
More information about the lldb-commits
mailing list