[Lldb-commits] [lldb] ee7d9f3 - [lldb] Add `scripting extension list --instances` (#226043)

via lldb-commits lldb-commits at lists.llvm.org
Tue Oct 13 21:49:56 PDT 2026


Author: Med Ismail Bennani
Date: 2026-10-13T21:49:50-07:00
New Revision: ee7d9f3f4a13d4432223f05a2e10d6cf533e0484

URL: https://github.com/llvm/llvm-project/commit/ee7d9f3f4a13d4432223f05a2e10d6cf533e0484
DIFF: https://github.com/llvm/llvm-project/commit/ee7d9f3f4a13d4432223f05a2e10d6cf533e0484.diff

LOG: [lldb] Add `scripting extension list --instances` (#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 resolving, for each extension object, the
file of the module that defines its class, and attributing it to the
object's `ScriptedMetadata`. It tracks every successfully created
extension object in a registry owned by the script interpreter, and
lists them with the new `--instances` flag of `scripting extension
list`, grouped by class. Each instance shows a UUID, the address of its
script object and the arguments it was created with:

```
(lldb) scripting extension list --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
```

This command is included to diagnostic report to help when investigating
scripting related bugs.

Assisted-by: Claude

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>

Added: 
    lldb/include/lldb/Interpreter/ScriptedInstanceRegistry.h
    lldb/source/Interpreter/ScriptedInstanceRegistry.cpp
    lldb/test/API/commands/scripting/extension/TestScriptingExtensionInstances.py
    lldb/test/API/commands/scripting/extension/instance_cmds.py
    lldb/test/API/commands/scripting/extension/instance_pkg/__init__.py
    lldb/test/API/commands/scripting/extension/instance_pkg/cmds.py
    lldb/test/Shell/Diagnostics/Inputs/scripted_command.py
    lldb/test/Shell/Diagnostics/TestScriptedExtensionInstances.test

Modified: 
    lldb/include/lldb/Core/PluginManager.h
    lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
    lldb/include/lldb/Interpreter/ScriptInterpreter.h
    lldb/include/lldb/Utility/ScriptedMetadata.h
    lldb/include/lldb/lldb-forward.h
    lldb/source/Commands/CommandObjectScripting.cpp
    lldb/source/Commands/Options.td
    lldb/source/Core/Diagnostics.cpp
    lldb/source/Core/PluginManager.cpp
    lldb/source/Interpreter/CMakeLists.txt
    lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
    lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py
    lldb/unittests/Interpreter/TestScriptedInterface.cpp

Removed: 
    


################################################################################
diff  --git a/lldb/include/lldb/Core/PluginManager.h b/lldb/include/lldb/Core/PluginManager.h
index 94d90db2854ba..f38392ff7ac04 100644
--- a/lldb/include/lldb/Core/PluginManager.h
+++ b/lldb/include/lldb/Core/PluginManager.h
@@ -675,7 +675,7 @@ class PluginManager {
 
   static bool UnregisterPlugin(ScriptedInterfaceCreateInstance create_callback);
 
-  static uint32_t GetNumScriptedInterfaces();
+  static size_t GetNumScriptedInterfaces();
 
   static llvm::StringRef GetScriptedInterfaceNameAtIndex(uint32_t idx);
 

diff  --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
index 79c2f08a1820b..690d9cdc4954c 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"
@@ -20,6 +21,7 @@
 
 #include "llvm/Support/Compiler.h"
 
+#include <cassert>
 #include <optional>
 #include <string>
 
@@ -27,7 +29,15 @@ namespace lldb_private {
 class ScriptedInterface {
 public:
   ScriptedInterface() = default;
-  virtual ~ScriptedInterface() = default;
+  virtual ~ScriptedInterface() { UnregisterInstance(); }
+
+  /// Delete the copy constructor. The copy would share the registry entry, and
+  /// the first one destroyed would unregister 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 +47,11 @@ class ScriptedInterface {
     return m_scripted_metadata;
   }
 
+  /// The name this interface is registered under with the PluginManager.
+  /// Every concrete scripted interface registers itself as a plugin, together
+  /// with the kind of extension it drives, so this name identifies that kind.
+  virtual llvm::StringRef GetPluginName() = 0;
+
   /// 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 +116,36 @@ class ScriptedInterface {
   }
 
 protected:
+  void RegisterInstance(const lldb::ScriptedInstanceRegistrySP &registry_sp,
+                        std::string class_name, FileSpec source_path,
+                        StructuredData::DictionarySP args_sp) {
+    assert(registry_sp && "registering with an invalid registry");
+    assert(!m_registry_wp.lock() && "script object is already registered");
+    ScriptedInstanceInfo info;
+    info.plugin_name = GetPluginName();
+    info.class_name = std::move(class_name);
+    if (m_object_instance_sp)
+      info.object_address =
+          reinterpret_cast<uintptr_t>(m_object_instance_sp->GetValue());
+    info.source_path = std::move(source_path);
+    info.args_sp = std::move(args_sp);
+    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..1b8cf605bee67 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -22,11 +22,13 @@
 #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 <memory>
 #include <optional>
 
 namespace lldb_private {
@@ -379,6 +381,10 @@ class ScriptInterpreter : public PluginInterface {
                       FileSpec extra_search_dir = {},
                       lldb::TargetSP loaded_into_target_sp = {});
 
+  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 +528,10 @@ class ScriptInterpreter : public PluginInterface {
 protected:
   Debugger &m_debugger;
   lldb::ScriptLanguage m_script_lang;
+
+private:
+  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..331e716289acd
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/ScriptedInstanceRegistry.h
@@ -0,0 +1,82 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/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;
+  /// The interface's PluginManager name, from the string literal its
+  /// GetPluginNameStatic() returns, so this never dangles.
+  llvm::StringRef plugin_name;
+  std::string class_name;
+  FileSpec source_path;
+  StructuredData::DictionarySP args_sp;
+};
+
+/// The live instances of one extension class. Instances belong to the same
+/// group when they share a class name, source path and extension kind, so a
+/// class reused for several kinds of extension gets one group per kind.
+struct ScriptedInstanceGroup {
+  std::string class_name;
+  lldb::ScriptedExtension extension = lldb::eScriptedExtensionInvalid;
+  FileSpec source_path;
+  std::vector<ScriptedInstanceInfo> instances;
+
+  bool operator<(const ScriptedInstanceGroup &other) const;
+
+  StructuredData::DictionarySP ToStructuredData() const;
+
+  void Dump(Stream &s, const Debugger &debugger, 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);
+
+  void Remove(uint64_t id);
+
+  std::vector<ScriptedInstanceInfo> GetInstances() const;
+
+  std::vector<ScriptedInstanceGroup> GetInstanceGroups(
+      llvm::ArrayRef<lldb::ScriptedExtension> extensions = {}) const;
+
+private:
+  std::vector<ScriptedInstanceInfo> CopyInstances() const;
+
+  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 7e6fdb7af2f40..d22bb2e23baef 100644
--- a/lldb/source/Commands/CommandObjectScripting.cpp
+++ b/lldb/source/Commands/CommandObjectScripting.cpp
@@ -143,9 +143,11 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed {
   CommandObjectScriptingExtensionList(CommandInterpreter &interpreter)
       : CommandObjectParsed(
             interpreter, "scripting extension list",
-            "List all the available scripting extension templates. ",
-            "scripting extension list [--language <scripting-language> --] "
-            "[--json --] [<extension-name> ...]") {
+            "List the available scripting extension templates, or with "
+            "--instances, the instantiated scripting extensions grouped by "
+            "class.",
+            "scripting extension list [--language <scripting-language>] "
+            "[--json] [--instances] [<extension-name> ...]") {
     AddSimpleArgumentList(eArgTypeScriptedExtension, eArgRepeatStar);
   }
 
@@ -182,6 +184,9 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed {
       case 'j':
         m_json_format = true;
         break;
+      case 'i':
+        m_instances = true;
+        break;
       default:
         llvm_unreachable("Unimplemented option");
       }
@@ -192,6 +197,7 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed {
     void OptionParsingStarting(ExecutionContext *execution_context) override {
       m_language = lldb::eScriptLanguageDefault;
       m_json_format = false;
+      m_instances = false;
     }
 
     llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
@@ -200,10 +206,16 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed {
 
     lldb::ScriptLanguage m_language = lldb::eScriptLanguageDefault;
     bool m_json_format = false;
+    bool m_instances = false;
   };
 
 protected:
   void DoExecute(Args &command, CommandReturnObject &result) override {
+    if (m_options.m_instances) {
+      ListInstances(command, result);
+      return;
+    }
+
     llvm::StringMap<std::vector<size_t>> grouped_by_extension;
     for (size_t i = 0; i < PluginManager::GetNumScriptedInterfaces(); i++) {
       lldb::ScriptedExtension extension =
@@ -254,6 +266,53 @@ class CommandObjectScriptingExtensionList : public CommandObjectParsed {
                                          use_color);
   }
 
+  void ListInstances(Args &command, CommandReturnObject &result) {
+    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,
+                                               m_options.m_language))
+      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();
+    const Debugger &debugger = GetDebugger();
+    const std::string separator = GetSeparator(debugger, use_color);
+
+    s.PutCString("Instantiated scripted extensions:");
+    if (groups.empty())
+      s << " None";
+    s.EOL();
+    for (const ScriptedInstanceGroup &group : groups) {
+      s << separator << '\n';
+      group.Dump(s, debugger, use_color);
+    }
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+  }
+
   std::vector<std::string>
   GetLanguagesForExtension(const std::vector<size_t> &indices) {
     std::vector<std::string> languages;

diff  --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td
index 66f54fa7e6493..8c1944716b391 100644
--- a/lldb/source/Commands/Options.td
+++ b/lldb/source/Commands/Options.td
@@ -1594,6 +1594,10 @@ let Command = "scripting extension list" in {
   def scripting_extension_list_json
       : Option<"json", "j">,
         Desc<"Output the scripted extension list in json format.">;
+  def scripting_extension_list_instances
+      : Option<"instances", "i">,
+        Desc<"List the instantiated scripted extensions, grouped by class, "
+             "instead of the available templates.">;
 }
 
 let Command = "scripting extension generate" in {

diff  --git a/lldb/source/Core/Diagnostics.cpp b/lldb/source/Core/Diagnostics.cpp
index 08adcaebdf038..445ebc56d0f4e 100644
--- a/lldb/source/Core/Diagnostics.cpp
+++ b/lldb/source/Core/Diagnostics.cpp
@@ -237,6 +237,7 @@ static constexpr TriageCommand g_triage_commands[] = {
     {"image lookup -va $pc", Requires::Frame},
     {"register read", Requires::Frame},
     {"frame variable", Requires::Frame},
+    {"scripting extension list --instances", Requires::Always},
 };
 
 static bool Available(Requires requirement, const ExecutionContext &exe_ctx) {

diff  --git a/lldb/source/Core/PluginManager.cpp b/lldb/source/Core/PluginManager.cpp
index 303f4701246c9..a6639f7213406 100644
--- a/lldb/source/Core/PluginManager.cpp
+++ b/lldb/source/Core/PluginManager.cpp
@@ -2095,7 +2095,7 @@ bool PluginManager::UnregisterPlugin(
   return GetScriptedInterfaceInstances().UnregisterPlugin(create_callback);
 }
 
-uint32_t PluginManager::GetNumScriptedInterfaces() {
+size_t PluginManager::GetNumScriptedInterfaces() {
   return GetScriptedInterfaceInstances().GetSnapshot().size();
 }
 

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/ScriptedInstanceRegistry.cpp b/lldb/source/Interpreter/ScriptedInstanceRegistry.cpp
new file mode 100644
index 0000000000000..1e6cdf57815a2
--- /dev/null
+++ b/lldb/source/Interpreter/ScriptedInstanceRegistry.cpp
@@ -0,0 +1,191 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/Debugger.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"
+
+using namespace lldb;
+using namespace lldb_private;
+
+static bool HasArgs(const ScriptedInstanceInfo &info) {
+  return info.args_sp && info.args_sp->GetSize();
+}
+
+static bool CreatedBefore(const ScriptedInstanceInfo &lhs,
+                          const ScriptedInstanceInfo &rhs) {
+  return lhs.id < rhs.id;
+}
+
+uint64_t ScriptedInstanceRegistry::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 ScriptedInstanceRegistry::Remove(uint64_t id) {
+  std::lock_guard<std::mutex> guard(m_mutex);
+  m_instances.erase(id);
+}
+
+std::vector<ScriptedInstanceInfo>
+ScriptedInstanceRegistry::CopyInstances() const {
+  std::lock_guard<std::mutex> guard(m_mutex);
+  std::vector<ScriptedInstanceInfo> instances;
+  instances.reserve(m_instances.size());
+  for (const auto &entry : m_instances)
+    instances.push_back(entry.second);
+  return instances;
+}
+
+std::vector<ScriptedInstanceInfo>
+ScriptedInstanceRegistry::GetInstances() const {
+  std::vector<ScriptedInstanceInfo> instances = CopyInstances();
+  llvm::sort(instances, CreatedBefore);
+  return instances;
+}
+
+bool ScriptedInstanceGroup::operator<(
+    const ScriptedInstanceGroup &other) const {
+  if (class_name != other.class_name)
+    return class_name < other.class_name;
+
+  const std::string path = source_path.GetPath();
+  const std::string other_path = other.source_path.GetPath();
+  if (path != other_path)
+    return path < other_path;
+
+  return extension < other.extension;
+}
+
+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 (size_t i = 0; i < PluginManager::GetNumScriptedInterfaces(); i++)
+    extension_by_plugin[PluginManager::GetScriptedInterfaceNameAtIndex(i)] =
+        PluginManager::GetScriptedInterfaceExtensionAtIndex(i);
+
+  std::vector<ScriptedInstanceGroup> groups;
+  for (ScriptedInstanceInfo &info : CopyInstances()) {
+    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));
+  }
+
+  for (ScriptedInstanceGroup &group : groups)
+    llvm::sort(group.instances, CreatedBefore);
+  llvm::sort(groups);
+  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, const Debugger &debugger,
+                                 bool use_color) const {
+  auto ansi_code = [use_color](llvm::StringRef code) {
+    return ansi::FormatAnsiTerminalCodes(code, use_color);
+  };
+  const std::string label_prefix = ansi_code(debugger.GetLabelAnsiPrefix());
+  const std::string label_suffix = ansi_code(debugger.GetLabelAnsiSuffix());
+  const std::string title_prefix = ansi_code(debugger.GetTitleAnsiPrefix());
+  const std::string title_suffix = ansi_code(debugger.GetTitleAnsiSuffix());
+
+  auto print_label = [&](llvm::StringRef indent, llvm::StringRef label) {
+    s << indent << label_prefix << label << ':' << label_suffix;
+  };
+  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';
+    info.args_sp->ForEachSorted(
+        [&](llvm::StringRef key, StructuredData::Object *value) {
+          s << indent << "  - " << key << ": ";
+          if (StructuredData::String *str = value->GetAsString())
+            s << str->GetValue();
+          else
+            value->Dump(s, /*pretty_print=*/false);
+          s << '\n';
+          return true;
+        });
+  };
+
+  print_label("  ", "Class");
+  s << ' ' << title_prefix << class_name << title_suffix << '\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..ccc340fe69e27 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -90,23 +90,32 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
           "scripted Interface python object '__class__.__module__' attribute "
           "is not a string");
 
-    llvm::StringRef py_obj_module_str_ref = py_obj_module_str.GetString();
-    PythonModule py_module = PythonModule::AddModule(py_obj_module_str_ref);
+    return GetModuleFile(py_obj_module_str.GetString());
+  }
+
+  /// The file the already imported \p module_name was loaded from, read from
+  /// its `__file__`. The caller must hold the GIL.
+  static llvm::Expected<FileSpec> GetModuleFile(llvm::StringRef module_name) {
+    using namespace python;
+
+    PythonModule py_module = PythonModule::AddModule(module_name);
     if (!py_module.IsValid())
-      return llvm::createStringError("failed to import '%s' module",
-                                     py_obj_module_str_ref.data());
+      return llvm::createStringError(
+          llvm::formatv("failed to import '{0}' module", module_name).str());
 
     PythonObject py_module_file = py_module.GetAttributeValue("__file__");
     if (!py_module_file.IsValid())
       return llvm::createStringError(
-          "module '%s' is missing '__file__' attribute",
-          py_obj_module_str_ref.data());
+          llvm::formatv("module '{0}' is missing '__file__' attribute",
+                        module_name)
+              .str());
 
     PythonString py_module_file_str = py_module_file.Str();
     if (!py_module_file_str.IsValid())
       return llvm::createStringError(
-          "module '%s.__file__' attribute is not a string",
-          py_obj_module_str_ref.data());
+          llvm::formatv("module '{0}.__file__' attribute is not a string",
+                        module_name)
+              .str());
 
     return FileSpec(py_module_file_str.GetString());
   }
@@ -416,6 +425,37 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
 
     m_object_instance_sp = StructuredData::GenericSP(
         new StructuredPythonObject(std::move(result)));
+
+    // An object handed over by the script wasn't built from the metadata,
+    // which may even describe its creator, so name it after its own class and
+    // don't attribute the metadata's arguments to it.
+    std::string qualified_class_name = script_obj ? "" : class_name.str();
+    PythonString obj_module_name =
+        obj_class.GetAttributeValue("__module__").AsType<PythonString>();
+    if (qualified_class_name.empty() && obj_module_name.IsValid())
+      qualified_class_name =
+          llvm::formatv("{0}.{1}", obj_module_name.GetString(),
+                        obj_class_name.GetString())
+              .str();
+
+    // Modules can be imported or reloaded without LLDB seeing it, so ask the
+    // class's module for its current file rather than remembering one.
+    FileSpec source_path;
+    if (obj_module_name.IsValid()) {
+      if (llvm::Expected<FileSpec> path_or_err =
+              GetModuleFile(obj_module_name.GetString()))
+        source_path = *path_or_err;
+      else
+        LLDB_LOG_ERROR(GetLog(LLDBLog::Script), path_or_err.takeError(),
+                       "failed to find the file defining {1}: {0}",
+                       qualified_class_name);
+    }
+    if (!script_obj)
+      m_scripted_metadata->SetSourcePath(source_path);
+    RegisterInstance(m_interpreter.GetScriptedInstanceRegistry(),
+                     std::move(qualified_class_name), std::move(source_path),
+                     script_obj ? nullptr : m_scripted_metadata->GetArgsSP());
+
     return m_object_instance_sp;
   }
 

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..bbf023d4c1d6d
--- /dev/null
+++ b/lldb/test/API/commands/scripting/extension/TestScriptingExtensionInstances.py
@@ -0,0 +1,187 @@
+"""
+Verify that `scripting extension list --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 list --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 list --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 list --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_path_without_command_script_import(self):
+        self.runCmd(
+            "script import sys; sys.path.insert(0, %r); import instance_cmds"
+            % self.getSourceDir()
+        )
+        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(
+            os.path.realpath(group.GetValueForKey("source_path").GetStringValue(4096)),
+            os.path.realpath(os.path.join(self.getSourceDir(), "instance_cmds.py")),
+        )
+
+    def test_path_of_class_in_submodule(self):
+        pkg_path = os.path.join(self.getSourceDir(), "instance_pkg")
+        self.runCmd("command script import " + pkg_path)
+        self.runCmd("command script add -c instance_pkg.cmds.EchoCommand echo-cmd")
+        self.addTearDownHook(
+            lambda: self.runCmd("command script delete echo-cmd", check=False)
+        )
+
+        # The module defining the class, not the package that was imported.
+        group = self.find_group("instance_pkg.cmds.EchoCommand")
+        self.assertIsNotNone(group, "scripted command instance is not listed")
+        self.assertEqual(
+            os.path.realpath(group.GetValueForKey("source_path").GetStringValue(4096)),
+            os.path.realpath(os.path.join(pkg_path, "cmds.py")),
+        )
+
+    def test_invalid_extension_name(self):
+        self.expect(
+            "scripting extension list --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/test/API/commands/scripting/extension/instance_pkg/__init__.py b/lldb/test/API/commands/scripting/extension/instance_pkg/__init__.py
new file mode 100644
index 0000000000000..007f143bbaa18
--- /dev/null
+++ b/lldb/test/API/commands/scripting/extension/instance_pkg/__init__.py
@@ -0,0 +1 @@
+from . import cmds

diff  --git a/lldb/test/API/commands/scripting/extension/instance_pkg/cmds.py b/lldb/test/API/commands/scripting/extension/instance_pkg/cmds.py
new file mode 100644
index 0000000000000..d792fb91f3dad
--- /dev/null
+++ b/lldb/test/API/commands/scripting/extension/instance_pkg/cmds.py
@@ -0,0 +1,9 @@
+import lldb
+
+
+class EchoCommand:
+    def __init__(self, debugger, internal_dict):
+        pass
+
+    def __call__(self, debugger, command, exe_ctx, result):
+        result.AppendMessage(command)

diff  --git a/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py b/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py
index 8ca84a090a324..094286ae63da7 100644
--- a/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py
+++ b/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py
@@ -170,6 +170,78 @@ def cleanup():
 
         self.assertIn("Failed to get scripted thread registers data.", log)
 
+    @skipUnlessDarwin
+    def test_scripted_extension_instances(self):
+        """Test that the threads and frames a scripted process creates in Python
+        are listed under their own classes, not under the process's."""
+        self.build()
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target, VALID_TARGET)
+
+        os.environ["SKIP_SCRIPTED_PROCESS_LAUNCH"] = "1"
+
+        def cleanup():
+            del os.environ["SKIP_SCRIPTED_PROCESS_LAUNCH"]
+
+        self.addTearDownHook(cleanup)
+
+        script_path = os.path.join(self.getSourceDir(), "dummy_scripted_process.py")
+        self.runCmd("command script import " + script_path)
+
+        args = lldb.SBStructuredData()
+        self.assertSuccess(args.SetFromJSON('{"answer": 42}'))
+        launch_info = lldb.SBLaunchInfo(None)
+        launch_info.SetProcessPluginName("ScriptedProcess")
+        launch_info.SetScriptedProcessClassName(
+            "dummy_scripted_process.DummyScriptedProcess"
+        )
+        launch_info.SetScriptedProcessDictionary(args)
+
+        error = lldb.SBError()
+        process = target.Launch(launch_info, error)
+        self.assertTrue(process and process.IsValid(), PROCESS_IS_VALID)
+        # Fetching the frames is what wraps the Python frame objects.
+        self.assertGreater(process.GetThreadAtIndex(0).GetNumFrames(), 0)
+
+        result = lldb.SBCommandReturnObject()
+        self.dbg.GetCommandInterpreter().HandleCommand(
+            "scripting extension list --instances --json", result
+        )
+        self.assertTrue(result.Succeeded(), result.GetError())
+        data = lldb.SBStructuredData()
+        self.assertSuccess(data.SetFromJSON(result.GetOutput()))
+        groups = {}
+        for i in range(data.GetSize()):
+            group = data.GetItemAtIndex(i)
+            groups[group.GetValueForKey("class_name").GetStringValue(256)] = group
+
+        def args_of(class_name):
+            instance = groups[class_name].GetValueForKey("instances").GetItemAtIndex(0)
+            return instance.GetValueForKey("args")
+
+        process_class = "dummy_scripted_process.DummyScriptedProcess"
+        thread_class = "dummy_scripted_process.DummyScriptedThread"
+        frame_class = "dummy_scripted_process.DummyScriptedFrame"
+        self.assertIn(process_class, groups)
+        self.assertIn(thread_class, groups)
+        self.assertIn(frame_class, groups)
+        self.assertEqual(groups[process_class].GetValueForKey("instances").GetSize(), 1)
+        for class_name in (thread_class, frame_class):
+            self.assertEqual(
+                os.path.realpath(
+                    groups[class_name]
+                    .GetValueForKey("source_path")
+                    .GetStringValue(4096)
+                ),
+                os.path.realpath(script_path),
+            )
+
+        # Only the process was created from the launch arguments.
+        self.assertEqual(
+            args_of(process_class).GetValueForKey("answer").GetIntegerValue(), 42
+        )
+        self.assertFalse(args_of(thread_class).IsValid())
+
     @skipUnlessDarwin
     def test_scripted_process_and_scripted_thread(self):
         """Test that we can launch an lldb scripted process using the SBAPI,

diff  --git a/lldb/test/Shell/Diagnostics/Inputs/scripted_command.py b/lldb/test/Shell/Diagnostics/Inputs/scripted_command.py
new file mode 100644
index 0000000000000..11c758e12d377
--- /dev/null
+++ b/lldb/test/Shell/Diagnostics/Inputs/scripted_command.py
@@ -0,0 +1,15 @@
+import lldb
+
+
+class EchoCommand:
+    def __init__(self, debugger, internal_dict):
+        pass
+
+    def __call__(self, debugger, command, exe_ctx, result):
+        result.AppendMessage(command)
+
+
+def __lldb_init_module(debugger, internal_dict):
+    debugger.HandleCommand(
+        "command script add -c scripted_command.EchoCommand echo-cmd"
+    )

diff  --git a/lldb/test/Shell/Diagnostics/TestScriptedExtensionInstances.test b/lldb/test/Shell/Diagnostics/TestScriptedExtensionInstances.test
new file mode 100644
index 0000000000000..8dac76d22e7d6
--- /dev/null
+++ b/lldb/test/Shell/Diagnostics/TestScriptedExtensionInstances.test
@@ -0,0 +1,13 @@
+# REQUIRES: python
+# Check that the diagnostics bundle lists the live scripted extensions, so a
+# triager can see which scripts were loaded and from where.
+
+# RUN: rm -rf %t
+# RUN: %lldb --no-lldbinit -b -o 'command script import %S/Inputs/scripted_command.py' \
+# RUN:   -o 'diagnostics dump -d %t'
+# RUN: FileCheck %s --input-file=%t/commands.txt
+
+# CHECK: === scripting extension list --instances ===
+# CHECK: Class: scripted_command.EchoCommand
+# CHECK: Extension: ScriptedCommand
+# CHECK: Path: {{.*}}Inputs{{[/\\]}}scripted_command.py

diff  --git a/lldb/unittests/Interpreter/TestScriptedInterface.cpp b/lldb/unittests/Interpreter/TestScriptedInterface.cpp
index 48fd98e601e0e..bc8ab3ad8888a 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;
@@ -20,6 +21,8 @@ class DummyScriptedInterface : public ScriptedInterface {
   GetAbstractMethodRequirements() const override {
     return {};
   }
+
+  llvm::StringRef GetPluginName() override { return "DummyPlugin"; }
 };
 
 class DummyScriptedCommandInterface : public ScriptedCommandInterface {
@@ -34,10 +37,43 @@ class DummyScriptedCommandInterface : public ScriptedCommandInterface {
   GetAbstractMethodRequirements() const override {
     return {};
   }
+
+  llvm::StringRef GetPluginName() override { return "DummyPlugin"; }
+};
+
+class RegisteredScriptedInterface : public DummyScriptedInterface {
+public:
+  RegisteredScriptedInterface(
+      const lldb::ScriptedInstanceRegistrySP &registry_sp,
+      llvm::StringRef class_name) {
+    RegisterInstance(registry_sp, class_name.str(), /*source_path=*/{},
+                     /*args_sp=*/nullptr);
+  }
 };
 
 } // 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, 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