[llvm-branch-commits] [lldb] [lldb] Backport scripting fixes and improvements to release 23.x branch (PR #221993)

Med Ismail Bennani via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Tue Sep 8 06:12:18 PDT 2026


https://github.com/medismailben created https://github.com/llvm/llvm-project/pull/221993

This PR back ports various fixes and improvements for lldb scripting to the LLVM Release 23.x branch.

>From c9e5146d175164035218eb0dcafe9df6ca1a794a Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Tue, 28 Jul 2026 17:09:59 -0700
Subject: [PATCH 01/16] [lldb/script] Migrate ParsedCommand & raw commands onto
 ScriptedPythonInterface (#210430)

Give both `command script add -c` (raw class) and `-p` (ParsedCommand) a
single, shared
`ScriptedCommandInterface`/`ScriptedCommandPythonInterface`, built on
the existing `ScriptedPythonInterface` machinery: `CreatePluginObject`
delegates to the base template, and every other method goes through
`Dispatch`.

Add a lighter `ScriptedCommand` ABC template (`scripted_command.py`) for
raw mode; the existing `ParsedCommand`/`LLDBOptionValueParser` classes
are kept as-is.

Extend `ScriptedPythonInterface` with a few `Transform` overloads
(`DebuggerSP`, `std::vector<std::string>`, `CommandReturnObject &`) so
those argument types route through `Dispatch` without extra plumbing.

Delete the standalone SWIG bridge functions this plugin was the sole
caller of. `CommandObjectScriptingObjectRaw` and
`CommandObjectScriptingObjectParsed` now hold a
`ScriptedCommandInterfaceSP` instead of a raw
`StructuredData::GenericSP`; `command script add`'s `DoExecute`
constructs the interface once via
`CreateScriptedCommandInterface()->CreatePluginObject` for either flag.

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
(cherry picked from commit c35bdc99a36bd518ff59b6cb209e84a5c7df24e1)
---
 lldb/bindings/python/CMakeLists.txt           |   1 +
 lldb/bindings/python/python-wrapper.swig      | 184 +------
 lldb/docs/CMakeLists.txt                      |   1 +
 .../python/templates/scripted_command.py      |  86 +++
 lldb/include/lldb/API/SBCommandReturnObject.h |   2 +
 lldb/include/lldb/API/SBDebugger.h            |   1 +
 .../Interfaces/ScriptedCommandInterface.h     |  78 +++
 .../lldb/Interpreter/ScriptInterpreter.h      |  88 +--
 lldb/include/lldb/lldb-enumerations.h         |   4 +-
 lldb/include/lldb/lldb-forward.h              |   3 +
 lldb/source/API/SBCommandReturnObject.cpp     |  50 +-
 lldb/source/API/SBCommandReturnObjectImpl.h   |  31 ++
 .../source/Commands/CommandObjectCommands.cpp | 202 +++----
 lldb/source/Interpreter/ScriptInterpreter.cpp |  19 +
 .../ScriptInterpreter/Python/CMakeLists.txt   |   1 +
 .../ScriptInterpreterPythonInterfaces.cpp     |   2 +
 .../ScriptInterpreterPythonInterfaces.h       |   1 +
 .../ScriptedCommandPythonInterface.cpp        | 248 +++++++++
 .../ScriptedCommandPythonInterface.h          |  81 +++
 .../Interfaces/ScriptedPythonInterface.cpp    |  52 ++
 .../Interfaces/ScriptedPythonInterface.h      |  36 ++
 .../Python/SWIGPythonBridge.h                 |  33 +-
 .../Python/ScriptInterpreterPython.cpp        | 506 +-----------------
 .../Python/ScriptInterpreterPythonImpl.h      |  52 +-
 .../Python/PythonTestSuite.cpp                |  51 +-
 25 files changed, 818 insertions(+), 995 deletions(-)
 create mode 100644 lldb/examples/python/templates/scripted_command.py
 create mode 100644 lldb/include/lldb/Interpreter/Interfaces/ScriptedCommandInterface.h
 create mode 100644 lldb/source/API/SBCommandReturnObjectImpl.h
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedCommandPythonInterface.cpp
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedCommandPythonInterface.h

diff --git a/lldb/bindings/python/CMakeLists.txt b/lldb/bindings/python/CMakeLists.txt
index d29b143c1408c..6ffdf9ccafabc 100644
--- a/lldb/bindings/python/CMakeLists.txt
+++ b/lldb/bindings/python/CMakeLists.txt
@@ -120,6 +120,7 @@ function(finish_swig_python swig_target lldb_python_bindings_dir lldb_python_tar
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_breakpoint.py"
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_hook.py"
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_stackframe_recognizer.py"
+    "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_command.py"
     )
 
   if(APPLE)
diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig
index 2392737402e20..c4071b3f7b5b1 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -213,25 +213,6 @@ PythonObject lldb_private::python::SWIGBridge::LLDBSwigPythonCreateSyntheticProv
   return PythonObject();
 }
 
-PythonObject lldb_private::python::SWIGBridge::LLDBSwigPythonCreateCommandObject(
-    const char *python_class_name, const char *session_dictionary_name,
-    lldb::DebuggerSP debugger_sp) {
-  if (python_class_name == NULL || python_class_name[0] == '\0' ||
-      !session_dictionary_name)
-    return PythonObject();
-
-  PyErr_Cleaner py_err_cleaner(true);
-  auto dict = PythonModule::MainModule().ResolveName<PythonDictionary>(
-      session_dictionary_name);
-  auto pfunc = PythonObject::ResolveNameWithDictionary<PythonCallable>(
-      python_class_name, dict);
-
-  if (!pfunc.IsAllocated())
-    return PythonObject();
-
-  return pfunc(SWIGBridge::ToSWIGWrapper(std::move(debugger_sp)), dict);
-}
-
 // wrapper that calls an optional instance member of an object taking no
 // arguments
 static PyObject *LLDBSwigPython_CallOptionalMember(
@@ -497,6 +478,32 @@ void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBError(PyObject * data
   return sb_ptr;
 }
 
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBCommandReturnObject(
+    PyObject *data) {
+  lldb::SBCommandReturnObject *sb_ptr = nullptr;
+
+  int valid_cast = SWIG_ConvertPtr(
+      data, (void **)&sb_ptr, SWIGTYPE_p_lldb__SBCommandReturnObject, 0);
+
+  if (valid_cast == -1)
+    return NULL;
+
+  return sb_ptr;
+}
+
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBDebugger(
+    PyObject *data) {
+  lldb::SBDebugger *sb_ptr = nullptr;
+
+  int valid_cast =
+      SWIG_ConvertPtr(data, (void **)&sb_ptr, SWIGTYPE_p_lldb__SBDebugger, 0);
+
+  if (valid_cast == -1)
+    return NULL;
+
+  return sb_ptr;
+}
+
 void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBEvent(PyObject * data) {
   lldb::SBEvent *sb_ptr = nullptr;
 
@@ -639,145 +646,6 @@ bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallCommand(
   return true;
 }
 
-bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallCommandObject(
-    PyObject *implementor, lldb::DebuggerSP debugger, const char *args,
-    lldb_private::CommandReturnObject &cmd_retobj,
-    lldb::ExecutionContextRefSP exe_ctx_ref_sp) {
-
-  PyErr_Cleaner py_err_cleaner(true);
-
-  PythonObject self(PyRefType::Borrowed, implementor);
-  auto pfunc = self.ResolveName<PythonCallable>("__call__");
-
-  if (!pfunc.IsAllocated())
-    return false;
-
-  auto cmd_retobj_arg = SWIGBridge::ToSWIGWrapper(cmd_retobj);
-
-  pfunc(SWIGBridge::ToSWIGWrapper(std::move(debugger)), PythonString(args),
-        SWIGBridge::ToSWIGWrapper(exe_ctx_ref_sp), cmd_retobj_arg.obj());
-
-  return true;
-}
-
-std::optional<std::string>
-lldb_private::python::SWIGBridge::LLDBSwigPythonGetRepeatCommandForScriptedCommand(PyObject *implementor,
-                                               std::string &command) {
-  PyErr_Cleaner py_err_cleaner(true);
-
-  PythonObject self(PyRefType::Borrowed, implementor);
-  auto pfunc = self.ResolveName<PythonCallable>("get_repeat_command");
-  // If not implemented, repeat the exact command.
-  if (!pfunc.IsAllocated())
-    return std::nullopt;
-
-  PythonString command_str(command);
-  PythonObject result = pfunc(command_str);
-
-  // A return of None is the equivalent of nullopt - means repeat
-  // the command as is:
-  if (result.IsNone())
-    return std::nullopt;
-
-  return result.Str().GetString().str();
-}
-
-StructuredData::DictionarySP
-lldb_private::python::SWIGBridge::LLDBSwigPythonHandleArgumentCompletionForScriptedCommand(PyObject *implementor,
-    std::vector<llvm::StringRef> &args_vec, size_t args_pos, size_t pos_in_arg) {
-
-  PyErr_Cleaner py_err_cleaner(true);
-
-  PythonObject self(PyRefType::Borrowed, implementor);
-  auto pfunc = self.ResolveName<PythonCallable>("handle_argument_completion");
-  // If this isn't implemented, return an empty dict to signal falling back to default completion:
-  if (!pfunc.IsAllocated())
-    return {};
-
-  PythonList args_list(PyInitialValue::Empty);
-  for (auto elem : args_vec)
-    args_list.AppendItem(PythonString(elem));
-
-  PythonObject result = pfunc(args_list, PythonInteger(args_pos), PythonInteger(pos_in_arg));
-  // Returning None means do the ordinary completion
-  if (result.IsNone())
-    return {};
-
-  // Convert the return dictionary to a DictionarySP.
-  StructuredData::ObjectSP result_obj_sp = result.CreateStructuredObject();
-  if (!result_obj_sp)
-    return {};
-
-  StructuredData::DictionarySP dict_sp(new StructuredData::Dictionary(result_obj_sp));
-  if (dict_sp->GetType() == lldb::eStructuredDataTypeInvalid)
-    return {};
-  return dict_sp;
-}
-
-StructuredData::DictionarySP
-lldb_private::python::SWIGBridge::LLDBSwigPythonHandleOptionArgumentCompletionForScriptedCommand(PyObject *implementor,
-    llvm::StringRef &long_option, size_t pos_in_arg) {
-
-  PyErr_Cleaner py_err_cleaner(true);
-
-  PythonObject self(PyRefType::Borrowed, implementor);
-  auto pfunc = self.ResolveName<PythonCallable>("handle_option_argument_completion");
-  // If this isn't implemented, return an empty dict to signal falling back to default completion:
-  if (!pfunc.IsAllocated())
-    return {};
-
-  PythonObject result = pfunc(PythonString(long_option), PythonInteger(pos_in_arg));
-  // Returning None means do the ordinary completion
-  if (result.IsNone())
-    return {};
-
-  // Returning a boolean:
-  // True means the completion was handled, but there were no completions
-  // False means that the completion was not handled, again, do the ordinary completion:
-  if (result.GetObjectType() == PyObjectType::Boolean) {
-    if (!result.IsTrue())
-      return {};
-    // Make up a completion dictionary with the right element:
-    StructuredData::DictionarySP dict_sp(new StructuredData::Dictionary());
-    dict_sp->AddBooleanItem("no-completion", true);
-    return dict_sp;
-  }
-
-
-  // Convert the return dictionary to a DictionarySP.
-  StructuredData::ObjectSP result_obj_sp = result.CreateStructuredObject();
-  if (!result_obj_sp)
-    return {};
-
-  StructuredData::DictionarySP dict_sp(new StructuredData::Dictionary(result_obj_sp));
-  if (dict_sp->GetType() == lldb::eStructuredDataTypeInvalid)
-    return {};
-  return dict_sp;
-}
-
-#include "lldb/Interpreter/CommandReturnObject.h"
-
-bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallParsedCommandObject(
-    PyObject *implementor, lldb::DebuggerSP debugger, lldb_private::StructuredDataImpl &args_impl,
-    lldb_private::CommandReturnObject &cmd_retobj,
-    lldb::ExecutionContextRefSP exe_ctx_ref_sp) {
-
-  PyErr_Cleaner py_err_cleaner(true);
-
-  PythonObject self(PyRefType::Borrowed, implementor);
-  auto pfunc = self.ResolveName<PythonCallable>("__call__");
-
-  if (!pfunc.IsAllocated()) {
-    cmd_retobj.AppendError("Could not find '__call__' method in implementation class");
-    return false;
-  }
-
-  pfunc(SWIGBridge::ToSWIGWrapper(std::move(debugger)), SWIGBridge::ToSWIGWrapper(args_impl),
-        SWIGBridge::ToSWIGWrapper(exe_ctx_ref_sp), SWIGBridge::ToSWIGWrapper(cmd_retobj).obj());
-
-  return true;
-}
-
 PythonObject lldb_private::python::SWIGBridge::LLDBSWIGPythonCreateOSPlugin(
     const char *python_class_name, const char *session_dictionary_name,
     const lldb::ProcessSP &process_sp) {
diff --git a/lldb/docs/CMakeLists.txt b/lldb/docs/CMakeLists.txt
index dd091836dc1aa..53647990f7842 100644
--- a/lldb/docs/CMakeLists.txt
+++ b/lldb/docs/CMakeLists.txt
@@ -33,6 +33,7 @@ if (LLDB_ENABLE_PYTHON AND SPHINX_FOUND)
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_breakpoint.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_hook.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_stackframe_recognizer.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
+      COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_command.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMENT "Copying lldb.py to pretend its a Python package.")
 
     add_dependencies(lldb-python-doc-package swig_wrapper_python)
diff --git a/lldb/examples/python/templates/scripted_command.py b/lldb/examples/python/templates/scripted_command.py
new file mode 100644
index 0000000000000..d9b18ea4376aa
--- /dev/null
+++ b/lldb/examples/python/templates/scripted_command.py
@@ -0,0 +1,86 @@
+from abc import ABCMeta, abstractmethod
+from typing import Optional
+
+import lldb
+
+
+class ScriptedCommand(metaclass=ABCMeta):
+    """
+    The base class for a scripted (raw) command.
+
+    A raw command receives the unparsed argument string exactly as the user
+    typed it, and is responsible for any parsing it needs. For a command
+    with a table-driven option/argument parser, see `ParsedCommand` instead.
+    Register it with `command script add -c <ClassName> ...`.
+
+    Most of the base class methods are `@abstractmethod` that need to be
+    overwritten by the inheriting class.
+    """
+
+    def __init__(self, debugger: lldb.SBDebugger):
+        """Construct a scripted command.
+
+        Args:
+            debugger (lldb.SBDebugger): The debugger this command is being
+                added to.
+        """
+        pass
+
+    @abstractmethod
+    def __call__(
+        self,
+        debugger: lldb.SBDebugger,
+        args: str,
+        exe_ctx: lldb.SBExecutionContext,
+        result: lldb.SBCommandReturnObject,
+    ) -> None:
+        """Execute the command.
+
+        Args:
+            debugger (lldb.SBDebugger): The debugger the command runs
+                against.
+            args (str): The raw, unparsed argument string.
+            exe_ctx (lldb.SBExecutionContext): The execution context.
+            result (lldb.SBCommandReturnObject): Write command output/errors
+                here.
+        """
+        pass
+
+    def get_short_help(self) -> Optional[str]:
+        """A one-line description shown by `help`.
+
+        Returns:
+            str: The short help string.
+        """
+        pass
+
+    def get_long_help(self) -> Optional[str]:
+        """The full help text shown by `help <command>`.
+
+        Returns:
+            str: The long help string.
+        """
+        pass
+
+    def get_flags(self) -> int:
+        """Command flags (a bitmask of `lldb.eCommandRequires*`/
+        `lldb.eCommandProcessMustBe*` etc.) controlling when this command is
+        available.
+
+        Returns:
+            int: The flags bitmask. Defaults to 0 (no restrictions).
+        """
+        return 0
+
+    def get_repeat_command(self, command: str) -> Optional[str]:
+        """Customize what runs when the user presses Enter to repeat this
+        command.
+
+        Args:
+            command (str): The command line that was run.
+
+        Returns:
+            str: The command line to run on repeat. Defaults to `None`,
+            meaning repeat the original command unmodified.
+        """
+        pass
diff --git a/lldb/include/lldb/API/SBCommandReturnObject.h b/lldb/include/lldb/API/SBCommandReturnObject.h
index 6386bd250afa5..b80a11b52c77f 100644
--- a/lldb/include/lldb/API/SBCommandReturnObject.h
+++ b/lldb/include/lldb/API/SBCommandReturnObject.h
@@ -17,6 +17,7 @@
 
 namespace lldb_private {
 class CommandPluginInterfaceImplementation;
+class CommandReturnObject;
 class SBCommandReturnObjectImpl;
 namespace python {
 class SWIGBridge;
@@ -144,6 +145,7 @@ class LLDB_API SBCommandReturnObject {
 
   friend class lldb_private::CommandPluginInterfaceImplementation;
   friend class lldb_private::python::SWIGBridge;
+  friend class lldb_private::ScriptInterpreter;
 
   SBCommandReturnObject(lldb_private::CommandReturnObject &ref);
 
diff --git a/lldb/include/lldb/API/SBDebugger.h b/lldb/include/lldb/API/SBDebugger.h
index 14b8350902811..3e302f121f5ec 100644
--- a/lldb/include/lldb/API/SBDebugger.h
+++ b/lldb/include/lldb/API/SBDebugger.h
@@ -678,6 +678,7 @@ class LLDB_API SBDebugger {
 protected:
   friend class lldb_private::CommandPluginInterfaceImplementation;
   friend class lldb_private::python::SWIGBridge;
+  friend class lldb_private::ScriptInterpreter;
   friend class lldb_private::SystemInitializerFull;
 
   SBDebugger(const lldb::DebuggerSP &debugger_sp);
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedCommandInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedCommandInterface.h
new file mode 100644
index 0000000000000..29f4d273e49f0
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedCommandInterface.h
@@ -0,0 +1,78 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_INTERFACES_SCRIPTEDCOMMANDINTERFACE_H
+#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDCOMMANDINTERFACE_H
+
+#include "ScriptedInterface.h"
+#include "lldb/lldb-private.h"
+
+namespace lldb_private {
+class ScriptedCommandInterface : virtual public ScriptedInterface {
+public:
+  virtual llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name,
+                     lldb::DebuggerSP debugger_sp) = 0;
+
+  virtual bool RunRawCommand(llvm::StringRef args,
+                             ScriptedCommandSynchronicity synchronicity,
+                             CommandReturnObject &cmd_retobj, Status &error,
+                             const ExecutionContext &exe_ctx) {
+    return false;
+  }
+
+  virtual bool RunParsedCommand(Args &args,
+                                ScriptedCommandSynchronicity synchronicity,
+                                CommandReturnObject &cmd_retobj, Status &error,
+                                const ExecutionContext &exe_ctx) {
+    return false;
+  }
+
+  virtual std::optional<std::string> GetRepeatCommand(Args &args) {
+    return std::nullopt;
+  }
+
+  virtual StructuredData::DictionarySP
+  HandleArgumentCompletion(std::vector<std::string> &args, size_t args_pos,
+                           size_t char_in_arg) {
+    return {};
+  }
+
+  virtual StructuredData::DictionarySP
+  HandleOptionArgumentCompletion(llvm::StringRef &long_option,
+                                 size_t char_in_arg) {
+    return {};
+  }
+
+  virtual bool GetShortHelp(std::string &dest) {
+    dest.clear();
+    return false;
+  }
+
+  virtual bool GetLongHelp(std::string &dest) {
+    dest.clear();
+    return false;
+  }
+
+  virtual uint32_t GetFlags() { return 0; }
+
+  virtual StructuredData::ObjectSP GetOptionsDefinition() { return {}; }
+
+  virtual StructuredData::ObjectSP GetArgumentsDefinition() { return {}; }
+
+  virtual void OptionParsingStarted() {}
+
+  virtual bool SetOptionValue(ExecutionContext *exe_ctx,
+                              llvm::StringRef long_option,
+                              llvm::StringRef value) {
+    return false;
+  }
+};
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDCOMMANDINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 0e65cb4b8ac4a..9d3e7e6f32ff5 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -263,11 +263,6 @@ class ScriptInterpreter : public PluginInterface {
     return StructuredData::ObjectSP();
   }
 
-  virtual StructuredData::GenericSP
-  CreateScriptCommandObject(const char *class_name) {
-    return StructuredData::GenericSP();
-  }
-
   virtual StructuredData::ObjectSP
   LoadPluginModule(const FileSpec &file_spec, lldb_private::Status &error) {
     return StructuredData::ObjectSP();
@@ -394,42 +389,6 @@ class ScriptInterpreter : public PluginInterface {
     return false;
   }
 
-  virtual bool RunScriptBasedCommand(
-      StructuredData::GenericSP impl_obj_sp, llvm::StringRef args,
-      ScriptedCommandSynchronicity synchronicity,
-      lldb_private::CommandReturnObject &cmd_retobj, Status &error,
-      const lldb_private::ExecutionContext &exe_ctx) {
-    return false;
-  }
-
-  virtual bool RunScriptBasedParsedCommand(
-      StructuredData::GenericSP impl_obj_sp, Args& args,
-      ScriptedCommandSynchronicity synchronicity,
-      lldb_private::CommandReturnObject &cmd_retobj, Status &error,
-      const lldb_private::ExecutionContext &exe_ctx) {
-    return false;
-  }
-
-  virtual std::optional<std::string>
-  GetRepeatCommandForScriptedCommand(StructuredData::GenericSP impl_obj_sp,
-                                     Args &args) {
-    return std::nullopt;
-  }
-
-  virtual StructuredData::DictionarySP
-  HandleArgumentCompletionForScriptedCommand(
-      StructuredData::GenericSP impl_obj_sp, std::vector<llvm::StringRef> &args,
-      size_t args_pos, size_t char_in_arg) {
-    return {};
-  }
-
-  virtual StructuredData::DictionarySP
-  HandleOptionArgumentCompletionForScriptedCommand(
-      StructuredData::GenericSP impl_obj_sp, llvm::StringRef &long_name,
-      size_t char_in_arg) {
-    return {};
-  }
-
   virtual bool RunScriptFormatKeyword(const char *impl_function,
                                       Process *process, std::string &output,
                                       Status &error) {
@@ -468,43 +427,6 @@ class ScriptInterpreter : public PluginInterface {
     return false;
   }
 
-  virtual bool
-  GetShortHelpForCommandObject(StructuredData::GenericSP cmd_obj_sp,
-                               std::string &dest) {
-    dest.clear();
-    return false;
-  }
-
-  virtual StructuredData::ObjectSP
-  GetOptionsForCommandObject(StructuredData::GenericSP cmd_obj_sp) {
-    return {};
-  }
-
-  virtual StructuredData::ObjectSP
-  GetArgumentsForCommandObject(StructuredData::GenericSP cmd_obj_sp) {
-    return {};
-  }
-
-  virtual bool SetOptionValueForCommandObject(
-      StructuredData::GenericSP cmd_obj_sp, ExecutionContext *exe_ctx,
-      llvm::StringRef long_option, llvm::StringRef value) {
-    return false;
-  }
-
-  virtual void
-  OptionParsingStartedForCommandObject(StructuredData::GenericSP cmd_obj_sp) {}
-
-  virtual uint32_t
-  GetFlagsForCommandObject(StructuredData::GenericSP cmd_obj_sp) {
-    return 0;
-  }
-
-  virtual bool GetLongHelpForCommandObject(StructuredData::GenericSP cmd_obj_sp,
-                                           std::string &dest) {
-    dest.clear();
-    return false;
-  }
-
   virtual bool CheckObjectExists(const char *name) { return false; }
 
   virtual bool
@@ -586,6 +508,10 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  virtual lldb::ScriptedCommandInterfaceSP CreateScriptedCommandInterface() {
+    return {};
+  }
+
   virtual StructuredData::ObjectSP
   CreateStructuredDataFromScriptObject(ScriptObject obj) {
     return {};
@@ -658,6 +584,12 @@ class ScriptInterpreter : public PluginInterface {
   lldb::BreakpointLocationSP GetOpaqueTypeFromSBBreakpointLocation(
       const lldb::SBBreakpointLocation &break_loc) const;
 
+  CommandReturnObject *GetOpaqueTypeFromSBCommandReturnObject(
+      const lldb::SBCommandReturnObject &cmd_retobj) const;
+
+  lldb::DebuggerSP
+  GetOpaqueTypeFromSBDebugger(const lldb::SBDebugger &debugger) const;
+
   lldb::ProcessAttachInfoSP
   GetOpaqueTypeFromSBAttachInfo(const lldb::SBAttachInfo &attach_info) const;
 
diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h
index 93c252b55de99..f73d0085f2320 100644
--- a/lldb/include/lldb/lldb-enumerations.h
+++ b/lldb/include/lldb/lldb-enumerations.h
@@ -268,7 +268,9 @@ enum ScriptedExtension {
   eScriptedExtensionScriptedThread,
   eScriptedExtensionScriptedFrame,
   eScriptedExtensionScriptedStackFrameRecognizer,
-  kLastScriptedExtension = eScriptedExtensionScriptedStackFrameRecognizer
+  eScriptedExtensionScriptedCommand,
+  eScriptedExtensionParsedCommand,
+  kLastScriptedExtension = eScriptedExtensionParsedCommand
 };
 
 /// Register numbering types.
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index 157aa5743f016..2572aa0dc344b 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -193,6 +193,7 @@ class ScriptedFrameInterface;
 class ScriptedFrameProviderInterface;
 class ScriptedMetadata;
 class ScriptedBreakpointInterface;
+class ScriptedCommandInterface;
 class ScriptedHookInterface;
 class ScriptedPlatformInterface;
 class ScriptedProcessInterface;
@@ -438,6 +439,8 @@ typedef std::shared_ptr<lldb_private::ScriptedBreakpointInterface>
     ScriptedBreakpointInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedStackFrameRecognizerInterface>
     ScriptedStackFrameRecognizerInterfaceSP;
+typedef std::shared_ptr<lldb_private::ScriptedCommandInterface>
+    ScriptedCommandInterfaceSP;
 typedef std::shared_ptr<lldb_private::Section> SectionSP;
 typedef std::unique_ptr<lldb_private::SectionList> SectionListUP;
 typedef std::weak_ptr<lldb_private::Section> SectionWP;
diff --git a/lldb/source/API/SBCommandReturnObject.cpp b/lldb/source/API/SBCommandReturnObject.cpp
index 62cc7a1f05573..b63f91e9712f4 100644
--- a/lldb/source/API/SBCommandReturnObject.cpp
+++ b/lldb/source/API/SBCommandReturnObject.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "lldb/API/SBCommandReturnObject.h"
+#include "SBCommandReturnObjectImpl.h"
 #include "Utils.h"
 #include "lldb/API/SBError.h"
 #include "lldb/API/SBFile.h"
@@ -25,30 +26,27 @@
 using namespace lldb;
 using namespace lldb_private;
 
-class lldb_private::SBCommandReturnObjectImpl {
-public:
-  SBCommandReturnObjectImpl() : m_ptr(new CommandReturnObject(false)) {}
-  SBCommandReturnObjectImpl(CommandReturnObject &ref)
-      : m_ptr(&ref), m_owned(false) {}
-  SBCommandReturnObjectImpl(const SBCommandReturnObjectImpl &rhs)
-      : m_ptr(new CommandReturnObject(*rhs.m_ptr)), m_owned(rhs.m_owned) {}
-  SBCommandReturnObjectImpl &operator=(const SBCommandReturnObjectImpl &rhs) {
-    SBCommandReturnObjectImpl copy(rhs);
-    std::swap(*this, copy);
-    return *this;
-  }
-  // rvalue ctor+assignment are not used by SBCommandReturnObject.
-  ~SBCommandReturnObjectImpl() {
-    if (m_owned)
-      delete m_ptr;
-  }
+SBCommandReturnObjectImpl::SBCommandReturnObjectImpl()
+    : m_ptr(new CommandReturnObject(false)) {}
 
-  CommandReturnObject &operator*() const { return *m_ptr; }
+SBCommandReturnObjectImpl::SBCommandReturnObjectImpl(CommandReturnObject &ref)
+    : m_ptr(&ref), m_owned(false) {}
 
-private:
-  CommandReturnObject *m_ptr;
-  bool m_owned = true;
-};
+SBCommandReturnObjectImpl::SBCommandReturnObjectImpl(
+    const SBCommandReturnObjectImpl &rhs)
+    : m_ptr(new CommandReturnObject(*rhs.m_ptr)), m_owned(rhs.m_owned) {}
+
+SBCommandReturnObjectImpl &
+SBCommandReturnObjectImpl::operator=(const SBCommandReturnObjectImpl &rhs) {
+  SBCommandReturnObjectImpl copy(rhs);
+  std::swap(*this, copy);
+  return *this;
+}
+
+SBCommandReturnObjectImpl::~SBCommandReturnObjectImpl() {
+  if (m_owned)
+    delete m_ptr;
+}
 
 SBCommandReturnObject::SBCommandReturnObject()
     : m_opaque_up(new SBCommandReturnObjectImpl()) {
@@ -221,19 +219,19 @@ void SBCommandReturnObject::AppendWarning(const char *message) {
 }
 
 CommandReturnObject *SBCommandReturnObject::operator->() const {
-  return &**m_opaque_up;
+  return m_opaque_up->get();
 }
 
 CommandReturnObject *SBCommandReturnObject::get() const {
-  return &**m_opaque_up;
+  return m_opaque_up->get();
 }
 
 CommandReturnObject &SBCommandReturnObject::operator*() const {
-  return **m_opaque_up;
+  return *m_opaque_up->get();
 }
 
 CommandReturnObject &SBCommandReturnObject::ref() const {
-  return **m_opaque_up;
+  return *m_opaque_up->get();
 }
 
 bool SBCommandReturnObject::GetDescription(SBStream &description) {
diff --git a/lldb/source/API/SBCommandReturnObjectImpl.h b/lldb/source/API/SBCommandReturnObjectImpl.h
new file mode 100644
index 0000000000000..e0a90bd1e12bd
--- /dev/null
+++ b/lldb/source/API/SBCommandReturnObjectImpl.h
@@ -0,0 +1,31 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_SOURCE_API_SBCOMMANDRETURNOBJECTIMPL_H
+#define LLDB_SOURCE_API_SBCOMMANDRETURNOBJECTIMPL_H
+
+namespace lldb_private {
+class CommandReturnObject;
+
+class SBCommandReturnObjectImpl {
+public:
+  SBCommandReturnObjectImpl();
+  SBCommandReturnObjectImpl(CommandReturnObject &ref);
+  SBCommandReturnObjectImpl(const SBCommandReturnObjectImpl &rhs);
+  SBCommandReturnObjectImpl &operator=(const SBCommandReturnObjectImpl &rhs);
+  ~SBCommandReturnObjectImpl();
+
+  CommandReturnObject *get() const { return m_ptr; }
+
+private:
+  CommandReturnObject *m_ptr;
+  bool m_owned = true;
+};
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_API_SBCOMMANDRETURNOBJECTIMPL_H
diff --git a/lldb/source/Commands/CommandObjectCommands.cpp b/lldb/source/Commands/CommandObjectCommands.cpp
index bb25b8ed70234..f069b7e27df9a 100644
--- a/lldb/source/Commands/CommandObjectCommands.cpp
+++ b/lldb/source/Commands/CommandObjectCommands.cpp
@@ -16,6 +16,7 @@
 #include "lldb/Interpreter/CommandInterpreter.h"
 #include "lldb/Interpreter/CommandOptionArgumentTable.h"
 #include "lldb/Interpreter/CommandReturnObject.h"
+#include "lldb/Interpreter/Interfaces/ScriptedCommandInterface.h"
 #include "lldb/Interpreter/OptionArgParser.h"
 #include "lldb/Interpreter/OptionValueBoolean.h"
 #include "lldb/Interpreter/OptionValueString.h"
@@ -1119,19 +1120,19 @@ class CommandObjectPythonFunction : public CommandObjectRaw {
 /// substitution).
 class CommandObjectScriptingObjectRaw : public CommandObjectRaw {
 public:
-  CommandObjectScriptingObjectRaw(CommandInterpreter &interpreter,
-                                  std::string name,
-                                  StructuredData::GenericSP cmd_obj_sp,
-                                  ScriptedCommandSynchronicity synch,
-                                  CompletionType completion_type)
-      : CommandObjectRaw(interpreter, name), m_cmd_obj_sp(cmd_obj_sp),
-        m_synchro(synch), m_fetched_help_short(false),
-        m_fetched_help_long(false), m_completion_type(completion_type) {
+  CommandObjectScriptingObjectRaw(
+      CommandInterpreter &interpreter, std::string name,
+      lldb::ScriptedCommandInterfaceSP cmd_interface_sp,
+      ScriptedCommandSynchronicity synch, CompletionType completion_type)
+      : CommandObjectRaw(interpreter, name),
+        m_cmd_interface_sp(cmd_interface_sp), m_synchro(synch),
+        m_fetched_help_short(false), m_fetched_help_long(false),
+        m_completion_type(completion_type) {
     StreamString stream;
     stream.Printf("For more information run 'help %s'", name.c_str());
     SetHelp(stream.GetString());
-    if (ScriptInterpreter *scripter = GetDebugger().GetScriptInterpreter())
-      GetFlags().Set(scripter->GetFlagsForCommandObject(cmd_obj_sp));
+    if (m_cmd_interface_sp)
+      GetFlags().Set(m_cmd_interface_sp->GetFlags());
   }
 
   ~CommandObjectScriptingObjectRaw() override = default;
@@ -1151,22 +1152,19 @@ class CommandObjectScriptingObjectRaw : public CommandObjectRaw {
 
   std::optional<std::string> GetRepeatCommand(Args &args,
                                               uint32_t index) override {
-    ScriptInterpreter *scripter = GetDebugger().GetScriptInterpreter();
-    if (!scripter)
+    if (!m_cmd_interface_sp)
       return std::nullopt;
 
-    return scripter->GetRepeatCommandForScriptedCommand(m_cmd_obj_sp, args);
+    return m_cmd_interface_sp->GetRepeatCommand(args);
   }
 
   llvm::StringRef GetHelp() override {
     if (m_fetched_help_short)
       return CommandObjectRaw::GetHelp();
-    ScriptInterpreter *scripter = GetDebugger().GetScriptInterpreter();
-    if (!scripter)
+    if (!m_cmd_interface_sp)
       return CommandObjectRaw::GetHelp();
     std::string docstring;
-    m_fetched_help_short =
-        scripter->GetShortHelpForCommandObject(m_cmd_obj_sp, docstring);
+    m_fetched_help_short = m_cmd_interface_sp->GetShortHelp(docstring);
     if (!docstring.empty())
       SetHelp(docstring);
 
@@ -1177,13 +1175,11 @@ class CommandObjectScriptingObjectRaw : public CommandObjectRaw {
     if (m_fetched_help_long)
       return CommandObjectRaw::GetHelpLong();
 
-    ScriptInterpreter *scripter = GetDebugger().GetScriptInterpreter();
-    if (!scripter)
+    if (!m_cmd_interface_sp)
       return CommandObjectRaw::GetHelpLong();
 
     std::string docstring;
-    m_fetched_help_long =
-        scripter->GetLongHelpForCommandObject(m_cmd_obj_sp, docstring);
+    m_fetched_help_long = m_cmd_interface_sp->GetLongHelp(docstring);
     if (!docstring.empty())
       SetHelpLong(docstring);
     return CommandObjectRaw::GetHelpLong();
@@ -1192,15 +1188,13 @@ class CommandObjectScriptingObjectRaw : public CommandObjectRaw {
 protected:
   void DoExecute(llvm::StringRef raw_command_line,
                  CommandReturnObject &result) override {
-    ScriptInterpreter *scripter = GetDebugger().GetScriptInterpreter();
-
     Status error;
 
     result.SetStatus(eReturnStatusInvalid);
 
-    if (!scripter ||
-        !scripter->RunScriptBasedCommand(m_cmd_obj_sp, raw_command_line,
-                                         m_synchro, result, error, m_exe_ctx)) {
+    if (!m_cmd_interface_sp ||
+        !m_cmd_interface_sp->RunRawCommand(raw_command_line, m_synchro, result,
+                                           error, m_exe_ctx)) {
       result.AppendError(error.AsCString());
     } else {
       // Don't change the status if the command already set it...
@@ -1214,7 +1208,7 @@ class CommandObjectScriptingObjectRaw : public CommandObjectRaw {
   }
 
 private:
-  StructuredData::GenericSP m_cmd_obj_sp;
+  lldb::ScriptedCommandInterfaceSP m_cmd_interface_sp;
   ScriptedCommandSynchronicity m_synchro;
   bool m_fetched_help_short : 1;
   bool m_fetched_help_long : 1;
@@ -1238,23 +1232,15 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
 private: 
   class CommandOptions : public Options {
   public:
-    CommandOptions(CommandInterpreter &interpreter, 
-        StructuredData::GenericSP cmd_obj_sp) : m_interpreter(interpreter), 
-            m_cmd_obj_sp(cmd_obj_sp) {}
+    CommandOptions(lldb::ScriptedCommandInterfaceSP cmd_interface_sp)
+        : m_cmd_interface_sp(cmd_interface_sp) {}
 
     ~CommandOptions() override = default;
 
     Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
                           ExecutionContext *execution_context) override {
       Status error;
-      ScriptInterpreter *scripter = 
-        m_interpreter.GetDebugger().GetScriptInterpreter();
-      if (!scripter) {
-        return Status::FromErrorString(
-            "No script interpreter for SetOptionValue.");
-        return error;
-      }
-      if (!m_cmd_obj_sp) {
+      if (!m_cmd_interface_sp) {
         return Status::FromErrorString(
             "SetOptionValue called with empty cmd_obj.");
         return error;
@@ -1268,10 +1254,10 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
       // Pass the long option, since you aren't actually required to have a
       // short_option, and for those options the index or short option character
       // aren't meaningful on the python side.
-      const char * long_option = 
-        m_options_definition_up.get()[option_idx].long_option;
-      bool success = scripter->SetOptionValueForCommandObject(m_cmd_obj_sp, 
-        execution_context, long_option, option_arg);
+      const char *long_option =
+          m_options_definition_up.get()[option_idx].long_option;
+      bool success = m_cmd_interface_sp->SetOptionValue(
+          execution_context, long_option, option_arg);
       if (!success)
         return Status::FromErrorStringWithFormatv(
             "Error setting option: {0} to {1}", long_option, option_arg);
@@ -1279,12 +1265,10 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
     }
 
     void OptionParsingStarting(ExecutionContext *execution_context) override {
-      ScriptInterpreter *scripter = 
-        m_interpreter.GetDebugger().GetScriptInterpreter();
-      if (!scripter || !m_cmd_obj_sp)
+      if (!m_cmd_interface_sp)
         return;
 
-      scripter->OptionParsingStartedForCommandObject(m_cmd_obj_sp);
+      m_cmd_interface_sp->OptionParsingStarted();
     }
 
     llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
@@ -1292,7 +1276,7 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
         return {};
       return llvm::ArrayRef(m_options_definition_up.get(), m_num_options);
     }
-    
+
     static Status ParseUsageMaskFromArray(StructuredData::ObjectSP obj_sp, 
         size_t counter, uint32_t &usage_mask) {
       // If the usage entry is not provided, we use LLDB_OPT_SET_ALL.
@@ -1726,10 +1710,7 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
                                    OptionElementVector &option_vec,
                                    int opt_element_index,
                                    CommandInterpreter &interpreter) override {
-      ScriptInterpreter *scripter =
-          interpreter.GetDebugger().GetScriptInterpreter();
-
-      if (!scripter)
+      if (!m_cmd_interface_sp)
         return;
 
       ExecutionContext exe_ctx = interpreter.GetExecutionContext();
@@ -1746,9 +1727,8 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
       // regular option completer handle that:
       StructuredData::DictionarySP completion_dict_sp;
       if (!is_enum)
-        completion_dict_sp =
-            scripter->HandleOptionArgumentCompletionForScriptedCommand(
-                m_cmd_obj_sp, option_name, request.GetCursorCharPos());
+        completion_dict_sp = m_cmd_interface_sp->HandleOptionArgumentCompletion(
+            option_name, request.GetCursorCharPos());
 
       if (!completion_dict_sp) {
         Options::HandleOptionArgumentCompletion(request, option_vec,
@@ -1809,19 +1789,17 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
     std::vector<std::vector<EnumValueStorage>> m_enum_storage;
     std::vector<std::vector<OptionEnumValueElement>> m_enum_vector;
     std::vector<std::string> m_usage_container;
-    CommandInterpreter &m_interpreter;
-    StructuredData::GenericSP m_cmd_obj_sp;
+    lldb::ScriptedCommandInterfaceSP m_cmd_interface_sp;
     static std::unordered_set<std::string> g_string_storer;
   };
 
 public:
-  static CommandObjectSP Create(CommandInterpreter &interpreter, 
-                std::string name,
-                StructuredData::GenericSP cmd_obj_sp,
-                ScriptedCommandSynchronicity synch, 
-                CommandReturnObject &result) {
+  static CommandObjectSP
+  Create(CommandInterpreter &interpreter, std::string name,
+         lldb::ScriptedCommandInterfaceSP cmd_interface_sp,
+         ScriptedCommandSynchronicity synch, CommandReturnObject &result) {
     CommandObjectSP new_cmd_sp(new CommandObjectScriptingObjectParsed(
-        interpreter, name, cmd_obj_sp, synch));
+        interpreter, name, cmd_interface_sp, synch));
 
     CommandObjectScriptingObjectParsed *parsed_cmd 
         = static_cast<CommandObjectScriptingObjectParsed *>(new_cmd_sp.get());
@@ -1843,34 +1821,33 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
     return new_cmd_sp;
   }
 
-  CommandObjectScriptingObjectParsed(CommandInterpreter &interpreter,
-                               std::string name,
-                               StructuredData::GenericSP cmd_obj_sp,
-                               ScriptedCommandSynchronicity synch)
-      : CommandObjectParsed(interpreter, name.c_str()), 
-        m_cmd_obj_sp(cmd_obj_sp), m_synchro(synch), 
-        m_options(interpreter, cmd_obj_sp), m_fetched_help_short(false), 
+  CommandObjectScriptingObjectParsed(
+      CommandInterpreter &interpreter, std::string name,
+      lldb::ScriptedCommandInterfaceSP cmd_interface_sp,
+      ScriptedCommandSynchronicity synch)
+      : CommandObjectParsed(interpreter, name.c_str()),
+        m_cmd_interface_sp(cmd_interface_sp), m_synchro(synch),
+        m_options(cmd_interface_sp), m_fetched_help_short(false),
         m_fetched_help_long(false) {
     StreamString stream;
-    ScriptInterpreter *scripter = GetDebugger().GetScriptInterpreter();
-    if (!scripter) {
+    if (!m_cmd_interface_sp) {
       m_options_error = Status::FromErrorString("No script interpreter");
       return;
     }
 
     // Set the flags:
-    GetFlags().Set(scripter->GetFlagsForCommandObject(cmd_obj_sp));
+    GetFlags().Set(m_cmd_interface_sp->GetFlags());
 
     // Now set up the options definitions from the options:
-    StructuredData::ObjectSP options_object_sp 
-        = scripter->GetOptionsForCommandObject(cmd_obj_sp);
+    StructuredData::ObjectSP options_object_sp =
+        m_cmd_interface_sp->GetOptionsDefinition();
     // It's okay not to have an options dict.
     if (options_object_sp) {
       // The options come as a dictionary of dictionaries.  The key of the
       // outer dict is the long option name (since that's required).  The
       // value holds all the other option specification bits.
-      StructuredData::Dictionary *options_dict 
-          = options_object_sp->GetAsDictionary();
+      StructuredData::Dictionary *options_dict =
+          options_object_sp->GetAsDictionary();
       // but if it exists, it has to be an array.
       if (options_dict) {
         m_options_error = m_options.SetOptionsFromArray(*(options_dict));
@@ -1884,8 +1861,8 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
     }
     // Then fetch the args.  Since the arguments can have usage masks you need
     // an array of arrays.
-    StructuredData::ObjectSP args_object_sp 
-      = scripter->GetArgumentsForCommandObject(cmd_obj_sp);
+    StructuredData::ObjectSP args_object_sp =
+        m_cmd_interface_sp->GetArgumentsDefinition();
     if (args_object_sp) {
       StructuredData::Array *args_array = args_object_sp->GetAsArray();        
       if (!args_array) {
@@ -2018,9 +1995,7 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
 public:
   void HandleArgumentCompletion(CompletionRequest &request,
                                 OptionElementVector &option_vec) override {
-    ScriptInterpreter *scripter = GetDebugger().GetScriptInterpreter();
-
-    if (!scripter)
+    if (!m_cmd_interface_sp)
       return;
 
     // Set up the options values on the scripted side:
@@ -2045,7 +2020,7 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
         option_slots.insert(elem.opt_arg_pos);
     }
 
-    std::vector<llvm::StringRef> args_vec;
+    std::vector<std::string> args_vec;
     Args &args = request.GetParsedLine();
     size_t num_args = args.GetArgumentCount();
     size_t cursor_idx = request.GetCursorIndex();
@@ -2053,13 +2028,13 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
 
     for (size_t idx = 0; idx < num_args; idx++) {
       if (option_slots.count(idx) == 0)
-        args_vec.push_back(args[idx].ref());
+        args_vec.push_back(args[idx].ref().str());
       else if (idx < cursor_idx)
         args_elem_pos--;
     }
     StructuredData::DictionarySP completion_dict_sp =
-        scripter->HandleArgumentCompletionForScriptedCommand(
-            m_cmd_obj_sp, args_vec, args_elem_pos, request.GetCursorCharPos());
+        m_cmd_interface_sp->HandleArgumentCompletion(
+            args_vec, args_elem_pos, request.GetCursorCharPos());
 
     if (!completion_dict_sp) {
       CommandObject::HandleArgumentCompletion(request, option_vec);
@@ -2075,22 +2050,19 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
 
   std::optional<std::string> GetRepeatCommand(Args &args,
                                               uint32_t index) override {
-    ScriptInterpreter *scripter = GetDebugger().GetScriptInterpreter();
-    if (!scripter)
+    if (!m_cmd_interface_sp)
       return std::nullopt;
 
-    return scripter->GetRepeatCommandForScriptedCommand(m_cmd_obj_sp, args);
+    return m_cmd_interface_sp->GetRepeatCommand(args);
   }
 
   llvm::StringRef GetHelp() override {
     if (m_fetched_help_short)
       return CommandObjectParsed::GetHelp();
-    ScriptInterpreter *scripter = GetDebugger().GetScriptInterpreter();
-    if (!scripter)
+    if (!m_cmd_interface_sp)
       return CommandObjectParsed::GetHelp();
     std::string docstring;
-    m_fetched_help_short =
-        scripter->GetShortHelpForCommandObject(m_cmd_obj_sp, docstring);
+    m_fetched_help_short = m_cmd_interface_sp->GetShortHelp(docstring);
     if (!docstring.empty())
       SetHelp(docstring);
 
@@ -2101,13 +2073,11 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
     if (m_fetched_help_long)
       return CommandObjectParsed::GetHelpLong();
 
-    ScriptInterpreter *scripter = GetDebugger().GetScriptInterpreter();
-    if (!scripter)
+    if (!m_cmd_interface_sp)
       return CommandObjectParsed::GetHelpLong();
 
     std::string docstring;
-    m_fetched_help_long =
-        scripter->GetLongHelpForCommandObject(m_cmd_obj_sp, docstring);
+    m_fetched_help_long = m_cmd_interface_sp->GetLongHelp(docstring);
     if (!docstring.empty())
       SetHelpLong(docstring);
     return CommandObjectParsed::GetHelpLong();
@@ -2122,17 +2092,13 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
   }
 
 protected:
-  void DoExecute(Args &args,
-                 CommandReturnObject &result) override {
-    ScriptInterpreter *scripter = GetDebugger().GetScriptInterpreter();
-
+  void DoExecute(Args &args, CommandReturnObject &result) override {
     Status error;
 
     result.SetStatus(eReturnStatusInvalid);
-    
-    if (!scripter ||
-        !scripter->RunScriptBasedParsedCommand(m_cmd_obj_sp, args,
-                                         m_synchro, result, error, m_exe_ctx)) {
+
+    if (!m_cmd_interface_sp || !m_cmd_interface_sp->RunParsedCommand(
+                                   args, m_synchro, result, error, m_exe_ctx)) {
       result.AppendError(error.AsCString());
     } else {
       // Don't change the status if the command already set it...
@@ -2146,7 +2112,7 @@ class CommandObjectScriptingObjectParsed : public CommandObjectParsed {
   }
 
 private:
-  StructuredData::GenericSP m_cmd_obj_sp;
+  lldb::ScriptedCommandInterfaceSP m_cmd_interface_sp;
   ScriptedCommandSynchronicity m_synchro;
   CommandOptions m_options;
   Status m_options_error;
@@ -2514,22 +2480,32 @@ class CommandObjectCommandsScriptAdd : public CommandObjectParsed,
         return;
       }
 
-      auto cmd_obj_sp = interpreter->CreateScriptCommandObject(
-          m_options.m_class_name.c_str());
-      if (!cmd_obj_sp) {
+      lldb::ScriptedCommandInterfaceSP cmd_interface_sp =
+          interpreter->CreateScriptedCommandInterface();
+      if (!cmd_interface_sp) {
+        result.AppendError("cannot create ScriptedCommandInterface");
+        return;
+      }
+
+      auto obj_or_err = cmd_interface_sp->CreatePluginObject(
+          m_options.m_class_name, GetDebugger().shared_from_this());
+      if (!obj_or_err) {
         result.AppendErrorWithFormatv("cannot create helper object for: "
-                                      "'{0}'", m_options.m_class_name);
+                                      "'{0}': {1}",
+                                      m_options.m_class_name,
+                                      llvm::toString(obj_or_err.takeError()));
         return;
       }
-      
+
       if (m_options.m_parsed_command) {
-        new_cmd_sp = CommandObjectScriptingObjectParsed::Create(m_interpreter, 
-            m_cmd_name, cmd_obj_sp, m_synchronicity, result);
+        new_cmd_sp = CommandObjectScriptingObjectParsed::Create(
+            m_interpreter, m_cmd_name, cmd_interface_sp, m_synchronicity,
+            result);
         if (!result.Succeeded())
           return;
       } else
         new_cmd_sp = std::make_shared<CommandObjectScriptingObjectRaw>(
-            m_interpreter, m_cmd_name, cmd_obj_sp, m_synchronicity,
+            m_interpreter, m_cmd_name, cmd_interface_sp, m_synchronicity,
             m_completion_type);
     }
     
diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp
index 4f6095d097d10..04ec3f5421981 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -7,6 +7,9 @@
 //===----------------------------------------------------------------------===//
 
 #include "lldb/Interpreter/ScriptInterpreter.h"
+#include "API/SBCommandReturnObjectImpl.h"
+#include "lldb/API/SBCommandReturnObject.h"
+#include "lldb/API/SBDebugger.h"
 #include "lldb/Core/Debugger.h"
 #include "lldb/Host/ConnectionFileDescriptor.h"
 #include "lldb/Host/Pipe.h"
@@ -96,6 +99,16 @@ ScriptInterpreter::GetOpaqueTypeFromSBBreakpointLocation(
   return break_loc.m_opaque_wp.lock();
 }
 
+CommandReturnObject *ScriptInterpreter::GetOpaqueTypeFromSBCommandReturnObject(
+    const lldb::SBCommandReturnObject &cmd_retobj) const {
+  return cmd_retobj.m_opaque_up->get();
+}
+
+lldb::DebuggerSP ScriptInterpreter::GetOpaqueTypeFromSBDebugger(
+    const lldb::SBDebugger &debugger) const {
+  return debugger.m_opaque_sp;
+}
+
 lldb::ProcessAttachInfoSP ScriptInterpreter::GetOpaqueTypeFromSBAttachInfo(
     const lldb::SBAttachInfo &attach_info) const {
   return attach_info.m_opaque_sp;
@@ -221,6 +234,10 @@ ScriptInterpreter::ExtensionToString(lldb::ScriptedExtension extension) {
     return "ScriptedFrame";
   case eScriptedExtensionScriptedStackFrameRecognizer:
     return "ScriptedStackFrameRecognizer";
+  case eScriptedExtensionScriptedCommand:
+    return "ScriptedCommand";
+  case eScriptedExtensionParsedCommand:
+    return "ParsedCommand";
   }
   llvm_unreachable("unhandled ScriptedExtension");
 }
@@ -241,6 +258,8 @@ ScriptInterpreter::StringToExtension(llvm::StringRef string) {
       .CaseLower("ScriptedFrame", eScriptedExtensionScriptedFrame)
       .CaseLower("ScriptedStackFrameRecognizer",
                  eScriptedExtensionScriptedStackFrameRecognizer)
+      .CaseLower("ScriptedCommand", eScriptedExtensionScriptedCommand)
+      .CaseLower("ParsedCommand", eScriptedExtensionParsedCommand)
       .Default(eScriptedExtensionInvalid);
 }
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
index 201574da72a19..d3dc5773eb0af 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
@@ -32,6 +32,7 @@ set(python_plugin_sources
   Interfaces/ScriptedPythonInterface.cpp
   Interfaces/ScriptedHookPythonInterface.cpp
   Interfaces/ScriptedBreakpointPythonInterface.cpp
+  Interfaces/ScriptedCommandPythonInterface.cpp
   Interfaces/ScriptedStackFrameRecognizerPythonInterface.cpp
   Interfaces/ScriptedThreadPlanPythonInterface.cpp
   Interfaces/ScriptedThreadPythonInterface.cpp
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
index 8914f6b239023..2c8e0ef8bbeb0 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
@@ -30,6 +30,7 @@ void ScriptInterpreterPythonInterfaces::Initialize() {
   ScriptedThreadPythonInterface::Initialize();
   ScriptedFramePythonInterface::Initialize();
   ScriptedStackFrameRecognizerPythonInterface::Initialize();
+  ScriptedCommandPythonInterface::Initialize();
 }
 
 void ScriptInterpreterPythonInterfaces::Terminate() {
@@ -43,4 +44,5 @@ void ScriptInterpreterPythonInterfaces::Terminate() {
   ScriptedThreadPythonInterface::Terminate();
   ScriptedFramePythonInterface::Terminate();
   ScriptedStackFrameRecognizerPythonInterface::Terminate();
+  ScriptedCommandPythonInterface::Terminate();
 }
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
index 03d747e63a592..7ddf7ec0e5ae5 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
@@ -14,6 +14,7 @@
 
 #include "OperatingSystemPythonInterface.h"
 #include "ScriptedBreakpointPythonInterface.h"
+#include "ScriptedCommandPythonInterface.h"
 #include "ScriptedFrameProviderPythonInterface.h"
 #include "ScriptedFramePythonInterface.h"
 #include "ScriptedHookPythonInterface.h"
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedCommandPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedCommandPythonInterface.cpp
new file mode 100644
index 0000000000000..defc29bb6864c
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedCommandPythonInterface.cpp
@@ -0,0 +1,248 @@
+//===----------------------------------------------------------------------===//
+//
+// 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-python.h"
+
+#include "lldb/API/SBCommandReturnObject.h"
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Interpreter/CommandReturnObject.h"
+#include "lldb/Target/ExecutionContext.h"
+#include "lldb/Utility/Args.h"
+#include "lldb/Utility/ScriptedMetadata.h"
+#include "lldb/lldb-enumerations.h"
+
+#include "../SWIGPythonBridge.h"
+#include "../ScriptInterpreterPythonImpl.h"
+#include "ScriptedCommandPythonInterface.h"
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::python;
+using Locker = ScriptInterpreterPythonImpl::Locker;
+
+ScriptedCommandPythonInterface::ScriptedCommandPythonInterface(
+    ScriptInterpreterPythonImpl &interpreter)
+    : ScriptedCommandInterface(), ScriptedPythonInterface(interpreter) {}
+
+llvm::Expected<StructuredData::GenericSP>
+ScriptedCommandPythonInterface::CreatePluginObject(
+    llvm::StringRef class_name, lldb::DebuggerSP debugger_sp) {
+  if (class_name.empty())
+    return llvm::createStringError("empty class name");
+
+  if (!debugger_sp)
+    return llvm::createStringError("invalid Debugger pointer");
+
+  m_debugger_sp = debugger_sp;
+  ScriptedMetadata scripted_metadata(class_name,
+                                     StructuredData::DictionarySP());
+  return ScriptedPythonInterface::CreatePluginObject(
+      scripted_metadata, /*script_obj=*/nullptr, debugger_sp);
+}
+
+bool ScriptedCommandPythonInterface::RunRawCommand(
+    llvm::StringRef args, ScriptedCommandSynchronicity synchronicity,
+    CommandReturnObject &cmd_retobj, Status &error,
+    const ExecutionContext &exe_ctx) {
+  lldb::DebuggerSP debugger_sp = m_debugger_sp;
+  if (!debugger_sp) {
+    error = Status::FromErrorString("invalid Debugger pointer");
+    return false;
+  }
+  lldb::ExecutionContextRefSP exe_ctx_ref_sp(new ExecutionContextRef(exe_ctx));
+
+  // Outer Locker sets up the session (with conditional NoSTDIN for
+  // interactive commands and TearDownSession on exit) that Dispatch's inner
+  // Locker doesn't provide. Nesting is only safe because Dispatch's own
+  // Locker never requests InitSession itself.
+  Locker py_lock(&m_interpreter,
+                 Locker::AcquireLock | Locker::InitSession |
+                     (cmd_retobj.GetInteractive() ? 0 : Locker::NoSTDIN),
+                 Locker::FreeLock | Locker::TearDownSession);
+  ScriptInterpreterPythonImpl::SynchronicityHandler synch_handler(
+      debugger_sp, synchronicity);
+
+  std::string args_str = args.str();
+  Dispatch("__call__", error, debugger_sp, args_str.c_str(), exe_ctx_ref_sp,
+           &cmd_retobj);
+
+  if (!error.Success() || cmd_retobj.GetStatus() == eReturnStatusFailed)
+    return false;
+
+  return true;
+}
+
+bool ScriptedCommandPythonInterface::RunParsedCommand(
+    Args &args, ScriptedCommandSynchronicity synchronicity,
+    CommandReturnObject &cmd_retobj, Status &error,
+    const ExecutionContext &exe_ctx) {
+  lldb::DebuggerSP debugger_sp = m_debugger_sp;
+  if (!debugger_sp) {
+    error = Status::FromErrorString("invalid Debugger pointer");
+    return false;
+  }
+  lldb::ExecutionContextRefSP exe_ctx_ref_sp(new ExecutionContextRef(exe_ctx));
+
+  Locker py_lock(&m_interpreter,
+                 Locker::AcquireLock | Locker::InitSession |
+                     (cmd_retobj.GetInteractive() ? 0 : Locker::NoSTDIN),
+                 Locker::FreeLock | Locker::TearDownSession);
+  ScriptInterpreterPythonImpl::SynchronicityHandler synch_handler(
+      debugger_sp, synchronicity);
+
+  StructuredData::ArraySP args_arr_sp(new StructuredData::Array());
+  for (const Args::ArgEntry &entry : args)
+    args_arr_sp->AddStringItem(entry.ref());
+  StructuredDataImpl args_impl(args_arr_sp);
+
+  Dispatch("__call__", error, debugger_sp, args_impl, exe_ctx_ref_sp,
+           &cmd_retobj);
+
+  if (!error.Success() || cmd_retobj.GetStatus() == eReturnStatusFailed)
+    return false;
+
+  return true;
+}
+
+std::optional<std::string>
+ScriptedCommandPythonInterface::GetRepeatCommand(Args &args) {
+  std::string command;
+  args.GetQuotedCommandString(command);
+  Status error;
+  StructuredData::ObjectSP obj =
+      Dispatch("get_repeat_command", error, command.c_str());
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return {};
+  return obj->GetStringValue().str();
+}
+
+StructuredData::DictionarySP
+ScriptedCommandPythonInterface::HandleArgumentCompletion(
+    std::vector<std::string> &args, size_t args_pos, size_t char_in_arg) {
+  Status error;
+  StructuredData::ObjectSP obj = Dispatch("handle_argument_completion", error,
+                                          args, args_pos, char_in_arg);
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return {};
+  StructuredData::DictionarySP dict_sp(new StructuredData::Dictionary(obj));
+  if (dict_sp->GetType() == lldb::eStructuredDataTypeInvalid)
+    return {};
+  return dict_sp;
+}
+
+StructuredData::DictionarySP
+ScriptedCommandPythonInterface::HandleOptionArgumentCompletion(
+    llvm::StringRef &long_option, size_t char_in_arg) {
+  Status error;
+  std::string long_option_str = long_option.str();
+  StructuredData::ObjectSP obj =
+      Dispatch("handle_option_argument_completion", error,
+               long_option_str.c_str(), char_in_arg);
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return {};
+
+  // A boolean return means: True means completion handled but no
+  // completions; False means completion not handled (fall back to default).
+  if (obj->GetType() == lldb::eStructuredDataTypeBoolean) {
+    if (!obj->GetBooleanValue())
+      return {};
+    StructuredData::DictionarySP dict_sp(new StructuredData::Dictionary());
+    dict_sp->AddBooleanItem("no-completion", true);
+    return dict_sp;
+  }
+
+  StructuredData::DictionarySP dict_sp(new StructuredData::Dictionary(obj));
+  if (dict_sp->GetType() == lldb::eStructuredDataTypeInvalid)
+    return {};
+  return dict_sp;
+}
+
+bool ScriptedCommandPythonInterface::GetShortHelp(std::string &dest) {
+  dest.clear();
+  Status error;
+  StructuredData::ObjectSP obj = Dispatch("get_short_help", error);
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return false;
+  dest = obj->GetStringValue().str();
+  return !dest.empty();
+}
+
+bool ScriptedCommandPythonInterface::GetLongHelp(std::string &dest) {
+  dest.clear();
+  Status error;
+  StructuredData::ObjectSP obj = Dispatch("get_long_help", error);
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return false;
+  dest = obj->GetStringValue().str();
+  return !dest.empty();
+}
+
+uint32_t ScriptedCommandPythonInterface::GetFlags() {
+  Status error;
+  StructuredData::ObjectSP obj = Dispatch("get_flags", error);
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return 0;
+  return static_cast<uint32_t>(obj->GetUnsignedIntegerValue());
+}
+
+StructuredData::ObjectSP
+ScriptedCommandPythonInterface::GetOptionsDefinition() {
+  Status error;
+  return Dispatch("get_options_definition", error);
+}
+
+StructuredData::ObjectSP
+ScriptedCommandPythonInterface::GetArgumentsDefinition() {
+  Status error;
+  return Dispatch("get_args_definition", error);
+}
+
+void ScriptedCommandPythonInterface::OptionParsingStarted() {
+  Status error;
+  Dispatch("option_parsing_started", error);
+}
+
+bool ScriptedCommandPythonInterface::SetOptionValue(ExecutionContext *exe_ctx,
+                                                    llvm::StringRef long_option,
+                                                    llvm::StringRef value) {
+  lldb::ExecutionContextRefSP exe_ctx_ref_sp;
+  if (exe_ctx)
+    exe_ctx_ref_sp = std::make_shared<ExecutionContextRef>(exe_ctx);
+  Status error;
+  std::string long_option_str = long_option.str();
+  std::string value_str = value.str();
+  StructuredData::ObjectSP obj =
+      Dispatch("set_option_value", error, exe_ctx_ref_sp,
+               long_option_str.c_str(), value_str.c_str());
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return false;
+  return obj->GetBooleanValue();
+}
+
+void ScriptedCommandPythonInterface::Initialize() {
+  const std::vector<llvm::StringRef> ci_usages = {
+      "command script add -c <ClassName> <cmd>",
+      "command script add -p <ClassName> <cmd>"};
+  const std::vector<llvm::StringRef> api_usages = {};
+  PluginManager::RegisterPlugin(
+      GetPluginNameStatic(),
+      "Implement a raw or parsed custom command backed by a Python class.",
+      CreateInstance, eScriptedExtensionScriptedCommand, eScriptLanguagePython,
+      ScriptedInterfaceUsages(ci_usages, api_usages));
+}
+
+void ScriptedCommandPythonInterface::Terminate() {
+  PluginManager::UnregisterPlugin(CreateInstance);
+}
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedCommandPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedCommandPythonInterface.h
new file mode 100644
index 0000000000000..8aecb58ddce67
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedCommandPythonInterface.h
@@ -0,0 +1,81 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDCOMMANDPYTHONINTERFACE_H
+#define LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDCOMMANDPYTHONINTERFACE_H
+
+#include "lldb/Interpreter/Interfaces/ScriptedCommandInterface.h"
+
+#include "ScriptedPythonInterface.h"
+namespace lldb_private {
+
+class ScriptedCommandPythonInterface : public ScriptedCommandInterface,
+                                       public ScriptedPythonInterface,
+                                       public PluginInterface {
+public:
+  ScriptedCommandPythonInterface(ScriptInterpreterPythonImpl &interpreter);
+
+  llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name,
+                     lldb::DebuggerSP debugger_sp) override;
+
+  llvm::SmallVector<AbstractMethodRequirement>
+  GetAbstractMethodRequirements() const override {
+    return llvm::SmallVector<AbstractMethodRequirement>();
+  }
+
+  bool RunRawCommand(llvm::StringRef args,
+                     ScriptedCommandSynchronicity synchronicity,
+                     CommandReturnObject &cmd_retobj, Status &error,
+                     const ExecutionContext &exe_ctx) override;
+
+  bool RunParsedCommand(Args &args, ScriptedCommandSynchronicity synchronicity,
+                        CommandReturnObject &cmd_retobj, Status &error,
+                        const ExecutionContext &exe_ctx) override;
+
+  std::optional<std::string> GetRepeatCommand(Args &args) override;
+
+  StructuredData::DictionarySP
+  HandleArgumentCompletion(std::vector<std::string> &args, size_t args_pos,
+                           size_t char_in_arg) override;
+
+  StructuredData::DictionarySP
+  HandleOptionArgumentCompletion(llvm::StringRef &long_option,
+                                 size_t char_in_arg) override;
+
+  bool GetShortHelp(std::string &dest) override;
+
+  bool GetLongHelp(std::string &dest) override;
+
+  uint32_t GetFlags() override;
+
+  StructuredData::ObjectSP GetOptionsDefinition() override;
+
+  StructuredData::ObjectSP GetArgumentsDefinition() override;
+
+  void OptionParsingStarted() override;
+
+  bool SetOptionValue(ExecutionContext *exe_ctx, llvm::StringRef long_option,
+                      llvm::StringRef value) override;
+
+  static void Initialize();
+
+  static void Terminate();
+
+  static llvm::StringRef GetPluginNameStatic() {
+    return "ScriptedCommandPythonInterface";
+  }
+
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+
+private:
+  lldb::DebuggerSP m_debugger_sp;
+};
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDCOMMANDPYTHONINTERFACE_H
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
index 61ceb40dd9d32..96391f7be8da0 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
@@ -8,6 +8,7 @@
 
 #include "../lldb-python.h"
 
+#include "lldb/API/SBDebugger.h"
 #include "lldb/Host/Config.h"
 #include "lldb/Utility/Log.h"
 #include "lldb/lldb-enumerations.h"
@@ -65,6 +66,21 @@ Event *ScriptedPythonInterface::ExtractValueFromPythonObject<Event *>(
   return nullptr;
 }
 
+template <>
+CommandReturnObject *
+ScriptedPythonInterface::ExtractValueFromPythonObject<CommandReturnObject *>(
+    python::PythonObject &p, Status &error) {
+  if (lldb::SBCommandReturnObject *sb_cmd_retobj =
+          reinterpret_cast<lldb::SBCommandReturnObject *>(
+              python::LLDBSWIGPython_CastPyObjectToSBCommandReturnObject(
+                  p.get())))
+    return m_interpreter.GetOpaqueTypeFromSBCommandReturnObject(*sb_cmd_retobj);
+  error =
+      Status::FromErrorString("couldn't cast lldb::SBCommandReturnObject to "
+                              "lldb_private::CommandReturnObject.");
+  return nullptr;
+}
+
 template <>
 lldb::StreamSP
 ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::StreamSP>(
@@ -380,3 +396,39 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<
 
   return static_cast<ValueType>(unmasked | flags);
 }
+
+template <>
+lldb::DebuggerSP
+ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::DebuggerSP>(
+    python::PythonObject &p, Status &error) {
+  if (lldb::SBDebugger *sb_dbg = reinterpret_cast<lldb::SBDebugger *>(
+          python::LLDBSWIGPython_CastPyObjectToSBDebugger(p.get())))
+    return m_interpreter.GetOpaqueTypeFromSBDebugger(*sb_dbg);
+  error = Status::FromErrorString(
+      "couldn't cast lldb::SBDebugger to lldb::DebuggerSP.");
+  return {};
+}
+
+template <>
+std::vector<std::string>
+ScriptedPythonInterface::ExtractValueFromPythonObject<std::vector<std::string>>(
+    python::PythonObject &p, Status &error) {
+  std::vector<std::string> result;
+  python::PythonList list(python::PyRefType::Borrowed, p.get());
+  if (!list.IsValid()) {
+    error = Status::FromErrorString(
+        "couldn't extract std::vector<std::string>: not a Python list.");
+    return result;
+  }
+
+  const uint32_t size = list.GetSize();
+  result.reserve(size);
+  for (uint32_t i = 0; i < size; ++i) {
+    python::PythonString item(python::PyRefType::Borrowed,
+                              list.GetItemAtIndex(i).get());
+    if (!item.IsValid())
+      continue;
+    result.push_back(item.GetString().str());
+  }
+  return result;
+}
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index aaa0b6a0f7a59..d82a59738a7db 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -15,6 +15,7 @@
 #include <type_traits>
 #include <utility>
 
+#include "lldb/API/SBCommandReturnObject.h"
 #include "lldb/Interpreter/Interfaces/ScriptedInterface.h"
 #include "lldb/Utility/DataBufferHeap.h"
 
@@ -668,6 +669,22 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     return python::SWIGBridge::ToSWIGWrapper(arg);
   }
 
+  python::PythonObject Transform(lldb::DebuggerSP arg) {
+    return python::SWIGBridge::ToSWIGWrapper(arg);
+  }
+
+  python::PythonObject Transform(const std::vector<std::string> &arg) {
+    python::PythonList list(python::PyInitialValue::Empty);
+    for (const std::string &s : arg)
+      list.AppendItem(python::PythonString(s));
+    return list;
+  }
+
+  python::ScopedPythonObject<lldb::SBCommandReturnObject>
+  Transform(CommandReturnObject *arg) {
+    return python::SWIGBridge::ToSWIGWrapper(*arg);
+  }
+
   template <typename T, typename U>
   void ReverseTransform(T &original_arg, U transformed_arg, Status &error) {
     // If U is not a PythonObject, don't touch it!
@@ -708,6 +725,15 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     ReverseTransform(original_arg, transformed_arg, error);
   }
 
+  // ScopedPythonObject is non-copyable — passing it through the generic
+  // TransformBack would trigger the deleted copy ctor. It manages its own
+  // cleanup via the destructor when the transformed-args tuple destructs, so
+  // there is nothing to reverse-transform back into the original arg.
+  template <typename T, typename SB>
+  void TransformBack(T &original_arg,
+                     python::ScopedPythonObject<SB> &transformed_arg,
+                     Status &error) {}
+
   template <std::size_t... I, typename... Ts, typename... Us>
   bool ReassignPtrsOrRefsArgs(std::tuple<Ts...> &original_args,
                               std::tuple<Us...> &transformed_args,
@@ -846,6 +872,16 @@ std::optional<lldb::ValueType>
 ScriptedPythonInterface::ExtractValueFromPythonObject<
     std::optional<lldb::ValueType>>(python::PythonObject &p, Status &error);
 
+template <>
+lldb::DebuggerSP
+ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::DebuggerSP>(
+    python::PythonObject &p, Status &error);
+
+template <>
+std::vector<std::string>
+ScriptedPythonInterface::ExtractValueFromPythonObject<std::vector<std::string>>(
+    python::PythonObject &p, Status &error);
+
 } // namespace lldb_private
 
 #endif // LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDPYTHONINTERFACE_H
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
index 07e0da1dcf70d..9c5391e754396 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
@@ -39,7 +39,7 @@ python::PythonObject ToSWIGHelper(void *obj, swig_type_info *info);
 
 /// A class that automatically clears an SB object when it goes out of scope.
 /// Use for cases where the SB object points to a temporary/unowned entity.
-template <typename T> class ScopedPythonObject : PythonObject {
+template <typename T> class ScopedPythonObject : public PythonObject {
 public:
   ScopedPythonObject(T *sb, swig_type_info *info)
       : PythonObject(ToSWIGHelper(sb, info)), m_sb(sb) {}
@@ -143,11 +143,6 @@ class SWIGBridge {
                                         const char *session_dictionary_name,
                                         const lldb::ValueObjectSP &valobj_sp);
 
-  static python::PythonObject
-  LLDBSwigPythonCreateCommandObject(const char *python_class_name,
-                                    const char *session_dictionary_name,
-                                    lldb::DebuggerSP debugger_sp);
-
   static size_t LLDBSwigPython_CalculateNumChildren(PyObject *implementor,
                                                     uint32_t max);
 
@@ -176,30 +171,6 @@ class SWIGBridge {
                             lldb_private::CommandReturnObject &cmd_retobj,
                             lldb::ExecutionContextRefSP exe_ctx_ref_sp);
 
-  static bool
-  LLDBSwigPythonCallCommandObject(PyObject *implementor,
-                                  lldb::DebuggerSP debugger, const char *args,
-                                  lldb_private::CommandReturnObject &cmd_retobj,
-                                  lldb::ExecutionContextRefSP exe_ctx_ref_sp);
-  static bool LLDBSwigPythonCallParsedCommandObject(
-      PyObject *implementor, lldb::DebuggerSP debugger,
-      StructuredDataImpl &args_impl,
-      lldb_private::CommandReturnObject &cmd_retobj,
-      lldb::ExecutionContextRefSP exe_ctx_ref_sp);
-
-  static std::optional<std::string>
-  LLDBSwigPythonGetRepeatCommandForScriptedCommand(PyObject *implementor,
-                                                   std::string &command);
-
-  static StructuredData::DictionarySP
-  LLDBSwigPythonHandleArgumentCompletionForScriptedCommand(
-      PyObject *implementor, std::vector<llvm::StringRef> &args_impl,
-      size_t args_pos, size_t pos_in_arg);
-
-  static StructuredData::DictionarySP
-  LLDBSwigPythonHandleOptionArgumentCompletionForScriptedCommand(
-      PyObject *implementor, llvm::StringRef &long_option, size_t pos_in_arg);
-
   static bool LLDBSwigPythonCallModuleInit(const char *python_module_name,
                                            const char *session_dictionary_name,
                                            lldb::DebuggerSP debugger);
@@ -247,6 +218,8 @@ void *LLDBSWIGPython_CastPyObjectToSBBreakpointLocation(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBAttachInfo(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBLaunchInfo(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBError(PyObject *data);
+void *LLDBSWIGPython_CastPyObjectToSBCommandReturnObject(PyObject *data);
+void *LLDBSWIGPython_CastPyObjectToSBDebugger(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBEvent(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBStream(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBThread(PyObject *data);
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index 7b1bd9d8411c5..bb8871e234bc1 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -289,6 +289,9 @@ llvm::Expected<std::string> ScriptInterpreterPython::ExtensionToImportPath(
     return "lldb.plugins.scripted_process";
   case eScriptedExtensionScriptedStackFrameRecognizer:
     return "lldb.plugins.scripted_stackframe_recognizer";
+  case eScriptedExtensionScriptedCommand:
+  case eScriptedExtensionParsedCommand:
+    return "lldb.plugins.scripted_command";
   case eScriptedExtensionInvalid:
     return llvm::createStringError("invalid extension name");
   }
@@ -2003,6 +2006,11 @@ ScriptInterpreterPythonImpl::CreateScriptedStackFrameRecognizerInterface() {
   return std::make_shared<ScriptedStackFrameRecognizerPythonInterface>(*this);
 }
 
+ScriptedCommandInterfaceSP
+ScriptInterpreterPythonImpl::CreateScriptedCommandInterface() {
+  return std::make_shared<ScriptedCommandPythonInterface>(*this);
+}
+
 ScriptedThreadInterfaceSP
 ScriptInterpreterPythonImpl::CreateScriptedThreadInterface() {
   return std::make_shared<ScriptedThreadPythonInterface>(*this);
@@ -2117,28 +2125,6 @@ ScriptInterpreterPythonImpl::CreateSyntheticScriptedProvider(
       new StructuredPythonObject(std::move(ret_val)));
 }
 
-StructuredData::GenericSP
-ScriptInterpreterPythonImpl::CreateScriptCommandObject(const char *class_name) {
-  DebuggerSP debugger_sp(m_debugger.shared_from_this());
-
-  if (class_name == nullptr || class_name[0] == '\0')
-    return StructuredData::GenericSP();
-
-  if (!debugger_sp.get())
-    return StructuredData::GenericSP();
-
-  Locker py_lock(this,
-                 Locker::AcquireLock | Locker::InitSession | Locker::NoSTDIN);
-  PythonObject ret_val = SWIGBridge::LLDBSwigPythonCreateCommandObject(
-      class_name, m_dictionary_name.c_str(), debugger_sp);
-
-  if (ret_val.IsValid())
-    return StructuredData::GenericSP(
-        new StructuredPythonObject(std::move(ret_val)));
-  else
-    return {};
-}
-
 bool ScriptInterpreterPythonImpl::GenerateTypeScriptFunction(
     const char *oneliner, std::string &output, const void *name_token) {
   StringList input;
@@ -2987,166 +2973,6 @@ bool ScriptInterpreterPythonImpl::RunScriptBasedCommand(
   return ret_val;
 }
 
-bool ScriptInterpreterPythonImpl::RunScriptBasedCommand(
-    StructuredData::GenericSP impl_obj_sp, llvm::StringRef args,
-    ScriptedCommandSynchronicity synchronicity,
-    lldb_private::CommandReturnObject &cmd_retobj, Status &error,
-    const lldb_private::ExecutionContext &exe_ctx) {
-  if (!impl_obj_sp || !impl_obj_sp->IsValid()) {
-    error = Status::FromErrorString("no function to execute");
-    return false;
-  }
-
-  lldb::DebuggerSP debugger_sp = m_debugger.shared_from_this();
-  lldb::ExecutionContextRefSP exe_ctx_ref_sp(new ExecutionContextRef(exe_ctx));
-
-  if (!debugger_sp.get()) {
-    error = Status::FromErrorString("invalid Debugger pointer");
-    return false;
-  }
-
-  bool ret_val = false;
-
-  {
-    Locker py_lock(this,
-                   Locker::AcquireLock | Locker::InitSession |
-                       (cmd_retobj.GetInteractive() ? 0 : Locker::NoSTDIN),
-                   Locker::FreeLock | Locker::TearDownSession);
-
-    SynchronicityHandler synch_handler(debugger_sp, synchronicity);
-
-    std::string args_str = args.str();
-    ret_val = SWIGBridge::LLDBSwigPythonCallCommandObject(
-        static_cast<PyObject *>(impl_obj_sp->GetValue()), debugger_sp,
-        args_str.c_str(), cmd_retobj, exe_ctx_ref_sp);
-  }
-
-  if (!ret_val)
-    error = Status::FromErrorString("unable to execute script function");
-  else if (cmd_retobj.GetStatus() == eReturnStatusFailed)
-    return false;
-
-  error.Clear();
-  return ret_val;
-}
-
-bool ScriptInterpreterPythonImpl::RunScriptBasedParsedCommand(
-    StructuredData::GenericSP impl_obj_sp, Args &args,
-    ScriptedCommandSynchronicity synchronicity,
-    lldb_private::CommandReturnObject &cmd_retobj, Status &error,
-    const lldb_private::ExecutionContext &exe_ctx) {
-  if (!impl_obj_sp || !impl_obj_sp->IsValid()) {
-    error = Status::FromErrorString("no function to execute");
-    return false;
-  }
-
-  lldb::DebuggerSP debugger_sp = m_debugger.shared_from_this();
-  lldb::ExecutionContextRefSP exe_ctx_ref_sp(new ExecutionContextRef(exe_ctx));
-
-  if (!debugger_sp.get()) {
-    error = Status::FromErrorString("invalid Debugger pointer");
-    return false;
-  }
-
-  bool ret_val = false;
-
-  {
-    Locker py_lock(this,
-                   Locker::AcquireLock | Locker::InitSession |
-                       (cmd_retobj.GetInteractive() ? 0 : Locker::NoSTDIN),
-                   Locker::FreeLock | Locker::TearDownSession);
-
-    SynchronicityHandler synch_handler(debugger_sp, synchronicity);
-
-    StructuredData::ArraySP args_arr_sp(new StructuredData::Array());
-
-    for (const Args::ArgEntry &entry : args) {
-      args_arr_sp->AddStringItem(entry.ref());
-    }
-    StructuredDataImpl args_impl(args_arr_sp);
-
-    ret_val = SWIGBridge::LLDBSwigPythonCallParsedCommandObject(
-        static_cast<PyObject *>(impl_obj_sp->GetValue()), debugger_sp,
-        args_impl, cmd_retobj, exe_ctx_ref_sp);
-  }
-
-  if (!ret_val)
-    error = Status::FromErrorString("unable to execute script function");
-  else if (cmd_retobj.GetStatus() == eReturnStatusFailed)
-    return false;
-
-  error.Clear();
-  return ret_val;
-}
-
-std::optional<std::string>
-ScriptInterpreterPythonImpl::GetRepeatCommandForScriptedCommand(
-    StructuredData::GenericSP impl_obj_sp, Args &args) {
-  if (!impl_obj_sp || !impl_obj_sp->IsValid())
-    return std::nullopt;
-
-  lldb::DebuggerSP debugger_sp = m_debugger.shared_from_this();
-
-  if (!debugger_sp.get())
-    return std::nullopt;
-
-  std::optional<std::string> ret_val;
-
-  {
-    Locker py_lock(this, Locker::AcquireLock | Locker::NoSTDIN,
-                   Locker::FreeLock);
-
-    StructuredData::ArraySP args_arr_sp(new StructuredData::Array());
-
-    // For scripting commands, we send the command string:
-    std::string command;
-    args.GetQuotedCommandString(command);
-    ret_val = SWIGBridge::LLDBSwigPythonGetRepeatCommandForScriptedCommand(
-        static_cast<PyObject *>(impl_obj_sp->GetValue()), command);
-  }
-  return ret_val;
-}
-
-StructuredData::DictionarySP
-ScriptInterpreterPythonImpl::HandleArgumentCompletionForScriptedCommand(
-    StructuredData::GenericSP impl_obj_sp, std::vector<llvm::StringRef> &args,
-    size_t args_pos, size_t char_in_arg) {
-  StructuredData::DictionarySP completion_dict_sp;
-  if (!impl_obj_sp || !impl_obj_sp->IsValid())
-    return completion_dict_sp;
-
-  {
-    Locker py_lock(this, Locker::AcquireLock | Locker::NoSTDIN,
-                   Locker::FreeLock);
-
-    completion_dict_sp =
-        SWIGBridge::LLDBSwigPythonHandleArgumentCompletionForScriptedCommand(
-            static_cast<PyObject *>(impl_obj_sp->GetValue()), args, args_pos,
-            char_in_arg);
-  }
-  return completion_dict_sp;
-}
-
-StructuredData::DictionarySP
-ScriptInterpreterPythonImpl::HandleOptionArgumentCompletionForScriptedCommand(
-    StructuredData::GenericSP impl_obj_sp, llvm::StringRef &long_option,
-    size_t char_in_arg) {
-  StructuredData::DictionarySP completion_dict_sp;
-  if (!impl_obj_sp || !impl_obj_sp->IsValid())
-    return completion_dict_sp;
-
-  {
-    Locker py_lock(this, Locker::AcquireLock | Locker::NoSTDIN,
-                   Locker::FreeLock);
-
-    completion_dict_sp = SWIGBridge::
-        LLDBSwigPythonHandleOptionArgumentCompletionForScriptedCommand(
-            static_cast<PyObject *>(impl_obj_sp->GetValue()), long_option,
-            char_in_arg);
-  }
-  return completion_dict_sp;
-}
-
 /// In Python, a special attribute __doc__ contains the docstring for an object
 /// (function, method, class, ...) if any is defined Otherwise, the attribute's
 /// value is None.
@@ -3180,322 +3006,6 @@ bool ScriptInterpreterPythonImpl::GetDocumentationForItem(const char *item,
   return false;
 }
 
-bool ScriptInterpreterPythonImpl::GetShortHelpForCommandObject(
-    StructuredData::GenericSP cmd_obj_sp, std::string &dest) {
-  dest.clear();
-
-  Locker py_lock(this, Locker::AcquireLock | Locker::NoSTDIN, Locker::FreeLock);
-
-  if (!cmd_obj_sp)
-    return false;
-
-  PythonObject implementor(PyRefType::Borrowed,
-                           (PyObject *)cmd_obj_sp->GetValue());
-
-  if (!implementor.IsAllocated())
-    return false;
-
-  llvm::Expected<PythonObject> expected_py_return =
-      implementor.CallMethod("get_short_help");
-
-  if (!expected_py_return) {
-    llvm::consumeError(expected_py_return.takeError());
-    return false;
-  }
-
-  PythonObject py_return = std::move(expected_py_return.get());
-
-  if (py_return.IsAllocated() && PythonString::Check(py_return.get())) {
-    PythonString py_string(PyRefType::Borrowed, py_return.get());
-    llvm::StringRef return_data(py_string.GetString());
-    dest.assign(return_data.data(), return_data.size());
-    return true;
-  }
-
-  return false;
-}
-
-uint32_t ScriptInterpreterPythonImpl::GetFlagsForCommandObject(
-    StructuredData::GenericSP cmd_obj_sp) {
-  uint32_t result = 0;
-
-  Locker py_lock(this, Locker::AcquireLock | Locker::NoSTDIN, Locker::FreeLock);
-
-  static char callee_name[] = "get_flags";
-
-  if (!cmd_obj_sp)
-    return result;
-
-  PythonObject implementor(PyRefType::Borrowed,
-                           (PyObject *)cmd_obj_sp->GetValue());
-
-  if (!implementor.IsAllocated())
-    return result;
-
-  PythonObject pmeth(PyRefType::Owned,
-                     PyObject_GetAttrString(implementor.get(), callee_name));
-
-  if (PyErr_Occurred())
-    PyErr_Clear();
-
-  if (!pmeth.IsAllocated())
-    return result;
-
-  if (PyCallable_Check(pmeth.get()) == 0) {
-    if (PyErr_Occurred())
-      PyErr_Clear();
-    return result;
-  }
-
-  if (PyErr_Occurred())
-    PyErr_Clear();
-
-  long long py_return = unwrapOrSetPythonException(
-      As<long long>(implementor.CallMethod(callee_name)));
-
-  // if it fails, print the error but otherwise go on
-  if (PyErr_Occurred()) {
-    PyErr_Print();
-    PyErr_Clear();
-  } else {
-    result = py_return;
-  }
-
-  return result;
-}
-
-StructuredData::ObjectSP
-ScriptInterpreterPythonImpl::GetOptionsForCommandObject(
-    StructuredData::GenericSP cmd_obj_sp) {
-  StructuredData::ObjectSP result = {};
-
-  Locker py_lock(this, Locker::AcquireLock | Locker::NoSTDIN, Locker::FreeLock);
-
-  static char callee_name[] = "get_options_definition";
-
-  if (!cmd_obj_sp)
-    return result;
-
-  PythonObject implementor(PyRefType::Borrowed,
-                           (PyObject *)cmd_obj_sp->GetValue());
-
-  if (!implementor.IsAllocated())
-    return result;
-
-  PythonObject pmeth(PyRefType::Owned,
-                     PyObject_GetAttrString(implementor.get(), callee_name));
-
-  if (PyErr_Occurred())
-    PyErr_Clear();
-
-  if (!pmeth.IsAllocated())
-    return result;
-
-  if (PyCallable_Check(pmeth.get()) == 0) {
-    if (PyErr_Occurred())
-      PyErr_Clear();
-    return result;
-  }
-
-  if (PyErr_Occurred())
-    PyErr_Clear();
-
-  PythonDictionary py_return = unwrapOrSetPythonException(
-      As<PythonDictionary>(implementor.CallMethod(callee_name)));
-
-  // if it fails, print the error but otherwise go on
-  if (PyErr_Occurred()) {
-    PyErr_Print();
-    PyErr_Clear();
-    return {};
-  }
-  return py_return.CreateStructuredObject();
-}
-
-StructuredData::ObjectSP
-ScriptInterpreterPythonImpl::GetArgumentsForCommandObject(
-    StructuredData::GenericSP cmd_obj_sp) {
-  StructuredData::ObjectSP result = {};
-
-  Locker py_lock(this, Locker::AcquireLock | Locker::NoSTDIN, Locker::FreeLock);
-
-  static char callee_name[] = "get_args_definition";
-
-  if (!cmd_obj_sp)
-    return result;
-
-  PythonObject implementor(PyRefType::Borrowed,
-                           (PyObject *)cmd_obj_sp->GetValue());
-
-  if (!implementor.IsAllocated())
-    return result;
-
-  PythonObject pmeth(PyRefType::Owned,
-                     PyObject_GetAttrString(implementor.get(), callee_name));
-
-  if (PyErr_Occurred())
-    PyErr_Clear();
-
-  if (!pmeth.IsAllocated())
-    return result;
-
-  if (PyCallable_Check(pmeth.get()) == 0) {
-    if (PyErr_Occurred())
-      PyErr_Clear();
-    return result;
-  }
-
-  if (PyErr_Occurred())
-    PyErr_Clear();
-
-  PythonList py_return = unwrapOrSetPythonException(
-      As<PythonList>(implementor.CallMethod(callee_name)));
-
-  // if it fails, print the error but otherwise go on
-  if (PyErr_Occurred()) {
-    PyErr_Print();
-    PyErr_Clear();
-    return {};
-  }
-  return py_return.CreateStructuredObject();
-}
-
-void ScriptInterpreterPythonImpl::OptionParsingStartedForCommandObject(
-    StructuredData::GenericSP cmd_obj_sp) {
-
-  Locker py_lock(this, Locker::AcquireLock | Locker::NoSTDIN, Locker::FreeLock);
-
-  static char callee_name[] = "option_parsing_started";
-
-  if (!cmd_obj_sp)
-    return;
-
-  PythonObject implementor(PyRefType::Borrowed,
-                           (PyObject *)cmd_obj_sp->GetValue());
-
-  if (!implementor.IsAllocated())
-    return;
-
-  PythonObject pmeth(PyRefType::Owned,
-                     PyObject_GetAttrString(implementor.get(), callee_name));
-
-  if (PyErr_Occurred())
-    PyErr_Clear();
-
-  if (!pmeth.IsAllocated())
-    return;
-
-  if (PyCallable_Check(pmeth.get()) == 0) {
-    if (PyErr_Occurred())
-      PyErr_Clear();
-    return;
-  }
-
-  if (PyErr_Occurred())
-    PyErr_Clear();
-
-  // option_parsing_starting doesn't return anything, ignore anything but
-  // python errors.
-  unwrapOrSetPythonException(As<bool>(implementor.CallMethod(callee_name)));
-
-  // if it fails, print the error but otherwise go on
-  if (PyErr_Occurred()) {
-    PyErr_Print();
-    PyErr_Clear();
-    return;
-  }
-}
-
-bool ScriptInterpreterPythonImpl::SetOptionValueForCommandObject(
-    StructuredData::GenericSP cmd_obj_sp, ExecutionContext *exe_ctx,
-    llvm::StringRef long_option, llvm::StringRef value) {
-  StructuredData::ObjectSP result = {};
-
-  Locker py_lock(this, Locker::AcquireLock | Locker::NoSTDIN, Locker::FreeLock);
-
-  static char callee_name[] = "set_option_value";
-
-  if (!cmd_obj_sp)
-    return false;
-
-  PythonObject implementor(PyRefType::Borrowed,
-                           (PyObject *)cmd_obj_sp->GetValue());
-
-  if (!implementor.IsAllocated())
-    return false;
-
-  PythonObject pmeth(PyRefType::Owned,
-                     PyObject_GetAttrString(implementor.get(), callee_name));
-
-  if (PyErr_Occurred())
-    PyErr_Clear();
-
-  if (!pmeth.IsAllocated())
-    return false;
-
-  if (PyCallable_Check(pmeth.get()) == 0) {
-    if (PyErr_Occurred())
-      PyErr_Clear();
-    return false;
-  }
-
-  if (PyErr_Occurred())
-    PyErr_Clear();
-
-  lldb::ExecutionContextRefSP exe_ctx_ref_sp;
-  if (exe_ctx)
-    exe_ctx_ref_sp = std::make_shared<ExecutionContextRef>(exe_ctx);
-  PythonObject ctx_ref_obj = SWIGBridge::ToSWIGWrapper(exe_ctx_ref_sp);
-
-  bool py_return = unwrapOrSetPythonException(As<bool>(
-      implementor.CallMethod(callee_name, ctx_ref_obj,
-                             long_option.str().c_str(), value.str().c_str())));
-
-  // if it fails, print the error but otherwise go on
-  if (PyErr_Occurred()) {
-    PyErr_Print();
-    PyErr_Clear();
-    return false;
-  }
-  return py_return;
-}
-
-bool ScriptInterpreterPythonImpl::GetLongHelpForCommandObject(
-    StructuredData::GenericSP cmd_obj_sp, std::string &dest) {
-  dest.clear();
-
-  Locker py_lock(this, Locker::AcquireLock | Locker::NoSTDIN, Locker::FreeLock);
-
-  if (!cmd_obj_sp)
-    return false;
-
-  PythonObject implementor(PyRefType::Borrowed,
-                           (PyObject *)cmd_obj_sp->GetValue());
-
-  if (!implementor.IsAllocated())
-    return false;
-
-  llvm::Expected<PythonObject> expected_py_return =
-      implementor.CallMethod("get_long_help");
-
-  if (!expected_py_return) {
-    llvm::consumeError(expected_py_return.takeError());
-    return false;
-  }
-
-  PythonObject py_return = std::move(expected_py_return.get());
-
-  bool got_string = false;
-  if (py_return.IsAllocated() && PythonString::Check(py_return.get())) {
-    PythonString str(PyRefType::Borrowed, py_return.get());
-    llvm::StringRef str_data(str.GetString());
-    dest.assign(str_data.data(), str_data.size());
-    got_string = true;
-  }
-
-  return got_string;
-}
-
 std::unique_ptr<ScriptInterpreterLocker>
 ScriptInterpreterPythonImpl::AcquireInterpreterLock() {
   std::unique_ptr<ScriptInterpreterLocker> py_lock(new Locker(
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
index d8a817198253f..51b373afbb9f4 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
@@ -70,9 +70,6 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
   CreateSyntheticScriptedProvider(const char *class_name,
                                   lldb::ValueObjectSP valobj) override;
 
-  StructuredData::GenericSP
-  CreateScriptCommandObject(const char *class_name) override;
-
   StructuredData::ObjectSP
   CreateStructuredDataFromScriptObject(ScriptObject obj) override;
 
@@ -86,6 +83,8 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
   lldb::ScriptedStackFrameRecognizerInterfaceSP
   CreateScriptedStackFrameRecognizerInterface() override;
 
+  lldb::ScriptedCommandInterfaceSP CreateScriptedCommandInterface() override;
+
   lldb::ScriptedThreadInterfaceSP CreateScriptedThreadInterface() override;
 
   lldb::ScriptedFrameInterfaceSP CreateScriptedFrameInterface() override;
@@ -137,30 +136,6 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
                         Status &error,
                         const lldb_private::ExecutionContext &exe_ctx) override;
 
-  bool RunScriptBasedCommand(
-      StructuredData::GenericSP impl_obj_sp, llvm::StringRef args,
-      ScriptedCommandSynchronicity synchronicity,
-      lldb_private::CommandReturnObject &cmd_retobj, Status &error,
-      const lldb_private::ExecutionContext &exe_ctx) override;
-
-  bool RunScriptBasedParsedCommand(
-      StructuredData::GenericSP impl_obj_sp, Args &args,
-      ScriptedCommandSynchronicity synchronicity,
-      lldb_private::CommandReturnObject &cmd_retobj, Status &error,
-      const lldb_private::ExecutionContext &exe_ctx) override;
-
-  std::optional<std::string>
-  GetRepeatCommandForScriptedCommand(StructuredData::GenericSP impl_obj_sp,
-                                     Args &args) override;
-
-  StructuredData::DictionarySP HandleArgumentCompletionForScriptedCommand(
-      StructuredData::GenericSP impl_obj_sp, std::vector<llvm::StringRef> &args,
-      size_t args_pos, size_t char_in_arg) override;
-
-  StructuredData::DictionarySP HandleOptionArgumentCompletionForScriptedCommand(
-      StructuredData::GenericSP impl_obj_sp, llvm::StringRef &long_options,
-      size_t char_in_arg) override;
-
   Status GenerateFunction(const char *signature, const StringList &input,
                           bool is_callback) override;
 
@@ -183,29 +158,6 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
 
   bool GetDocumentationForItem(const char *item, std::string &dest) override;
 
-  bool GetShortHelpForCommandObject(StructuredData::GenericSP cmd_obj_sp,
-                                    std::string &dest) override;
-
-  uint32_t
-  GetFlagsForCommandObject(StructuredData::GenericSP cmd_obj_sp) override;
-
-  bool GetLongHelpForCommandObject(StructuredData::GenericSP cmd_obj_sp,
-                                   std::string &dest) override;
-
-  StructuredData::ObjectSP
-  GetOptionsForCommandObject(StructuredData::GenericSP cmd_obj_sp) override;
-
-  StructuredData::ObjectSP
-  GetArgumentsForCommandObject(StructuredData::GenericSP cmd_obj_sp) override;
-
-  bool SetOptionValueForCommandObject(StructuredData::GenericSP cmd_obj_sp,
-                                      ExecutionContext *exe_ctx,
-                                      llvm::StringRef long_option,
-                                      llvm::StringRef value) override;
-
-  void OptionParsingStartedForCommandObject(
-      StructuredData::GenericSP cmd_obj_sp) override;
-
   bool CheckObjectExists(const char *name) override {
     if (!name || !name[0])
       return false;
diff --git a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
index c9298191ec3c1..808bb6157e5b8 100644
--- a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
+++ b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
@@ -73,13 +73,6 @@ lldb_private::python::SWIGBridge::LLDBSwigPythonCreateSyntheticProvider(
   return python::PythonObject();
 }
 
-python::PythonObject
-lldb_private::python::SWIGBridge::LLDBSwigPythonCreateCommandObject(
-    const char *python_class_name, const char *session_dictionary_name,
-    lldb::DebuggerSP debugger_sp) {
-  return python::PythonObject();
-}
-
 size_t lldb_private::python::SWIGBridge::LLDBSwigPython_CalculateNumChildren(
     PyObject *implementor, uint32_t max) {
   return 0;
@@ -126,6 +119,16 @@ lldb_private::python::LLDBSWIGPython_CastPyObjectToSBError(PyObject *data) {
   return nullptr;
 }
 
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBCommandReturnObject(
+    PyObject *data) {
+  return nullptr;
+}
+
+void *
+lldb_private::python::LLDBSWIGPython_CastPyObjectToSBDebugger(PyObject *data) {
+  return nullptr;
+}
+
 void *
 lldb_private::python::LLDBSWIGPython_CastPyObjectToSBEvent(PyObject *data) {
   return nullptr;
@@ -207,40 +210,6 @@ bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallCommand(
   return false;
 }
 
-bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallCommandObject(
-    PyObject *implementor, lldb::DebuggerSP debugger, const char *args,
-    lldb_private::CommandReturnObject &cmd_retobj,
-    lldb::ExecutionContextRefSP exe_ctx_ref_sp) {
-  return false;
-}
-
-bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallParsedCommandObject(
-    PyObject *implementor, lldb::DebuggerSP debugger,
-    StructuredDataImpl &args_impl,
-    lldb_private::CommandReturnObject &cmd_retobj,
-    lldb::ExecutionContextRefSP exe_ctx_ref_sp) {
-  return false;
-}
-
-std::optional<std::string>
-LLDBSwigPythonGetRepeatCommandForScriptedCommand(PyObject *implementor,
-                                                 std::string &command) {
-  return std::nullopt;
-}
-
-StructuredData::DictionarySP
-LLDBSwigPythonHandleArgumentCompletionForScriptedCommand(
-    PyObject *implementor, std::vector<llvm::StringRef> &args, size_t args_pos,
-    size_t pos_in_arg) {
-  return {};
-}
-
-StructuredData::DictionarySP
-LLDBSwigPythonHandleOptionArgumentCompletionForScriptedCommand(
-    PyObject *implementor, llvm::StringRef &long_options, size_t char_in_arg) {
-  return {};
-}
-
 bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallModuleInit(
     const char *python_module_name, const char *session_dictionary_name,
     lldb::DebuggerSP debugger) {

>From 77652cce551b8f542ee5569cf7d0c330375b4c07 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Tue, 28 Jul 2026 17:42:01 -0700
Subject: [PATCH 02/16] [lldb] Include `Debugger.h` in
 `ScriptedCommandPythonInterface.cpp` (NFC) (#212662)

Commit c35bdc99a36b passes a `lldb::DebuggerSP` to the
`ScriptedPythonInterface` base template, whose `Transform` overload is
constrained on `std::is_base_of_v` and so needs `Debugger` to be
complete. This patch includes `lldb/Core/Debugger.h` since the
translation unit only saw the forward declaration.

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
(cherry picked from commit f1ba92abeffcd4d262d70843fa083c10b179cb4d)
---
 .../Python/Interfaces/ScriptedCommandPythonInterface.cpp         | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedCommandPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedCommandPythonInterface.cpp
index defc29bb6864c..8d088b44427bc 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedCommandPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedCommandPythonInterface.cpp
@@ -9,6 +9,7 @@
 #include "../lldb-python.h"
 
 #include "lldb/API/SBCommandReturnObject.h"
+#include "lldb/Core/Debugger.h"
 #include "lldb/Core/PluginManager.h"
 #include "lldb/Interpreter/CommandReturnObject.h"
 #include "lldb/Target/ExecutionContext.h"

>From e92c0dc6cb216ab531cd2bda14126240a351a70a Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Wed, 29 Jul 2026 18:44:26 -0700
Subject: [PATCH 03/16] [lldb/script] Add class-based summary providers via
 ScriptedStringSummaryInterface (#210469)

(cherry picked from commit 44a11330d9c246c7c042c16f26a10373956731f7)
---
 lldb/bindings/python/CMakeLists.txt           |  1 +
 lldb/docs/CMakeLists.txt                      |  1 +
 .../templates/scripted_string_summary.py      | 40 ++++++++++
 lldb/include/lldb/API/SBTypeSummary.h         |  4 +
 .../include/lldb/DataFormatters/TypeSummary.h | 54 ++++++++++++-
 .../ScriptedStringSummaryInterface.h          | 28 +++++++
 .../lldb/Interpreter/ScriptInterpreter.h      |  5 ++
 lldb/include/lldb/lldb-enumerations.h         |  3 +-
 lldb/include/lldb/lldb-forward.h              |  3 +
 lldb/source/API/SBTypeSummary.cpp             | 25 +++++++
 lldb/source/Commands/CommandObjectType.cpp    | 72 ++++++++++++++++--
 lldb/source/DataFormatters/TypeSummary.cpp    | 75 +++++++++++++++++++
 lldb/source/Interpreter/ScriptInterpreter.cpp |  4 +
 .../ScriptInterpreter/Python/CMakeLists.txt   |  1 +
 .../ScriptInterpreterPythonInterfaces.cpp     |  2 +
 .../ScriptInterpreterPythonInterfaces.h       |  1 +
 .../Interfaces/ScriptedPythonInterface.h      | 12 +++
 .../ScriptedStringSummaryPythonInterface.cpp  | 61 +++++++++++++++
 .../ScriptedStringSummaryPythonInterface.h    | 48 ++++++++++++
 .../Python/ScriptInterpreterPython.cpp        |  7 ++
 .../Python/ScriptInterpreterPythonImpl.h      |  3 +
 21 files changed, 442 insertions(+), 8 deletions(-)
 create mode 100644 lldb/examples/python/templates/scripted_string_summary.py
 create mode 100644 lldb/include/lldb/Interpreter/Interfaces/ScriptedStringSummaryInterface.h
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedStringSummaryPythonInterface.cpp
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedStringSummaryPythonInterface.h

diff --git a/lldb/bindings/python/CMakeLists.txt b/lldb/bindings/python/CMakeLists.txt
index 6ffdf9ccafabc..d2761528c6493 100644
--- a/lldb/bindings/python/CMakeLists.txt
+++ b/lldb/bindings/python/CMakeLists.txt
@@ -121,6 +121,7 @@ function(finish_swig_python swig_target lldb_python_bindings_dir lldb_python_tar
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_hook.py"
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_stackframe_recognizer.py"
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_command.py"
+    "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_string_summary.py"
     )
 
   if(APPLE)
diff --git a/lldb/docs/CMakeLists.txt b/lldb/docs/CMakeLists.txt
index 53647990f7842..5357db6e652a1 100644
--- a/lldb/docs/CMakeLists.txt
+++ b/lldb/docs/CMakeLists.txt
@@ -34,6 +34,7 @@ if (LLDB_ENABLE_PYTHON AND SPHINX_FOUND)
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_hook.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_stackframe_recognizer.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_command.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
+      COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_string_summary.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMENT "Copying lldb.py to pretend its a Python package.")
 
     add_dependencies(lldb-python-doc-package swig_wrapper_python)
diff --git a/lldb/examples/python/templates/scripted_string_summary.py b/lldb/examples/python/templates/scripted_string_summary.py
new file mode 100644
index 0000000000000..217c3bb6a8e25
--- /dev/null
+++ b/lldb/examples/python/templates/scripted_string_summary.py
@@ -0,0 +1,40 @@
+from abc import ABCMeta, abstractmethod
+
+import lldb
+
+
+class ScriptedStringSummary(metaclass=ABCMeta):
+    """
+    The base class for a scripted string summary provider.
+
+    A summary provider produces the one-line string shown next to a value in
+    `frame variable`/`expression` output. Register it with
+    `type summary add -l <ClassName> <TypeName>`.
+
+    Most of the base class methods are `@abstractmethod` that need to be
+    overwritten by the inheriting class.
+    """
+
+    def __init__(self):
+        """Construct a scripted summary provider.
+
+        Summary providers are constructed with no arguments and are shared
+        across every value they're asked to summarize.
+        """
+        pass
+
+    @abstractmethod
+    def get_summary(
+        self, valobj: lldb.SBValue, options: lldb.SBTypeSummaryOptions
+    ) -> str:
+        """Get the summary string for a value.
+
+        Args:
+            valobj (lldb.SBValue): The value to summarize.
+            options (lldb.SBTypeSummaryOptions): The options to use when
+                producing the summary.
+
+        Returns:
+            str: The summary string for `valobj`.
+        """
+        pass
diff --git a/lldb/include/lldb/API/SBTypeSummary.h b/lldb/include/lldb/API/SBTypeSummary.h
index b6869e53a39e7..9ca26532d8156 100644
--- a/lldb/include/lldb/API/SBTypeSummary.h
+++ b/lldb/include/lldb/API/SBTypeSummary.h
@@ -81,6 +81,10 @@ class SBTypeSummary {
   CreateWithScriptCode(const char *data,
                        uint32_t options = 0); // see lldb::eTypeOption values
 
+  static SBTypeSummary
+  CreateWithClassName(const char *data,
+                      uint32_t options = 0); // see lldb::eTypeOption values
+
 #ifndef SWIG
   static SBTypeSummary CreateWithCallback(FormatCallback cb,
                                           uint32_t options = 0,
diff --git a/lldb/include/lldb/DataFormatters/TypeSummary.h b/lldb/include/lldb/DataFormatters/TypeSummary.h
index a0938556e0174..4abf18c9c39a3 100644
--- a/lldb/include/lldb/DataFormatters/TypeSummary.h
+++ b/lldb/include/lldb/DataFormatters/TypeSummary.h
@@ -48,7 +48,14 @@ class TypeSummaryOptions {
 
 class TypeSummaryImpl {
 public:
-  enum class Kind { eSummaryString, eScript, eBytecode, eCallback, eInternal };
+  enum class Kind {
+    eSummaryString,
+    eScript,
+    eBytecode,
+    eCallback,
+    eInternal,
+    eScriptedClass
+  };
 
   virtual ~TypeSummaryImpl() = default;
 
@@ -423,6 +430,51 @@ struct ScriptSummaryFormat : public TypeSummaryImpl {
   const ScriptSummaryFormat &operator=(const ScriptSummaryFormat &) = delete;
 };
 
+// Python-based summaries backed by a class, running an instance's
+// `get_summary` method to show data. Unlike ScriptSummaryFormat (a bare
+// function resolved once and cached), the Python object here is itself the
+// cache: it's created lazily on the first call to FormatObject (since this
+// format can be constructed via SBTypeSummary::CreateWithClassName before
+// any debugger/target context exists) and then reused across every
+// subsequent call, for every value of the matching type.
+struct ScriptedSummaryFormat : public TypeSummaryImpl {
+  std::string m_class_name;
+  lldb::ScriptedStringSummaryInterfaceSP m_interface_sp;
+
+  ScriptedSummaryFormat(const TypeSummaryImpl::Flags &flags,
+                        const char *class_name, uint32_t ptr_match_depth = 1);
+
+  ~ScriptedSummaryFormat() override = default;
+
+  const char *GetClassName() const { return m_class_name.c_str(); }
+
+  void SetClassName(const char *class_name) {
+    if (class_name)
+      m_class_name.assign(class_name);
+    else
+      m_class_name.clear();
+    m_interface_sp.reset();
+  }
+
+  bool FormatObject(ValueObject *valobj, std::string &dest,
+                    const TypeSummaryOptions &options) override;
+
+  std::string GetDescription() override;
+
+  std::string GetName() override;
+
+  static bool classof(const TypeSummaryImpl *S) {
+    return S->GetKind() == Kind::eScriptedClass;
+  }
+
+  typedef std::shared_ptr<ScriptedSummaryFormat> SharedPointer;
+
+private:
+  ScriptedSummaryFormat(const ScriptedSummaryFormat &) = delete;
+  const ScriptedSummaryFormat &
+  operator=(const ScriptedSummaryFormat &) = delete;
+};
+
 /// A summary formatter that is defined in LLDB formmater bytecode.
 ///
 /// See `BytecodeSyntheticChildren` for the corresponding synthetic formatter.
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedStringSummaryInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedStringSummaryInterface.h
new file mode 100644
index 0000000000000..83f1822823005
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedStringSummaryInterface.h
@@ -0,0 +1,28 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_INTERFACES_SCRIPTEDSTRINGSUMMARYINTERFACE_H
+#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDSTRINGSUMMARYINTERFACE_H
+
+#include "ScriptedInterface.h"
+#include "lldb/lldb-private.h"
+
+namespace lldb_private {
+class ScriptedStringSummaryInterface : virtual public ScriptedInterface {
+public:
+  virtual llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name) = 0;
+
+  virtual llvm::Expected<std::string>
+  GetSummary(ValueObject &valobj, const TypeSummaryOptions &options) {
+    return llvm::createStringError("not implemented");
+  }
+};
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDSTRINGSUMMARYINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 9d3e7e6f32ff5..448091e0ceb85 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -512,6 +512,11 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  virtual lldb::ScriptedStringSummaryInterfaceSP
+  CreateScriptedStringSummaryInterface() {
+    return {};
+  }
+
   virtual StructuredData::ObjectSP
   CreateStructuredDataFromScriptObject(ScriptObject obj) {
     return {};
diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h
index f73d0085f2320..af0ea08b81e45 100644
--- a/lldb/include/lldb/lldb-enumerations.h
+++ b/lldb/include/lldb/lldb-enumerations.h
@@ -270,7 +270,8 @@ enum ScriptedExtension {
   eScriptedExtensionScriptedStackFrameRecognizer,
   eScriptedExtensionScriptedCommand,
   eScriptedExtensionParsedCommand,
-  kLastScriptedExtension = eScriptedExtensionParsedCommand
+  eScriptedExtensionScriptedStringSummary,
+  kLastScriptedExtension = eScriptedExtensionScriptedStringSummary
 };
 
 /// Register numbering types.
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index 2572aa0dc344b..fceb94a8a3e7d 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -200,6 +200,7 @@ class ScriptedProcessInterface;
 class ScriptedThreadInterface;
 class ScriptedThreadPlanInterface;
 class ScriptedStackFrameRecognizerInterface;
+class ScriptedStringSummaryInterface;
 class ScriptedSyntheticChildren;
 class SearchFilter;
 class Section;
@@ -441,6 +442,8 @@ typedef std::shared_ptr<lldb_private::ScriptedStackFrameRecognizerInterface>
     ScriptedStackFrameRecognizerInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedCommandInterface>
     ScriptedCommandInterfaceSP;
+typedef std::shared_ptr<lldb_private::ScriptedStringSummaryInterface>
+    ScriptedStringSummaryInterfaceSP;
 typedef std::shared_ptr<lldb_private::Section> SectionSP;
 typedef std::unique_ptr<lldb_private::SectionList> SectionListUP;
 typedef std::weak_ptr<lldb_private::Section> SectionWP;
diff --git a/lldb/source/API/SBTypeSummary.cpp b/lldb/source/API/SBTypeSummary.cpp
index 58ec068ab9600..a424394d2768f 100644
--- a/lldb/source/API/SBTypeSummary.cpp
+++ b/lldb/source/API/SBTypeSummary.cpp
@@ -135,6 +135,17 @@ SBTypeSummary SBTypeSummary::CreateWithScriptCode(const char *data,
       TypeSummaryImplSP(new ScriptSummaryFormat(options, "", data)));
 }
 
+SBTypeSummary SBTypeSummary::CreateWithClassName(const char *data,
+                                                 uint32_t options) {
+  LLDB_INSTRUMENT_VA(data, options);
+
+  if (!data || data[0] == 0)
+    return SBTypeSummary();
+
+  return SBTypeSummary(
+      TypeSummaryImplSP(new ScriptedSummaryFormat(options, data)));
+}
+
 SBTypeSummary SBTypeSummary::CreateWithCallback(FormatCallback cb,
                                                 uint32_t options,
                                                 const char *description) {
@@ -372,6 +383,16 @@ bool SBTypeSummary::IsEqualTo(lldb::SBTypeSummary &rhs) {
     return GetOptions() == rhs.GetOptions();
   case TypeSummaryImpl::Kind::eInternal:
     return (m_opaque_sp.get() == rhs.m_opaque_sp.get());
+  case TypeSummaryImpl::Kind::eScriptedClass: {
+    ScriptedSummaryFormat *lhs_ptr =
+        llvm::dyn_cast<ScriptedSummaryFormat>(m_opaque_sp.get());
+    ScriptedSummaryFormat *rhs_ptr =
+        llvm::dyn_cast<ScriptedSummaryFormat>(rhs.m_opaque_sp.get());
+    if (!lhs_ptr || !rhs_ptr)
+      return false;
+    return strcmp(lhs_ptr->GetClassName(), rhs_ptr->GetClassName()) == 0 &&
+           GetOptions() == rhs.GetOptions();
+  }
   }
 
   return false;
@@ -417,6 +438,10 @@ bool SBTypeSummary::CopyOnWrite_Impl() {
                  llvm::dyn_cast<StringSummaryFormat>(m_opaque_sp.get())) {
     new_sp = TypeSummaryImplSP(new StringSummaryFormat(
         GetOptions(), current_summary_ptr->GetSummaryString()));
+  } else if (ScriptedSummaryFormat *current_summary_ptr =
+                 llvm::dyn_cast<ScriptedSummaryFormat>(m_opaque_sp.get())) {
+    new_sp = TypeSummaryImplSP(new ScriptedSummaryFormat(
+        GetOptions(), current_summary_ptr->GetClassName()));
   }
 
   SetSP(new_sp);
diff --git a/lldb/source/Commands/CommandObjectType.cpp b/lldb/source/Commands/CommandObjectType.cpp
index c1dc949a7b815..ed843a6ebbaca 100644
--- a/lldb/source/Commands/CommandObjectType.cpp
+++ b/lldb/source/Commands/CommandObjectType.cpp
@@ -21,6 +21,7 @@
 #include "lldb/Interpreter/CommandReturnObject.h"
 #include "lldb/Interpreter/OptionArgParser.h"
 #include "lldb/Interpreter/OptionGroupFormat.h"
+#include "lldb/Interpreter/OptionGroupPythonClassWithDict.h"
 #include "lldb/Interpreter/OptionValueBoolean.h"
 #include "lldb/Interpreter/OptionValueLanguage.h"
 #include "lldb/Interpreter/OptionValueString.h"
@@ -123,9 +124,9 @@ const char *FormatCategoryToString(FormatCategoryItem item, bool long_name) {
 class CommandObjectTypeSummaryAdd : public CommandObjectParsed,
                                     public IOHandlerDelegateMultiline {
 private:
-  class CommandOptions : public Options {
+  class CommandOptions : public OptionGroup {
   public:
-    CommandOptions(CommandInterpreter &interpreter) {}
+    CommandOptions() = default;
 
     ~CommandOptions() override = default;
 
@@ -151,12 +152,16 @@ class CommandObjectTypeSummaryAdd : public CommandObjectParsed,
     uint32_t m_ptr_match_depth = 1;
   };
 
+  OptionGroupOptions m_option_group;
   CommandOptions m_options;
+  OptionGroupPythonClassWithDict m_class_options;
 
-  Options *GetOptions() override { return &m_options; }
+  Options *GetOptions() override { return &m_option_group; }
 
   bool Execute_ScriptSummary(Args &command, CommandReturnObject &result);
 
+  bool Execute_PythonClassSummary(Args &command, CommandReturnObject &result);
+
   bool Execute_StringSummary(Args &command, CommandReturnObject &result);
 
 public:
@@ -1163,7 +1168,7 @@ Status CommandObjectTypeSummaryAdd::CommandOptions::SetOptionValue(
     uint32_t option_idx, llvm::StringRef option_arg,
     ExecutionContext *execution_context) {
   Status error;
-  const int short_option = m_getopt_table[option_idx].val;
+  const int short_option = g_type_summary_add_options[option_idx].short_option;
   bool success;
 
   switch (short_option) {
@@ -1374,6 +1379,48 @@ bool CommandObjectTypeSummaryAdd::Execute_ScriptSummary(
   return result.Succeeded();
 }
 
+bool CommandObjectTypeSummaryAdd::Execute_PythonClassSummary(
+    Args &command, CommandReturnObject &result) {
+  const size_t argc = command.GetArgumentCount();
+
+  if (argc < 1 && !m_options.m_name) {
+    result.AppendErrorWithFormat("%s takes one or more args",
+                                 m_cmd_name.c_str());
+    return false;
+  }
+
+  const std::string &class_name = m_class_options.GetName();
+  if (class_name.empty()) {
+    result.AppendError("must provide a Python class name");
+    return false;
+  }
+
+  TypeSummaryImplSP script_format = std::make_shared<ScriptedSummaryFormat>(
+      m_options.m_flags, class_name.c_str(), m_options.m_ptr_match_depth);
+
+  Status error;
+
+  for (auto &entry : command.entries()) {
+    AddSummary(ConstString(entry.ref()), script_format, m_options.m_match_type,
+               m_options.m_category, &error);
+    if (error.Fail()) {
+      result.AppendError(error.AsCString());
+      return false;
+    }
+  }
+
+  if (m_options.m_name) {
+    AddNamedSummary(m_options.m_name, script_format, &error);
+    if (error.Fail()) {
+      result.AppendError(error.AsCString());
+      result.AppendError("added to types, but not given a name");
+      return false;
+    }
+  }
+
+  return result.Succeeded();
+}
+
 #endif
 
 bool CommandObjectTypeSummaryAdd::Execute_StringSummary(
@@ -1450,7 +1497,14 @@ CommandObjectTypeSummaryAdd::CommandObjectTypeSummaryAdd(
     CommandInterpreter &interpreter)
     : CommandObjectParsed(interpreter, "type summary add",
                           "Add a new summary style for a type.", nullptr),
-      IOHandlerDelegateMultiline("DONE"), m_options(interpreter) {
+      IOHandlerDelegateMultiline("DONE"),
+      m_class_options("scripted string summary", /*is_class=*/true, 'L', 'K',
+                      'V', /*required_options=*/0) {
+  m_option_group.Append(&m_options);
+  m_option_group.Append(&m_class_options, LLDB_OPT_SET_1 | LLDB_OPT_SET_2,
+                        LLDB_OPT_SET_ALL);
+  m_option_group.Finalize();
+
   AddSimpleArgumentList(eArgTypeName, eArgRepeatPlus);
 
   SetHelpLong(
@@ -1553,7 +1607,13 @@ void CommandObjectTypeSummaryAdd::DoExecute(Args &command,
                                             CommandReturnObject &result) {
   WarnOnPotentialUnquotedUnsignedType(command, result);
 
-  if (m_options.m_is_add_script) {
+  if (!m_class_options.GetName().empty()) {
+#if LLDB_ENABLE_PYTHON
+    Execute_PythonClassSummary(command, result);
+#else
+    result.AppendError("python is disabled");
+#endif
+  } else if (m_options.m_is_add_script) {
 #if LLDB_ENABLE_PYTHON
     Execute_ScriptSummary(command, result);
 #else
diff --git a/lldb/source/DataFormatters/TypeSummary.cpp b/lldb/source/DataFormatters/TypeSummary.cpp
index 4f01466f85148..73691bb53570f 100644
--- a/lldb/source/DataFormatters/TypeSummary.cpp
+++ b/lldb/source/DataFormatters/TypeSummary.cpp
@@ -16,6 +16,8 @@
 #include "lldb/Core/Debugger.h"
 #include "lldb/DataFormatters/ValueObjectPrinter.h"
 #include "lldb/Interpreter/CommandInterpreter.h"
+#include "lldb/Interpreter/Interfaces/ScriptedStringSummaryInterface.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Symbol/CompilerType.h"
 #include "lldb/Target/StackFrame.h"
 #include "lldb/Target/Target.h"
@@ -60,6 +62,8 @@ std::string TypeSummaryImpl::GetSummaryKindName() {
     return "c++";
   case Kind::eBytecode:
     return "bytecode";
+  case Kind::eScriptedClass:
+    return "python class";
   }
   llvm_unreachable("Unknown type kind name");
 }
@@ -242,6 +246,77 @@ std::string ScriptSummaryFormat::GetDescription() {
 
 std::string ScriptSummaryFormat::GetName() { return m_script_formatter_name; }
 
+ScriptedSummaryFormat::ScriptedSummaryFormat(
+    const TypeSummaryImpl::Flags &flags, const char *class_name,
+    uint32_t ptr_match_depth)
+    : TypeSummaryImpl(Kind::eScriptedClass, flags, ptr_match_depth),
+      m_class_name(class_name ? class_name : ""), m_interface_sp() {}
+
+bool ScriptedSummaryFormat::FormatObject(ValueObject *valobj,
+                                         std::string &retval,
+                                         const TypeSummaryOptions &options) {
+  if (!valobj)
+    return false;
+
+  TargetSP target_sp(valobj->GetTargetSP());
+
+  if (!target_sp) {
+    retval.assign("error: no target");
+    return false;
+  }
+
+  ScriptInterpreter *script_interpreter =
+      target_sp->GetDebugger().GetScriptInterpreter();
+
+  if (!script_interpreter) {
+    retval.assign("error: no ScriptInterpreter");
+    return false;
+  }
+
+  if (!m_interface_sp) {
+    m_interface_sp = script_interpreter->CreateScriptedStringSummaryInterface();
+    if (!m_interface_sp) {
+      retval.assign("error: no ScriptedStringSummaryInterface");
+      return false;
+    }
+
+    llvm::Expected<StructuredData::GenericSP> obj_or_err =
+        m_interface_sp->CreatePluginObject(m_class_name);
+    if (!obj_or_err) {
+      retval.assign(llvm::toString(obj_or_err.takeError()));
+      m_interface_sp.reset();
+      return false;
+    }
+  }
+
+  llvm::Expected<std::string> summary =
+      m_interface_sp->GetSummary(*valobj, options);
+  if (!summary) {
+    retval.assign(llvm::toString(summary.takeError()));
+    return false;
+  }
+
+  retval = std::move(*summary);
+  return true;
+}
+
+std::string ScriptedSummaryFormat::GetDescription() {
+  StreamString sstr;
+  sstr.Printf("%s%s%s%s%s%s%s ptr-match-depth=%u\n  ",
+              Cascades() ? "" : " (not cascading)",
+              !DoesPrintChildren(nullptr) ? "" : " (show children)",
+              !DoesPrintValue(nullptr) ? " (hide value)" : "",
+              IsOneLiner() ? " (one-line printout)" : "",
+              SkipsPointers() ? " (skip pointers)" : "",
+              SkipsReferences() ? " (skip references)" : "",
+              HideNames(nullptr) ? " (hide member names)" : "",
+              GetPtrMatchDepth());
+  sstr.PutCString(m_class_name);
+  return std::string(sstr.GetString());
+}
+
+std::string ScriptedSummaryFormat::GetName() { return m_class_name; }
+
 BytecodeSummaryFormat::BytecodeSummaryFormat(
     const TypeSummaryImpl::Flags &flags,
     std::unique_ptr<llvm::MemoryBuffer> bytecode)
diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp
index 04ec3f5421981..2a7580afb8aaa 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -238,6 +238,8 @@ ScriptInterpreter::ExtensionToString(lldb::ScriptedExtension extension) {
     return "ScriptedCommand";
   case eScriptedExtensionParsedCommand:
     return "ParsedCommand";
+  case eScriptedExtensionScriptedStringSummary:
+    return "ScriptedStringSummary";
   }
   llvm_unreachable("unhandled ScriptedExtension");
 }
@@ -260,6 +262,8 @@ ScriptInterpreter::StringToExtension(llvm::StringRef string) {
                  eScriptedExtensionScriptedStackFrameRecognizer)
       .CaseLower("ScriptedCommand", eScriptedExtensionScriptedCommand)
       .CaseLower("ParsedCommand", eScriptedExtensionParsedCommand)
+      .CaseLower("ScriptedStringSummary",
+                 eScriptedExtensionScriptedStringSummary)
       .Default(eScriptedExtensionInvalid);
 }
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
index d3dc5773eb0af..e3f0c570c0312 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
@@ -34,6 +34,7 @@ set(python_plugin_sources
   Interfaces/ScriptedBreakpointPythonInterface.cpp
   Interfaces/ScriptedCommandPythonInterface.cpp
   Interfaces/ScriptedStackFrameRecognizerPythonInterface.cpp
+  Interfaces/ScriptedStringSummaryPythonInterface.cpp
   Interfaces/ScriptedThreadPlanPythonInterface.cpp
   Interfaces/ScriptedThreadPythonInterface.cpp
 )
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
index 2c8e0ef8bbeb0..185951c9e55fa 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
@@ -31,6 +31,7 @@ void ScriptInterpreterPythonInterfaces::Initialize() {
   ScriptedFramePythonInterface::Initialize();
   ScriptedStackFrameRecognizerPythonInterface::Initialize();
   ScriptedCommandPythonInterface::Initialize();
+  ScriptedStringSummaryPythonInterface::Initialize();
 }
 
 void ScriptInterpreterPythonInterfaces::Terminate() {
@@ -45,4 +46,5 @@ void ScriptInterpreterPythonInterfaces::Terminate() {
   ScriptedFramePythonInterface::Terminate();
   ScriptedStackFrameRecognizerPythonInterface::Terminate();
   ScriptedCommandPythonInterface::Terminate();
+  ScriptedStringSummaryPythonInterface::Terminate();
 }
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
index 7ddf7ec0e5ae5..43f2b4c011a18 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
@@ -21,6 +21,7 @@
 #include "ScriptedPlatformPythonInterface.h"
 #include "ScriptedProcessPythonInterface.h"
 #include "ScriptedStackFrameRecognizerPythonInterface.h"
+#include "ScriptedStringSummaryPythonInterface.h"
 #include "ScriptedThreadPlanPythonInterface.h"
 #include "ScriptedThreadPythonInterface.h"
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index d82a59738a7db..24cae7317b746 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -685,6 +685,10 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     return python::SWIGBridge::ToSWIGWrapper(*arg);
   }
 
+  python::PythonObject Transform(const TypeSummaryOptions &arg) {
+    return python::SWIGBridge::ToSWIGWrapper(arg);
+  }
+
   template <typename T, typename U>
   void ReverseTransform(T &original_arg, U transformed_arg, Status &error) {
     // If U is not a PythonObject, don't touch it!
@@ -696,6 +700,14 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     original_arg = ExtractValueFromPythonObject<T>(transformed_arg, error);
   }
 
+  // Read-only arguments (passed as `const T&`) have nothing to write back:
+  // there's no `T` value to reassign into a const reference, and no
+  // `ExtractValueFromPythonObject<T>` specialization should be required just
+  // to satisfy this round-trip for a value the callee never mutates.
+  template <typename T>
+  void ReverseTransform(const T &original_arg,
+                        python::PythonObject transformed_arg, Status &error) {}
+
   void ReverseTransform(bool &original_arg,
                         python::PythonObject transformed_arg, Status &error) {
     python::PythonBoolean boolean_arg = python::PythonBoolean(
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedStringSummaryPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedStringSummaryPythonInterface.cpp
new file mode 100644
index 0000000000000..b0efa7615af96
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedStringSummaryPythonInterface.cpp
@@ -0,0 +1,61 @@
+//===----------------------------------------------------------------------===//
+//
+// 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-python.h"
+
+#include "lldb/Core/PluginManager.h"
+#include "lldb/DataFormatters/TypeSummary.h"
+#include "lldb/ValueObject/ValueObject.h"
+#include "lldb/lldb-enumerations.h"
+
+#include "../ScriptInterpreterPythonImpl.h"
+#include "ScriptedStringSummaryPythonInterface.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+ScriptedStringSummaryPythonInterface::ScriptedStringSummaryPythonInterface(
+    ScriptInterpreterPythonImpl &interpreter)
+    : ScriptedStringSummaryInterface(), ScriptedPythonInterface(interpreter) {}
+
+llvm::Expected<StructuredData::GenericSP>
+ScriptedStringSummaryPythonInterface::CreatePluginObject(
+    llvm::StringRef class_name) {
+  if (class_name.empty())
+    return llvm::createStringError("empty class name");
+
+  return ScriptedPythonInterface::CreatePluginObject(
+      ScriptedMetadata(class_name, nullptr), nullptr);
+}
+
+llvm::Expected<std::string> ScriptedStringSummaryPythonInterface::GetSummary(
+    ValueObject &valobj, const TypeSummaryOptions &options) {
+  Status error;
+  StructuredData::ObjectSP obj =
+      Dispatch("get_summary", error, valobj.GetSP(), options);
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return error.ToError();
+  return obj->GetStringValue().str();
+}
+
+void ScriptedStringSummaryPythonInterface::Initialize() {
+  const std::vector<llvm::StringRef> ci_usages = {
+      "type summary add -L <ClassName> [-K <key> -V <value> ...] <TypeName>"};
+  const std::vector<llvm::StringRef> api_usages = {
+      "SBTypeSummary.CreateWithClassName"};
+  PluginManager::RegisterPlugin(
+      GetPluginNameStatic(),
+      "Provide a summary string for a type, used by 'type summary add -l'",
+      CreateInstance, eScriptedExtensionScriptedStringSummary,
+      eScriptLanguagePython, {ci_usages, api_usages});
+}
+
+void ScriptedStringSummaryPythonInterface::Terminate() {
+  PluginManager::UnregisterPlugin(CreateInstance);
+}
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedStringSummaryPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedStringSummaryPythonInterface.h
new file mode 100644
index 0000000000000..0968ff8c9c403
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedStringSummaryPythonInterface.h
@@ -0,0 +1,48 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDSTRINGSUMMARYPYTHONINTERFACE_H
+#define LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDSTRINGSUMMARYPYTHONINTERFACE_H
+
+#include "lldb/Interpreter/Interfaces/ScriptedStringSummaryInterface.h"
+
+#include "ScriptedPythonInterface.h"
+namespace lldb_private {
+
+class ScriptedStringSummaryPythonInterface
+    : public ScriptedStringSummaryInterface,
+      public ScriptedPythonInterface,
+      public PluginInterface {
+public:
+  ScriptedStringSummaryPythonInterface(
+      ScriptInterpreterPythonImpl &interpreter);
+
+  llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name) override;
+
+  llvm::SmallVector<AbstractMethodRequirement>
+  GetAbstractMethodRequirements() const override {
+    return llvm::SmallVector<AbstractMethodRequirement>({{"get_summary", 3}});
+  }
+
+  llvm::Expected<std::string>
+  GetSummary(ValueObject &valobj, const TypeSummaryOptions &options) override;
+
+  static void Initialize();
+
+  static void Terminate();
+
+  static llvm::StringRef GetPluginNameStatic() {
+    return "ScriptedStringSummaryPythonInterface";
+  }
+
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+};
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDSTRINGSUMMARYPYTHONINTERFACE_H
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index bb8871e234bc1..81e6a656629bf 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -292,6 +292,8 @@ llvm::Expected<std::string> ScriptInterpreterPython::ExtensionToImportPath(
   case eScriptedExtensionScriptedCommand:
   case eScriptedExtensionParsedCommand:
     return "lldb.plugins.scripted_command";
+  case eScriptedExtensionScriptedStringSummary:
+    return "lldb.plugins.scripted_string_summary";
   case eScriptedExtensionInvalid:
     return llvm::createStringError("invalid extension name");
   }
@@ -2011,6 +2013,11 @@ ScriptInterpreterPythonImpl::CreateScriptedCommandInterface() {
   return std::make_shared<ScriptedCommandPythonInterface>(*this);
 }
 
+ScriptedStringSummaryInterfaceSP
+ScriptInterpreterPythonImpl::CreateScriptedStringSummaryInterface() {
+  return std::make_shared<ScriptedStringSummaryPythonInterface>(*this);
+}
+
 ScriptedThreadInterfaceSP
 ScriptInterpreterPythonImpl::CreateScriptedThreadInterface() {
   return std::make_shared<ScriptedThreadPythonInterface>(*this);
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
index 51b373afbb9f4..bf7f677401b12 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
@@ -85,6 +85,9 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
 
   lldb::ScriptedCommandInterfaceSP CreateScriptedCommandInterface() override;
 
+  lldb::ScriptedStringSummaryInterfaceSP
+  CreateScriptedStringSummaryInterface() override;
+
   lldb::ScriptedThreadInterfaceSP CreateScriptedThreadInterface() override;
 
   lldb::ScriptedFrameInterfaceSP CreateScriptedFrameInterface() override;

>From 219bcbe8e0e0b45a1ae9e50ef741b45ce2f742b8 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Thu, 30 Jul 2026 13:39:36 -0700
Subject: [PATCH 04/16] [lldb/script] Migrate synthetic children providers onto
 ScriptedPythonInterface (#210845)

Give `type synthetic add -l` a formal
`ScriptedSyntheticChildrenInterface`, matching the architecture used
elsewhere in this series: a C++ interface header, a Python-backed
implementation, `PluginManager` registration with CLI/API usages, and a
generatable ABC template (`scripted_synthetic_children.py`) wired into
`scripting extension generate`.

Every method goes through the shared `Dispatch<T>()` machinery instead
of hand-rolling its own Locker/raw-SWIG calls. `Dispatch<T>()` is taught
to introspect the target method's arity via
`PythonCallable::GetArgInfo()` and drop trailing args before calling, so
providers that legitimately define an argument as optional
(`num_children(self)` vs. `num_children(self, max_count)`) still work
through the generic dispatch path.

This retires the ad-hoc `LLDBSwigPython_*` synthetic-children bridge
functions entirely.

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
(cherry picked from commit b05a5d09547bea535b4c26ee522ce310db118c47)
---
 lldb/bindings/python/CMakeLists.txt           |   1 +
 lldb/bindings/python/python-swigsafecast.swig |   7 +
 lldb/bindings/python/python-wrapper.swig      | 163 +-----------
 lldb/docs/CMakeLists.txt                      |   1 +
 .../templates/scripted_synthetic_children.py  | 112 ++++++++
 lldb/include/lldb/API/SBPlatform.h            |   1 -
 .../lldb/DataFormatters/TypeSynthetic.h       |   3 +-
 .../ScriptedSyntheticChildrenInterface.h      |  44 ++++
 .../lldb/Interpreter/ScriptInterpreter.h      |  50 +---
 lldb/include/lldb/lldb-enumerations.h         |   3 +-
 lldb/include/lldb/lldb-forward.h              |   3 +
 lldb/source/DataFormatters/TypeSynthetic.cpp  |  59 +++--
 lldb/source/Interpreter/ScriptInterpreter.cpp |   6 +-
 .../ScriptInterpreter/Python/CMakeLists.txt   |   1 +
 .../ScriptInterpreterPythonInterfaces.cpp     |   2 +
 .../ScriptInterpreterPythonInterfaces.h       |   1 +
 .../Interfaces/ScriptedPythonInterface.h      |  95 ++++---
 ...riptedSyntheticChildrenPythonInterface.cpp | 144 +++++++++++
 ...ScriptedSyntheticChildrenPythonInterface.h |  63 +++++
 .../Python/SWIGPythonBridge.h                 |  26 +-
 .../Python/ScriptInterpreterPython.cpp        | 240 +-----------------
 .../Python/ScriptInterpreterPythonImpl.h      |  30 +--
 .../Python/PythonTestSuite.cpp                |  37 +--
 23 files changed, 506 insertions(+), 586 deletions(-)
 create mode 100644 lldb/examples/python/templates/scripted_synthetic_children.py
 create mode 100644 lldb/include/lldb/Interpreter/Interfaces/ScriptedSyntheticChildrenInterface.h
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSyntheticChildrenPythonInterface.cpp
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSyntheticChildrenPythonInterface.h

diff --git a/lldb/bindings/python/CMakeLists.txt b/lldb/bindings/python/CMakeLists.txt
index d2761528c6493..77b8902091923 100644
--- a/lldb/bindings/python/CMakeLists.txt
+++ b/lldb/bindings/python/CMakeLists.txt
@@ -122,6 +122,7 @@ function(finish_swig_python swig_target lldb_python_bindings_dir lldb_python_tar
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_stackframe_recognizer.py"
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_command.py"
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_string_summary.py"
+    "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_synthetic_children.py"
     )
 
   if(APPLE)
diff --git a/lldb/bindings/python/python-swigsafecast.swig b/lldb/bindings/python/python-swigsafecast.swig
index a86dc44ce4106..c5003c019aae2 100644
--- a/lldb/bindings/python/python-swigsafecast.swig
+++ b/lldb/bindings/python/python-swigsafecast.swig
@@ -17,6 +17,13 @@ PythonObject SWIGBridge::ToSWIGWrapper(lldb::ValueObjectSP value_sp) {
   return ToSWIGWrapper(std::unique_ptr<lldb::SBValue>(new lldb::SBValue(value_sp)));
 }
 
+PythonObject SWIGBridge::ToSWIGWrapper(lldb::ValueObjectSP value_sp,
+                                       bool use_synthetic) {
+  auto sb_value = std::unique_ptr<lldb::SBValue>(new lldb::SBValue(value_sp));
+  sb_value->SetPreferSyntheticValue(use_synthetic);
+  return ToSWIGWrapper(std::move(sb_value));
+}
+
 PythonObject SWIGBridge::ToSWIGWrapper(lldb::TargetSP target_sp) {
   return ToSWIGHelper(new lldb::SBTarget(std::move(target_sp)),
                       SWIGTYPE_p_lldb__SBTarget);
diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig
index c4071b3f7b5b1..868c172880c7d 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -181,15 +181,14 @@ bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallTypeScript(
   return true;
 }
 
-PythonObject lldb_private::python::SWIGBridge::LLDBSwigPythonCreateSyntheticProvider(
+PythonObject lldb_private::python::SWIGBridge::LLDBSwigPythonCreateCommandObject(
     const char *python_class_name, const char *session_dictionary_name,
-    const lldb::ValueObjectSP &valobj_sp) {
+    lldb::DebuggerSP debugger_sp) {
   if (python_class_name == NULL || python_class_name[0] == '\0' ||
       !session_dictionary_name)
     return PythonObject();
 
   PyErr_Cleaner py_err_cleaner(true);
-
   auto dict = PythonModule::MainModule().ResolveName<PythonDictionary>(
       session_dictionary_name);
   auto pfunc = PythonObject::ResolveNameWithDictionary<PythonCallable>(
@@ -198,19 +197,7 @@ PythonObject lldb_private::python::SWIGBridge::LLDBSwigPythonCreateSyntheticProv
   if (!pfunc.IsAllocated())
     return PythonObject();
 
-  auto sb_value = std::unique_ptr<lldb::SBValue>(new lldb::SBValue(valobj_sp));
-  sb_value->SetPreferSyntheticValue(false);
-
-  PythonObject val_arg = SWIGBridge::ToSWIGWrapper(std::move(sb_value));
-  if (!val_arg.IsAllocated())
-    return PythonObject();
-
-  PythonObject result = pfunc(val_arg, dict);
-
-  if (result.IsAllocated())
-    return result;
-
-  return PythonObject();
+  return pfunc(SWIGBridge::ToSWIGWrapper(std::move(debugger_sp)), dict);
 }
 
 // wrapper that calls an optional instance member of an object taking no
@@ -237,150 +224,6 @@ static PyObject *LLDBSwigPython_CallOptionalMember(
   return result.release();
 }
 
-size_t lldb_private::python::SWIGBridge::LLDBSwigPython_CalculateNumChildren(PyObject * implementor,
-                                                         uint32_t max) {
-  PythonObject self(PyRefType::Borrowed, implementor);
-  auto pfunc = self.ResolveName<PythonCallable>("num_children");
-
-  if (!pfunc.IsAllocated())
-    return 0;
-
-  auto arg_info = pfunc.GetArgInfo();
-  if (!arg_info) {
-    llvm::consumeError(arg_info.takeError());
-    return 0;
-  }
-
-  size_t ret_val;
-  if (arg_info.get().max_positional_args < 1)
-    ret_val = unwrapOrSetPythonException(As<long long>(pfunc.Call()));
-  else
-    ret_val = unwrapOrSetPythonException(
-        As<long long>(pfunc.Call(PythonInteger(max))));
-
-  if (PyErr_Occurred()) {
-    PyErr_Print();
-    PyErr_Clear();
-    return 0;
-  }
-
-  if (arg_info.get().max_positional_args < 1)
-    ret_val = std::min(ret_val, static_cast<size_t>(max));
-
-  return ret_val;
-}
-
-PyObject *lldb_private::python::SWIGBridge::LLDBSwigPython_GetChildAtIndex(PyObject * implementor,
-                                                       uint32_t idx) {
-  PyErr_Cleaner py_err_cleaner(true);
-
-  PythonObject self(PyRefType::Borrowed, implementor);
-  auto pfunc = self.ResolveName<PythonCallable>("get_child_at_index");
-
-  if (!pfunc.IsAllocated())
-    return nullptr;
-
-  PythonObject result = pfunc(PythonInteger(idx));
-
-  if (!result.IsAllocated())
-    return nullptr;
-
-  lldb::SBValue *sbvalue_ptr = nullptr;
-  if (SWIG_ConvertPtr(result.get(), (void **)&sbvalue_ptr,
-                      SWIGTYPE_p_lldb__SBValue, 0) == -1)
-    return nullptr;
-
-  if (sbvalue_ptr == nullptr)
-    return nullptr;
-
-  return result.release();
-}
-
-uint32_t lldb_private::python::SWIGBridge::LLDBSwigPython_GetIndexOfChildWithName(
-    PyObject * implementor, const char *child_name) {
-  PyErr_Cleaner py_err_cleaner(true);
-
-  PythonObject self(PyRefType::Borrowed, implementor);
-  auto pfunc = self.ResolveName<PythonCallable>("get_child_index");
-
-  if (!pfunc.IsAllocated())
-    return UINT32_MAX;
-
-  llvm::Expected<PythonObject> result = pfunc.Call(PythonString(child_name));
-
-  long long retval =
-      unwrapOrSetPythonException(As<long long>(std::move(result)));
-
-  if (PyErr_Occurred()) {
-    PyErr_Clear(); // FIXME print this? do something else
-    return UINT32_MAX;
-  }
-
-  if (retval >= 0)
-    return (uint32_t)retval;
-
-  return UINT32_MAX;
-}
-
-bool lldb_private::python::SWIGBridge::LLDBSwigPython_UpdateSynthProviderInstance(PyObject *
-                                                              implementor) {
-  bool ret_val = false;
-
-  static char callee_name[] = "update";
-
-  PyObject *py_return =
-      LLDBSwigPython_CallOptionalMember(implementor, callee_name);
-
-  if (py_return == Py_True)
-    ret_val = true;
-
-  Py_XDECREF(py_return);
-
-  return ret_val;
-}
-
-bool lldb_private::python::SWIGBridge::LLDBSwigPython_MightHaveChildrenSynthProviderInstance(
-    PyObject * implementor) {
-  bool ret_val = false;
-
-  static char callee_name[] = "has_children";
-
-  PyObject *py_return =
-      LLDBSwigPython_CallOptionalMember(implementor, callee_name, Py_True);
-
-  if (py_return == Py_True)
-    ret_val = true;
-
-  Py_XDECREF(py_return);
-
-  return ret_val;
-}
-
-PyObject *lldb_private::python::SWIGBridge::LLDBSwigPython_GetValueSynthProviderInstance(
-    PyObject * implementor) {
-  PyObject *ret_val = nullptr;
-
-  static char callee_name[] = "get_value";
-
-  PyObject *py_return =
-      LLDBSwigPython_CallOptionalMember(implementor, callee_name, Py_None);
-
-  if (py_return == Py_None || py_return == nullptr)
-    ret_val = nullptr;
-
-  lldb::SBValue *sbvalue_ptr = NULL;
-
-  if (SWIG_ConvertPtr(py_return, (void **)&sbvalue_ptr,
-                      SWIGTYPE_p_lldb__SBValue, 0) == -1)
-    ret_val = nullptr;
-  else if (sbvalue_ptr == NULL)
-    ret_val = nullptr;
-  else
-    ret_val = py_return;
-
-  Py_XDECREF(py_return);
-  return ret_val;
-}
 
 void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBData(PyObject * data) {
   lldb::SBData *sb_ptr = nullptr;
diff --git a/lldb/docs/CMakeLists.txt b/lldb/docs/CMakeLists.txt
index 5357db6e652a1..1b172b3c8564e 100644
--- a/lldb/docs/CMakeLists.txt
+++ b/lldb/docs/CMakeLists.txt
@@ -35,6 +35,7 @@ if (LLDB_ENABLE_PYTHON AND SPHINX_FOUND)
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_stackframe_recognizer.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_command.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_string_summary.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
+      COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_synthetic_children.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMENT "Copying lldb.py to pretend its a Python package.")
 
     add_dependencies(lldb-python-doc-package swig_wrapper_python)
diff --git a/lldb/examples/python/templates/scripted_synthetic_children.py b/lldb/examples/python/templates/scripted_synthetic_children.py
new file mode 100644
index 0000000000000..8705c02ad566f
--- /dev/null
+++ b/lldb/examples/python/templates/scripted_synthetic_children.py
@@ -0,0 +1,112 @@
+from abc import ABCMeta, abstractmethod
+from typing import Optional
+
+import lldb
+
+
+class ScriptedSyntheticChildren(metaclass=ABCMeta):
+    """
+    The base class for a scripted synthetic children provider.
+
+    A synthetic children provider allows you to customize how a value is
+    expanded into children when displayed (e.g. `frame variable`, `bt`).
+    Register it with `type synthetic add -l <ClassName> ...`.
+
+    Most of the base class methods are `@abstractmethod` that need to be
+    overwritten by the inheriting class.
+    """
+
+    valobj: lldb.SBValue
+
+    def __init__(self, valobj: lldb.SBValue):
+        """Construct a scripted synthetic children provider.
+
+        Args:
+            valobj (lldb.SBValue): The value this provider generates children
+                for.
+        """
+        self.valobj = valobj
+
+    @abstractmethod
+    def num_children(self) -> int:
+        """The number of children this value has.
+
+        This can optionally take a second `max_count` parameter (i.e.
+        `def num_children(self, max_count)`) if computing the exact count is
+        expensive; in that case return `max_count` once at least that many
+        children are known to exist.
+
+        Returns:
+            int: The number of children.
+        """
+        pass
+
+    @abstractmethod
+    def get_child_at_index(self, index: int) -> Optional[lldb.SBValue]:
+        """Get the child at the given index.
+
+        Args:
+            index (int): The index of the child to return.
+
+        Returns:
+            lldb.SBValue: The value for the child at this index, or `None` if
+            there is no child at this index.
+        """
+        pass
+
+    def get_child_index(self, name: str) -> Optional[int]:
+        """Get the index of the child with the given name.
+
+        Args:
+            name (str): The name of the child to look up.
+
+        Returns:
+            int: The index of the child with this name, or `None`/a negative
+            value if no such child exists. Defaults to a linear search over
+            `get_child_at_index`/`num_children`.
+        """
+        pass
+
+    def update(self) -> bool:
+        """Called when the value backing this provider may have changed
+        (e.g. after a `continue`), giving the provider a chance to refresh
+        any cached state.
+
+        Returns:
+            bool: `True` if the previously computed children can be reused,
+            `False` if they should be recomputed. Defaults to `False`.
+        """
+        return False
+
+    def has_children(self) -> bool:
+        """Whether this value might have children, without necessarily
+        computing them. Used as a cheap check to decide whether to show an
+        expansion arrow in graphical frontends, for example.
+
+        Returns:
+            bool: `True` if this value might have children, `False`
+            otherwise. Defaults to `True`.
+        """
+        return True
+
+    def get_value(self) -> Optional[lldb.SBValue]:
+        """Make this a value-providing synthetic children provider: the
+        value returned here becomes the value for this `SBValue`, in place
+        of the value backing it. None of the other methods on this class
+        (`num_children`, `get_child_at_index`, `get_child_index`) are
+        consulted, and the children of the original value are not shown.
+
+        Returns:
+            lldb.SBValue: The value to use instead of this value's own,
+            or `None` to leave this value unaffected. Defaults to `None`.
+        """
+        return None
+
+    def get_type_name(self) -> Optional[str]:
+        """Override the type name shown for this synthetic value.
+
+        Returns:
+            str: The type name to display, or `None`/empty to keep the
+            default. Defaults to `None`.
+        """
+        pass
diff --git a/lldb/include/lldb/API/SBPlatform.h b/lldb/include/lldb/API/SBPlatform.h
index 7cb19b3f7ec46..e231fda70e816 100644
--- a/lldb/include/lldb/API/SBPlatform.h
+++ b/lldb/include/lldb/API/SBPlatform.h
@@ -17,7 +17,6 @@
 
 struct PlatformConnectOptions;
 struct PlatformShellCommand;
-class ProcessInstanceInfoMatch;
 
 namespace lldb {
 
diff --git a/lldb/include/lldb/DataFormatters/TypeSynthetic.h b/lldb/include/lldb/DataFormatters/TypeSynthetic.h
index ae44d7e8f96eb..2194696fa738c 100644
--- a/lldb/include/lldb/DataFormatters/TypeSynthetic.h
+++ b/lldb/include/lldb/DataFormatters/TypeSynthetic.h
@@ -473,8 +473,7 @@ class ScriptedSyntheticChildren : public SyntheticChildren {
 
   private:
     std::string m_python_class;
-    StructuredData::ObjectSP m_wrapper_sp;
-    ScriptInterpreter *m_interpreter;
+    lldb::ScriptedSyntheticChildrenInterfaceSP m_interface_sp;
 
     FrontEnd(const FrontEnd &) = delete;
     const FrontEnd &operator=(const FrontEnd &) = delete;
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedSyntheticChildrenInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedSyntheticChildrenInterface.h
new file mode 100644
index 0000000000000..c73018c779e1e
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedSyntheticChildrenInterface.h
@@ -0,0 +1,44 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_INTERFACES_SCRIPTEDSYNTHETICCHILDRENINTERFACE_H
+#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDSYNTHETICCHILDRENINTERFACE_H
+
+#include "ScriptedInterface.h"
+#include "lldb/lldb-private.h"
+#include "llvm/Support/ErrorExtras.h"
+
+namespace lldb_private {
+class ScriptedSyntheticChildrenInterface : virtual public ScriptedInterface {
+public:
+  virtual llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, ValueObject &backend) = 0;
+
+  virtual llvm::Expected<uint32_t> CalculateNumChildren(uint32_t max) {
+    return 0;
+  }
+
+  virtual lldb::ValueObjectSP GetChildAtIndex(uint32_t idx) {
+    return lldb::ValueObjectSP();
+  }
+
+  virtual llvm::Expected<uint32_t> GetIndexOfChildWithName(ConstString name) {
+    return llvm::createStringErrorV("type has no child named '{0}'", name);
+  }
+
+  virtual lldb::ChildCacheState Update() { return lldb::eRefetch; }
+
+  virtual bool MightHaveChildren() { return true; }
+
+  virtual lldb::ValueObjectSP GetSyntheticValue() { return nullptr; }
+
+  virtual ConstString GetSyntheticTypeName() { return ConstString(); }
+};
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDSYNTHETICCHILDRENINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 448091e0ceb85..7df5055cfea86 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -12,7 +12,9 @@
 #include "lldb/API/SBAttachInfo.h"
 #include "lldb/API/SBBreakpoint.h"
 #include "lldb/API/SBBreakpointLocation.h"
+#include "lldb/API/SBCommandReturnObject.h"
 #include "lldb/API/SBData.h"
+#include "lldb/API/SBDebugger.h"
 #include "lldb/API/SBError.h"
 #include "lldb/API/SBEvent.h"
 #include "lldb/API/SBExecutionContext.h"
@@ -257,12 +259,6 @@ class ScriptInterpreter : public PluginInterface {
     return false;
   }
 
-  virtual StructuredData::ObjectSP
-  CreateSyntheticScriptedProvider(const char *class_name,
-                                  lldb::ValueObjectSP valobj) {
-    return StructuredData::ObjectSP();
-  }
-
   virtual StructuredData::ObjectSP
   LoadPluginModule(const FileSpec &file_spec, lldb_private::Status &error) {
     return StructuredData::ObjectSP();
@@ -343,43 +339,6 @@ class ScriptInterpreter : public PluginInterface {
     // Clean up any ref counts to SBObjects that might be in global variables
   }
 
-  virtual size_t
-  CalculateNumChildren(const StructuredData::ObjectSP &implementor,
-                       uint32_t max) {
-    return 0;
-  }
-
-  virtual lldb::ValueObjectSP
-  GetChildAtIndex(const StructuredData::ObjectSP &implementor, uint32_t idx) {
-    return lldb::ValueObjectSP();
-  }
-
-  virtual llvm::Expected<uint32_t>
-  GetIndexOfChildWithName(const StructuredData::ObjectSP &implementor,
-                          const char *child_name) {
-    return llvm::createStringError("Type has no child named '%s'", child_name);
-  }
-
-  virtual bool
-  UpdateSynthProviderInstance(const StructuredData::ObjectSP &implementor) {
-    return false;
-  }
-
-  virtual bool MightHaveChildrenSynthProviderInstance(
-      const StructuredData::ObjectSP &implementor) {
-    return true;
-  }
-
-  virtual lldb::ValueObjectSP
-  GetSyntheticValue(const StructuredData::ObjectSP &implementor) {
-    return nullptr;
-  }
-
-  virtual ConstString
-  GetSyntheticTypeName(const StructuredData::ObjectSP &implementor) {
-    return ConstString();
-  }
-
   virtual bool
   RunScriptBasedCommand(const char *impl_function, llvm::StringRef args,
                         ScriptedCommandSynchronicity synchronicity,
@@ -517,6 +476,11 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  virtual lldb::ScriptedSyntheticChildrenInterfaceSP
+  CreateScriptedSyntheticChildrenInterface() {
+    return {};
+  }
+
   virtual StructuredData::ObjectSP
   CreateStructuredDataFromScriptObject(ScriptObject obj) {
     return {};
diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h
index af0ea08b81e45..a60034c048cc2 100644
--- a/lldb/include/lldb/lldb-enumerations.h
+++ b/lldb/include/lldb/lldb-enumerations.h
@@ -271,7 +271,8 @@ enum ScriptedExtension {
   eScriptedExtensionScriptedCommand,
   eScriptedExtensionParsedCommand,
   eScriptedExtensionScriptedStringSummary,
-  kLastScriptedExtension = eScriptedExtensionScriptedStringSummary
+  eScriptedExtensionScriptedSyntheticChildren,
+  kLastScriptedExtension = eScriptedExtensionScriptedSyntheticChildren
 };
 
 /// Register numbering types.
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index fceb94a8a3e7d..2a4044e9a9b88 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -202,6 +202,7 @@ class ScriptedThreadPlanInterface;
 class ScriptedStackFrameRecognizerInterface;
 class ScriptedStringSummaryInterface;
 class ScriptedSyntheticChildren;
+class ScriptedSyntheticChildrenInterface;
 class SearchFilter;
 class Section;
 class SectionList;
@@ -444,6 +445,8 @@ typedef std::shared_ptr<lldb_private::ScriptedCommandInterface>
     ScriptedCommandInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedStringSummaryInterface>
     ScriptedStringSummaryInterfaceSP;
+typedef std::shared_ptr<lldb_private::ScriptedSyntheticChildrenInterface>
+    ScriptedSyntheticChildrenInterfaceSP;
 typedef std::shared_ptr<lldb_private::Section> SectionSP;
 typedef std::unique_ptr<lldb_private::SectionList> SectionListUP;
 typedef std::weak_ptr<lldb_private::Section> SectionWP;
diff --git a/lldb/source/DataFormatters/TypeSynthetic.cpp b/lldb/source/DataFormatters/TypeSynthetic.cpp
index 76eb61b92e446..8817f4ef887e5 100644
--- a/lldb/source/DataFormatters/TypeSynthetic.cpp
+++ b/lldb/source/DataFormatters/TypeSynthetic.cpp
@@ -16,6 +16,7 @@
 #include "lldb/DataFormatters/FormatterBytecode.h"
 #include "lldb/DataFormatters/TypeSynthetic.h"
 #include "lldb/Interpreter/CommandInterpreter.h"
+#include "lldb/Interpreter/Interfaces/ScriptedSyntheticChildrenInterface.h"
 #include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Symbol/CompilerType.h"
 #include "lldb/Target/Target.h"
@@ -164,8 +165,7 @@ lldb::ValueObjectSP SyntheticChildrenFrontEnd::CreateChildValueObjectFromData(
 
 ScriptedSyntheticChildren::FrontEnd::FrontEnd(std::string pclass,
                                               ValueObject &backend)
-    : SyntheticChildrenFrontEnd(backend), m_python_class(pclass),
-      m_wrapper_sp(), m_interpreter(nullptr) {
+    : SyntheticChildrenFrontEnd(backend), m_python_class(pclass) {
   if (backend.GetID() == LLDB_INVALID_UID)
     return;
 
@@ -174,77 +174,84 @@ ScriptedSyntheticChildren::FrontEnd::FrontEnd(std::string pclass,
   if (!target_sp)
     return;
 
-  m_interpreter = target_sp->GetDebugger().GetScriptInterpreter();
+  ScriptInterpreter *interpreter =
+      target_sp->GetDebugger().GetScriptInterpreter();
 
-  if (m_interpreter != nullptr)
-    m_wrapper_sp = m_interpreter->CreateSyntheticScriptedProvider(
-        m_python_class.c_str(), backend.GetSP());
+  if (!interpreter)
+    return;
+
+  m_interface_sp = interpreter->CreateScriptedSyntheticChildrenInterface();
+  if (!m_interface_sp)
+    return;
+
+  auto obj_or_err = m_interface_sp->CreatePluginObject(m_python_class, backend);
+  if (!obj_or_err) {
+    llvm::consumeError(obj_or_err.takeError());
+    m_interface_sp.reset();
+  }
 }
 
 ScriptedSyntheticChildren::FrontEnd::~FrontEnd() = default;
 
 lldb::ValueObjectSP
 ScriptedSyntheticChildren::FrontEnd::GetChildAtIndex(uint32_t idx) {
-  if (!m_wrapper_sp || !m_interpreter)
+  if (!m_interface_sp)
     return lldb::ValueObjectSP();
 
-  return m_interpreter->GetChildAtIndex(m_wrapper_sp, idx);
+  return m_interface_sp->GetChildAtIndex(idx);
 }
 
 bool ScriptedSyntheticChildren::FrontEnd::IsValid() {
-  return (m_wrapper_sp && m_wrapper_sp->IsValid() && m_interpreter);
+  return m_interface_sp != nullptr;
 }
 
 llvm::Expected<uint32_t>
 ScriptedSyntheticChildren::FrontEnd::CalculateNumChildren() {
-  if (!m_wrapper_sp || m_interpreter == nullptr)
+  if (!m_interface_sp)
     return 0;
-  return m_interpreter->CalculateNumChildren(m_wrapper_sp, UINT32_MAX);
+  return m_interface_sp->CalculateNumChildren(UINT32_MAX);
 }
 
 llvm::Expected<uint32_t>
 ScriptedSyntheticChildren::FrontEnd::CalculateNumChildren(uint32_t max) {
-  if (!m_wrapper_sp || m_interpreter == nullptr)
+  if (!m_interface_sp)
     return 0;
-  return m_interpreter->CalculateNumChildren(m_wrapper_sp, max);
+  return m_interface_sp->CalculateNumChildren(max);
 }
 
 lldb::ChildCacheState ScriptedSyntheticChildren::FrontEnd::Update() {
-  if (!m_wrapper_sp || m_interpreter == nullptr)
+  if (!m_interface_sp)
     return lldb::ChildCacheState::eRefetch;
 
-  return m_interpreter->UpdateSynthProviderInstance(m_wrapper_sp)
-             ? lldb::ChildCacheState::eReuse
-             : lldb::ChildCacheState::eRefetch;
+  return m_interface_sp->Update();
 }
 
 bool ScriptedSyntheticChildren::FrontEnd::MightHaveChildren() {
-  if (!m_wrapper_sp || m_interpreter == nullptr)
+  if (!m_interface_sp)
     return false;
 
-  return m_interpreter->MightHaveChildrenSynthProviderInstance(m_wrapper_sp);
+  return m_interface_sp->MightHaveChildren();
 }
 
 llvm::Expected<size_t>
 ScriptedSyntheticChildren::FrontEnd::GetIndexOfChildWithName(ConstString name) {
-  if (!m_wrapper_sp || m_interpreter == nullptr)
+  if (!m_interface_sp)
     return llvm::createStringErrorV("type has no child named '{0}'", name);
-  return m_interpreter->GetIndexOfChildWithName(m_wrapper_sp,
-                                                name.GetCString());
+  return m_interface_sp->GetIndexOfChildWithName(name);
 }
 
 lldb::ValueObjectSP ScriptedSyntheticChildren::FrontEnd::GetSyntheticValue() {
-  if (!m_wrapper_sp || m_interpreter == nullptr)
+  if (!m_interface_sp)
     return nullptr;
 
-  return m_interpreter->GetSyntheticValue(m_wrapper_sp);
+  return m_interface_sp->GetSyntheticValue();
 }
 
 ConstString ScriptedSyntheticChildren::FrontEnd::GetSyntheticTypeName() {
-  if (!m_wrapper_sp || m_interpreter == nullptr)
+  if (!m_interface_sp)
     return ConstString();
 
-  return m_interpreter->GetSyntheticTypeName(m_wrapper_sp);
+  return m_interface_sp->GetSyntheticTypeName();
 }
 
 std::string ScriptedSyntheticChildren::GetDescription() {
diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp
index 2a7580afb8aaa..c16716bbc00fb 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -8,8 +8,6 @@
 
 #include "lldb/Interpreter/ScriptInterpreter.h"
 #include "API/SBCommandReturnObjectImpl.h"
-#include "lldb/API/SBCommandReturnObject.h"
-#include "lldb/API/SBDebugger.h"
 #include "lldb/Core/Debugger.h"
 #include "lldb/Host/ConnectionFileDescriptor.h"
 #include "lldb/Host/Pipe.h"
@@ -240,6 +238,8 @@ ScriptInterpreter::ExtensionToString(lldb::ScriptedExtension extension) {
     return "ParsedCommand";
   case eScriptedExtensionScriptedStringSummary:
     return "ScriptedStringSummary";
+  case eScriptedExtensionScriptedSyntheticChildren:
+    return "ScriptedSyntheticChildren";
   }
   llvm_unreachable("unhandled ScriptedExtension");
 }
@@ -264,6 +264,8 @@ ScriptInterpreter::StringToExtension(llvm::StringRef string) {
       .CaseLower("ParsedCommand", eScriptedExtensionParsedCommand)
       .CaseLower("ScriptedStringSummary",
                  eScriptedExtensionScriptedStringSummary)
+      .CaseLower("ScriptedSyntheticChildren",
+                 eScriptedExtensionScriptedSyntheticChildren)
       .Default(eScriptedExtensionInvalid);
 }
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
index e3f0c570c0312..0515570c5c4ca 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
@@ -35,6 +35,7 @@ set(python_plugin_sources
   Interfaces/ScriptedCommandPythonInterface.cpp
   Interfaces/ScriptedStackFrameRecognizerPythonInterface.cpp
   Interfaces/ScriptedStringSummaryPythonInterface.cpp
+  Interfaces/ScriptedSyntheticChildrenPythonInterface.cpp
   Interfaces/ScriptedThreadPlanPythonInterface.cpp
   Interfaces/ScriptedThreadPythonInterface.cpp
 )
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
index 185951c9e55fa..401787e1342c6 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
@@ -32,6 +32,7 @@ void ScriptInterpreterPythonInterfaces::Initialize() {
   ScriptedStackFrameRecognizerPythonInterface::Initialize();
   ScriptedCommandPythonInterface::Initialize();
   ScriptedStringSummaryPythonInterface::Initialize();
+  ScriptedSyntheticChildrenPythonInterface::Initialize();
 }
 
 void ScriptInterpreterPythonInterfaces::Terminate() {
@@ -47,4 +48,5 @@ void ScriptInterpreterPythonInterfaces::Terminate() {
   ScriptedStackFrameRecognizerPythonInterface::Terminate();
   ScriptedCommandPythonInterface::Terminate();
   ScriptedStringSummaryPythonInterface::Terminate();
+  ScriptedSyntheticChildrenPythonInterface::Terminate();
 }
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
index 43f2b4c011a18..a878b41018c36 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
@@ -22,6 +22,7 @@
 #include "ScriptedProcessPythonInterface.h"
 #include "ScriptedStackFrameRecognizerPythonInterface.h"
 #include "ScriptedStringSummaryPythonInterface.h"
+#include "ScriptedSyntheticChildrenPythonInterface.h"
 #include "ScriptedThreadPlanPythonInterface.h"
 #include "ScriptedThreadPythonInterface.h"
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index 24cae7317b746..3223d53e25c62 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -111,7 +111,7 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
 
   llvm::Expected<std::map<llvm::StringLiteral, AbstractMethodCheckerPayload>>
   CheckAbstractMethodImplementation(
-      const python::PythonDictionary &class_dict) const {
+      const python::PythonObject &obj_class) const {
 
     using namespace python;
 
@@ -125,18 +125,17 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     for (const AbstractMethodRequirement &requirement :
          GetAbstractMethodRequirements()) {
       llvm::StringLiteral method_name = requirement.name;
-      if (!class_dict.HasKey(method_name))
+      // Look up via attribute access so inherited methods are found; the
+      // class's own __dict__ omits anything defined on a base class.
+      if (!obj_class.HasAttribute(method_name))
         SET_CASE_AND_CONTINUE(method_name,
                               AbstractMethodCheckerCases::eNotImplemented)
-      llvm::Expected<PythonObject> callable_or_err =
-          class_dict.GetItem(method_name);
-      if (!callable_or_err) {
-        llvm::consumeError(callable_or_err.takeError());
+      PythonObject attr = obj_class.GetAttributeValue(method_name);
+      if (!attr.IsAllocated())
         SET_CASE_AND_CONTINUE(method_name,
                               AbstractMethodCheckerCases::eNotAllocated)
-      }
 
-      PythonCallable callable = callable_or_err->AsType<PythonCallable>();
+      PythonCallable callable = attr.AsType<PythonCallable>();
       if (!callable)
         SET_CASE_AND_CONTINUE(method_name,
                               AbstractMethodCheckerCases::eNotCallable)
@@ -296,26 +295,7 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     PythonString obj_class_name =
         obj_class.GetAttributeValue("__name__").AsType<PythonString>();
 
-    PythonObject object_class_mapping_proxy =
-        obj_class.GetAttributeValue("__dict__");
-    if (!obj_class.HasAttribute("__dict__"))
-      return create_error(
-          "Resulting object class doesn't have '__dict__' member.");
-
-    PythonCallable dict_converter = PythonModule::BuiltinsModule()
-                                        .ResolveName("dict")
-                                        .AsType<PythonCallable>();
-    if (!dict_converter.IsAllocated())
-      return create_error(
-          "Python 'builtins' module doesn't have 'dict' class.");
-
-    PythonDictionary object_class_dict =
-        dict_converter(object_class_mapping_proxy).AsType<PythonDictionary>();
-    if (!object_class_dict.IsAllocated())
-      return create_error("Coudn't create dictionary from resulting object "
-                          "class mapping proxy object.");
-
-    auto checker_or_err = CheckAbstractMethodImplementation(object_class_dict);
+    auto checker_or_err = CheckAbstractMethodImplementation(obj_class);
     if (!checker_or_err)
       return checker_or_err.takeError();
 
@@ -532,15 +512,36 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     std::tuple<Args...> original_args = std::forward_as_tuple(args...);
     auto transformed_args = TransformArgs(original_args);
 
+    // Trim trailing args if the Python method accepts fewer positional
+    // parameters than we're passing (e.g. `num_children(self)` vs.
+    // `num_children(self, max_count)`).
+    size_t call_arity = sizeof...(Args);
+    if (PythonObject py_method = implementor.GetAttributeValue(method_name);
+        py_method.IsAllocated()) {
+      PythonCallable callable = py_method.AsType<PythonCallable>();
+      if (callable.IsAllocated()) {
+        if (llvm::Expected<PythonCallable::ArgInfo> arg_info =
+                callable.GetArgInfo()) {
+          if (arg_info->max_positional_args !=
+                  PythonCallable::ArgInfo::UNBOUNDED &&
+              arg_info->max_positional_args < call_arity)
+            call_arity = arg_info->max_positional_args;
+        } else {
+          llvm::consumeError(arg_info.takeError());
+        }
+      }
+    }
+
     llvm::Expected<PythonObject> expected_return_object =
         llvm::createStringError("not initialized");
-    std::apply(
-        [&implementor, &method_name, &expected_return_object](auto &&...args) {
-          llvm::consumeError(expected_return_object.takeError());
-          expected_return_object =
-              implementor.CallMethod(method_name.data(), args...);
-        },
-        transformed_args);
+    CallWithArity(call_arity, transformed_args,
+                  std::make_index_sequence<sizeof...(Args) + 1>{},
+                  [&implementor, &method_name,
+                   &expected_return_object](auto &&...call_args) {
+                    llvm::consumeError(expected_return_object.takeError());
+                    expected_return_object = implementor.CallMethod(
+                        method_name.data(), call_args...);
+                  });
 
     if (llvm::Error e = expected_return_object.takeError()) {
       error = Status::FromError(std::move(e));
@@ -732,6 +733,30 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     return TransformTuple(args, std::make_index_sequence<sizeof...(Args)>());
   }
 
+  // Apply `fn` with the first `N` elements of `t`, for compile-time `N`.
+  template <std::size_t N, typename Tuple, typename Fn, std::size_t... I>
+  static void ApplyPrefixImpl(Tuple &&t, Fn &&fn, std::index_sequence<I...>) {
+    std::forward<Fn>(fn)(std::get<I>(std::forward<Tuple>(t))...);
+  }
+
+  template <std::size_t N, typename Tuple, typename Fn>
+  static void ApplyPrefix(Tuple &&t, Fn &&fn) {
+    ApplyPrefixImpl<N>(std::forward<Tuple>(t), std::forward<Fn>(fn),
+                       std::make_index_sequence<N>{});
+  }
+
+  // Call `fn` with a runtime-selected prefix of `t`: exactly `call_arity`
+  // leading elements. `Is...` enumerates every compile-time count in
+  // `[0, sizeof...(Args)]`; the runtime check picks the matching one.
+  template <typename Tuple, std::size_t... Is, typename Fn>
+  static void CallWithArity(size_t call_arity, Tuple &&t,
+                            std::index_sequence<Is...>, Fn &&fn) {
+    (void)std::initializer_list<int>{(
+        Is == call_arity
+            ? (ApplyPrefix<Is>(std::forward<Tuple>(t), std::forward<Fn>(fn)), 0)
+            : 0)...};
+  }
+
   template <typename T, typename U>
   void TransformBack(T &original_arg, U transformed_arg, Status &error) {
     ReverseTransform(original_arg, transformed_arg, error);
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSyntheticChildrenPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSyntheticChildrenPythonInterface.cpp
new file mode 100644
index 0000000000000..4e45111fafc1c
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSyntheticChildrenPythonInterface.cpp
@@ -0,0 +1,144 @@
+//===----------------------------------------------------------------------===//
+//
+// 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-python.h"
+
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Utility/ScriptedMetadata.h"
+#include "lldb/ValueObject/ValueObject.h"
+#include "lldb/lldb-enumerations.h"
+
+#include "../SWIGPythonBridge.h"
+#include "../ScriptInterpreterPythonImpl.h"
+#include "ScriptedSyntheticChildrenPythonInterface.h"
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::python;
+using Locker = ScriptInterpreterPythonImpl::Locker;
+
+ScriptedSyntheticChildrenPythonInterface::
+    ScriptedSyntheticChildrenPythonInterface(
+        ScriptInterpreterPythonImpl &interpreter)
+    : ScriptedSyntheticChildrenInterface(),
+      ScriptedPythonInterface(interpreter) {}
+
+llvm::Expected<StructuredData::GenericSP>
+ScriptedSyntheticChildrenPythonInterface::CreatePluginObject(
+    llvm::StringRef class_name, ValueObject &backend) {
+  if (class_name.empty())
+    return llvm::createStringError("empty class name");
+
+  ValueObjectSP valobj_sp = backend.GetSP();
+  if (!valobj_sp)
+    return llvm::createStringError("invalid backing value");
+
+  Locker py_lock(&m_interpreter,
+                 Locker::AcquireLock | Locker::InitSession | Locker::NoSTDIN,
+                 Locker::FreeLock | Locker::TearDownSession);
+
+  // Hand the provider's __init__ a fresh SBValue view of the backing value
+  // with synthetic children disabled, so introspecting it doesn't recursively
+  // re-enter this provider. `SetPreferSyntheticValue` lives on the SBValue's
+  // ValueImpl, so this override doesn't affect the caller's original view.
+  PythonObject val_arg =
+      SWIGBridge::ToSWIGWrapper(valobj_sp, /*use_synthetic=*/false);
+
+  ScriptedMetadata scripted_metadata(class_name,
+                                     StructuredData::DictionarySP());
+  return ScriptedPythonInterface::CreatePluginObject(
+      scripted_metadata, /*script_obj=*/nullptr, std::move(val_arg));
+}
+
+llvm::Expected<uint32_t>
+ScriptedSyntheticChildrenPythonInterface::CalculateNumChildren(uint32_t max) {
+  Status error;
+  StructuredData::ObjectSP obj = Dispatch("num_children", error, max);
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return 0;
+  // Cap at max in case the provider ignores the argument (e.g. defines
+  // `num_children(self)`) and returns an unbounded count.
+  return std::min<uint32_t>(obj->GetUnsignedIntegerValue(), max);
+}
+
+lldb::ValueObjectSP
+ScriptedSyntheticChildrenPythonInterface::GetChildAtIndex(uint32_t idx) {
+  Status error;
+  return Dispatch<lldb::ValueObjectSP>("get_child_at_index", error, idx);
+}
+
+llvm::Expected<uint32_t>
+ScriptedSyntheticChildrenPythonInterface::GetIndexOfChildWithName(
+    ConstString name) {
+  Status error;
+  StructuredData::ObjectSP obj =
+      Dispatch("get_child_index", error, name.GetCString());
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return llvm::createStringErrorV("type has no child named '{0}'", name);
+
+  // `CreateStructuredObject` only produces a `SignedInteger` for values that
+  // don't fit as unsigned, i.e. negative ones; a non-negative index comes
+  // back as `UnsignedInteger` instead, so check the sign this way rather
+  // than via `GetSignedIntegerValue`, which would misread every valid index.
+  if (obj->GetAsSignedInteger())
+    return llvm::createStringErrorV("type has no child named '{0}'", name);
+  return static_cast<uint32_t>(obj->GetUnsignedIntegerValue());
+}
+
+lldb::ChildCacheState ScriptedSyntheticChildrenPythonInterface::Update() {
+  Status error;
+  // update() is optional; a missing method means "always refetch".
+  StructuredData::ObjectSP obj = Dispatch("update", error);
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return lldb::eRefetch;
+  return obj->GetBooleanValue() ? lldb::eReuse : lldb::eRefetch;
+}
+
+bool ScriptedSyntheticChildrenPythonInterface::MightHaveChildren() {
+  Status error;
+  // has_children() is optional and defaults to True when missing.
+  StructuredData::ObjectSP obj = Dispatch("has_children", error);
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return true;
+  return obj->GetBooleanValue();
+}
+
+lldb::ValueObjectSP
+ScriptedSyntheticChildrenPythonInterface::GetSyntheticValue() {
+  Status error;
+  return Dispatch<lldb::ValueObjectSP>("get_value", error);
+}
+
+ConstString ScriptedSyntheticChildrenPythonInterface::GetSyntheticTypeName() {
+  Status error;
+  StructuredData::ObjectSP obj = Dispatch("get_type_name", error);
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return {};
+  return ConstString(obj->GetStringValue());
+}
+
+void ScriptedSyntheticChildrenPythonInterface::Initialize() {
+  const std::vector<llvm::StringRef> ci_usages = {
+      "type synthetic add -l <ClassName> <TypeName>"};
+  const std::vector<llvm::StringRef> api_usages = {
+      "SBTypeSynthetic.CreateWithClassName"};
+  PluginManager::RegisterPlugin(
+      GetPluginNameStatic(),
+      "Provide synthetic children for a type, used by 'type synthetic add -l'",
+      CreateInstance, eScriptedExtensionScriptedSyntheticChildren,
+      eScriptLanguagePython, {ci_usages, api_usages});
+}
+
+void ScriptedSyntheticChildrenPythonInterface::Terminate() {
+  PluginManager::UnregisterPlugin(CreateInstance);
+}
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSyntheticChildrenPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSyntheticChildrenPythonInterface.h
new file mode 100644
index 0000000000000..0cb2ffc13415f
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSyntheticChildrenPythonInterface.h
@@ -0,0 +1,63 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDSYNTHETICCHILDRENPYTHONINTERFACE_H
+#define LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDSYNTHETICCHILDRENPYTHONINTERFACE_H
+
+#include "lldb/Interpreter/Interfaces/ScriptedSyntheticChildrenInterface.h"
+
+#include "ScriptedPythonInterface.h"
+namespace lldb_private {
+
+class ScriptedSyntheticChildrenPythonInterface
+    : public ScriptedSyntheticChildrenInterface,
+      public ScriptedPythonInterface,
+      public PluginInterface {
+public:
+  ScriptedSyntheticChildrenPythonInterface(
+      ScriptInterpreterPythonImpl &interpreter);
+
+  llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, ValueObject &backend) override;
+
+  llvm::SmallVector<AbstractMethodRequirement>
+  GetAbstractMethodRequirements() const override {
+    // Providers that never expose children (num_children == 0 / has_children ==
+    // False) legitimately don't implement get_child_at_index; LLDB simply
+    // won't call it. Treating any single method as required here is stricter
+    // than the pre-migration behavior and would reject those providers.
+    return {};
+  }
+
+  llvm::Expected<uint32_t> CalculateNumChildren(uint32_t max) override;
+
+  lldb::ValueObjectSP GetChildAtIndex(uint32_t idx) override;
+
+  llvm::Expected<uint32_t> GetIndexOfChildWithName(ConstString name) override;
+
+  lldb::ChildCacheState Update() override;
+
+  bool MightHaveChildren() override;
+
+  lldb::ValueObjectSP GetSyntheticValue() override;
+
+  ConstString GetSyntheticTypeName() override;
+
+  static void Initialize();
+
+  static void Terminate();
+
+  static llvm::StringRef GetPluginNameStatic() {
+    return "ScriptedSyntheticChildrenPythonInterface";
+  }
+
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+};
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDSYNTHETICCHILDRENPYTHONINTERFACE_H
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
index 9c5391e754396..2530342f74bd3 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
@@ -79,6 +79,8 @@ class SWIGBridge {
   static PythonObject
   ToSWIGWrapper(std::unique_ptr<lldb::SBCommandReturnObject> result_up);
   static PythonObject ToSWIGWrapper(lldb::ValueObjectSP value_sp);
+  static PythonObject ToSWIGWrapper(lldb::ValueObjectSP value_sp,
+                                    bool use_synthetic);
   static PythonObject ToSWIGWrapper(lldb::TargetSP target_sp);
   static PythonObject ToSWIGWrapper(lldb::ProcessSP process_sp);
   static PythonObject ToSWIGWrapper(lldb::ModuleSP module_sp);
@@ -139,31 +141,13 @@ class SWIGBridge {
       const lldb::TypeSummaryOptionsSP &options_sp, std::string &retval);
 
   static python::PythonObject
-  LLDBSwigPythonCreateSyntheticProvider(const char *python_class_name,
-                                        const char *session_dictionary_name,
-                                        const lldb::ValueObjectSP &valobj_sp);
-
-  static size_t LLDBSwigPython_CalculateNumChildren(PyObject *implementor,
-                                                    uint32_t max);
-
-  static PyObject *LLDBSwigPython_GetChildAtIndex(PyObject *implementor,
-                                                  uint32_t idx);
-
-  static uint32_t
-  LLDBSwigPython_GetIndexOfChildWithName(PyObject *implementor,
-                                         const char *child_name);
+  LLDBSwigPythonCreateCommandObject(const char *python_class_name,
+                                    const char *session_dictionary_name,
+                                    lldb::DebuggerSP debugger_sp);
 
   static lldb::ValueObjectSP
   LLDBSWIGPython_GetValueObjectSPFromSBValue(void *data);
 
-  static bool LLDBSwigPython_UpdateSynthProviderInstance(PyObject *implementor);
-
-  static bool
-  LLDBSwigPython_MightHaveChildrenSynthProviderInstance(PyObject *implementor);
-
-  static PyObject *
-  LLDBSwigPython_GetValueSynthProviderInstance(PyObject *implementor);
-
   static bool
   LLDBSwigPythonCallCommand(const char *python_function_name,
                             const char *session_dictionary_name,
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index 81e6a656629bf..c7065df8212a4 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -294,6 +294,8 @@ llvm::Expected<std::string> ScriptInterpreterPython::ExtensionToImportPath(
     return "lldb.plugins.scripted_command";
   case eScriptedExtensionScriptedStringSummary:
     return "lldb.plugins.scripted_string_summary";
+  case eScriptedExtensionScriptedSyntheticChildren:
+    return "lldb.plugins.scripted_synthetic_children";
   case eScriptedExtensionInvalid:
     return llvm::createStringError("invalid extension name");
   }
@@ -2018,6 +2020,11 @@ ScriptInterpreterPythonImpl::CreateScriptedStringSummaryInterface() {
   return std::make_shared<ScriptedStringSummaryPythonInterface>(*this);
 }
 
+ScriptedSyntheticChildrenInterfaceSP
+ScriptInterpreterPythonImpl::CreateScriptedSyntheticChildrenInterface() {
+  return std::make_shared<ScriptedSyntheticChildrenPythonInterface>(*this);
+}
+
 ScriptedThreadInterfaceSP
 ScriptInterpreterPythonImpl::CreateScriptedThreadInterface() {
   return std::make_shared<ScriptedThreadPythonInterface>(*this);
@@ -2101,37 +2108,6 @@ StructuredData::DictionarySP ScriptInterpreterPythonImpl::GetDynamicSettings(
   return py_dict.CreateStructuredDictionary();
 }
 
-StructuredData::ObjectSP
-ScriptInterpreterPythonImpl::CreateSyntheticScriptedProvider(
-    const char *class_name, lldb::ValueObjectSP valobj) {
-  if (class_name == nullptr || class_name[0] == '\0')
-    return StructuredData::ObjectSP();
-
-  if (!valobj.get())
-    return StructuredData::ObjectSP();
-
-  ExecutionContext exe_ctx(valobj->GetExecutionContextRef());
-  Target *target = exe_ctx.GetTargetPtr();
-
-  if (!target)
-    return StructuredData::ObjectSP();
-
-  Debugger &debugger = target->GetDebugger();
-  ScriptInterpreterPythonImpl *python_interpreter =
-      GetPythonInterpreter(debugger);
-
-  if (!python_interpreter)
-    return StructuredData::ObjectSP();
-
-  Locker py_lock(this,
-                 Locker::AcquireLock | Locker::InitSession | Locker::NoSTDIN);
-  PythonObject ret_val = SWIGBridge::LLDBSwigPythonCreateSyntheticProvider(
-      class_name, python_interpreter->m_dictionary_name.c_str(), valobj);
-
-  return StructuredData::ObjectSP(
-      new StructuredPythonObject(std::move(ret_val)));
-}
-
 bool ScriptInterpreterPythonImpl::GenerateTypeScriptFunction(
     const char *oneliner, std::string &output, const void *name_token) {
   StringList input;
@@ -2368,208 +2344,6 @@ bool ScriptInterpreterPythonImpl::WatchpointCallbackFunction(
   return true;
 }
 
-size_t ScriptInterpreterPythonImpl::CalculateNumChildren(
-    const StructuredData::ObjectSP &implementor_sp, uint32_t max) {
-  if (!implementor_sp)
-    return 0;
-  StructuredData::Generic *generic = implementor_sp->GetAsGeneric();
-  if (!generic)
-    return 0;
-  auto *implementor = static_cast<PyObject *>(generic->GetValue());
-  if (!implementor)
-    return 0;
-
-  size_t ret_val = 0;
-
-  {
-    Locker py_lock(this,
-                   Locker::AcquireLock | Locker::InitSession | Locker::NoSTDIN);
-    ret_val = SWIGBridge::LLDBSwigPython_CalculateNumChildren(implementor, max);
-  }
-
-  return ret_val;
-}
-
-lldb::ValueObjectSP ScriptInterpreterPythonImpl::GetChildAtIndex(
-    const StructuredData::ObjectSP &implementor_sp, uint32_t idx) {
-  if (!implementor_sp)
-    return lldb::ValueObjectSP();
-
-  StructuredData::Generic *generic = implementor_sp->GetAsGeneric();
-  if (!generic)
-    return lldb::ValueObjectSP();
-  auto *implementor = static_cast<PyObject *>(generic->GetValue());
-  if (!implementor)
-    return lldb::ValueObjectSP();
-
-  lldb::ValueObjectSP ret_val;
-  {
-    Locker py_lock(this,
-                   Locker::AcquireLock | Locker::InitSession | Locker::NoSTDIN);
-    PyObject *child_ptr =
-        SWIGBridge::LLDBSwigPython_GetChildAtIndex(implementor, idx);
-    if (child_ptr != nullptr && child_ptr != Py_None) {
-      lldb::SBValue *sb_value_ptr =
-          (lldb::SBValue *)LLDBSWIGPython_CastPyObjectToSBValue(child_ptr);
-      if (sb_value_ptr == nullptr)
-        Py_XDECREF(child_ptr);
-      else
-        ret_val = SWIGBridge::LLDBSWIGPython_GetValueObjectSPFromSBValue(
-            sb_value_ptr);
-    } else {
-      Py_XDECREF(child_ptr);
-    }
-  }
-
-  return ret_val;
-}
-
-llvm::Expected<uint32_t> ScriptInterpreterPythonImpl::GetIndexOfChildWithName(
-    const StructuredData::ObjectSP &implementor_sp, const char *child_name) {
-  if (!implementor_sp)
-    return llvm::createStringErrorV("type has no child named '{0}'",
-                                    child_name);
-
-  StructuredData::Generic *generic = implementor_sp->GetAsGeneric();
-  if (!generic)
-    return llvm::createStringErrorV("type has no child named '{0}'",
-                                    child_name);
-  auto *implementor = static_cast<PyObject *>(generic->GetValue());
-  if (!implementor)
-    return llvm::createStringErrorV("type has no child named '{0}'",
-                                    child_name);
-
-  uint32_t ret_val = UINT32_MAX;
-
-  {
-    Locker py_lock(this,
-                   Locker::AcquireLock | Locker::InitSession | Locker::NoSTDIN);
-    ret_val = SWIGBridge::LLDBSwigPython_GetIndexOfChildWithName(implementor,
-                                                                 child_name);
-  }
-
-  if (ret_val == UINT32_MAX)
-    return llvm::createStringErrorV("type has no child named '{0}'",
-                                    child_name);
-  return ret_val;
-}
-
-bool ScriptInterpreterPythonImpl::UpdateSynthProviderInstance(
-    const StructuredData::ObjectSP &implementor_sp) {
-  bool ret_val = false;
-
-  if (!implementor_sp)
-    return ret_val;
-
-  StructuredData::Generic *generic = implementor_sp->GetAsGeneric();
-  if (!generic)
-    return ret_val;
-  auto *implementor = static_cast<PyObject *>(generic->GetValue());
-  if (!implementor)
-    return ret_val;
-
-  {
-    Locker py_lock(this,
-                   Locker::AcquireLock | Locker::InitSession | Locker::NoSTDIN);
-    ret_val =
-        SWIGBridge::LLDBSwigPython_UpdateSynthProviderInstance(implementor);
-  }
-
-  return ret_val;
-}
-
-bool ScriptInterpreterPythonImpl::MightHaveChildrenSynthProviderInstance(
-    const StructuredData::ObjectSP &implementor_sp) {
-  bool ret_val = false;
-
-  if (!implementor_sp)
-    return ret_val;
-
-  StructuredData::Generic *generic = implementor_sp->GetAsGeneric();
-  if (!generic)
-    return ret_val;
-  auto *implementor = static_cast<PyObject *>(generic->GetValue());
-  if (!implementor)
-    return ret_val;
-
-  {
-    Locker py_lock(this,
-                   Locker::AcquireLock | Locker::InitSession | Locker::NoSTDIN);
-    ret_val = SWIGBridge::LLDBSwigPython_MightHaveChildrenSynthProviderInstance(
-        implementor);
-  }
-
-  return ret_val;
-}
-
-lldb::ValueObjectSP ScriptInterpreterPythonImpl::GetSyntheticValue(
-    const StructuredData::ObjectSP &implementor_sp) {
-  lldb::ValueObjectSP ret_val(nullptr);
-
-  if (!implementor_sp)
-    return ret_val;
-
-  StructuredData::Generic *generic = implementor_sp->GetAsGeneric();
-  if (!generic)
-    return ret_val;
-  auto *implementor = static_cast<PyObject *>(generic->GetValue());
-  if (!implementor)
-    return ret_val;
-
-  {
-    Locker py_lock(this,
-                   Locker::AcquireLock | Locker::InitSession | Locker::NoSTDIN);
-    PyObject *child_ptr =
-        SWIGBridge::LLDBSwigPython_GetValueSynthProviderInstance(implementor);
-    if (child_ptr != nullptr && child_ptr != Py_None) {
-      lldb::SBValue *sb_value_ptr =
-          (lldb::SBValue *)LLDBSWIGPython_CastPyObjectToSBValue(child_ptr);
-      if (sb_value_ptr == nullptr)
-        Py_XDECREF(child_ptr);
-      else
-        ret_val = SWIGBridge::LLDBSWIGPython_GetValueObjectSPFromSBValue(
-            sb_value_ptr);
-    } else {
-      Py_XDECREF(child_ptr);
-    }
-  }
-
-  return ret_val;
-}
-
-ConstString ScriptInterpreterPythonImpl::GetSyntheticTypeName(
-    const StructuredData::ObjectSP &implementor_sp) {
-  Locker py_lock(this,
-                 Locker::AcquireLock | Locker::InitSession | Locker::NoSTDIN);
-
-  if (!implementor_sp)
-    return {};
-
-  StructuredData::Generic *generic = implementor_sp->GetAsGeneric();
-  if (!generic)
-    return {};
-
-  PythonObject implementor(PyRefType::Borrowed,
-                           (PyObject *)generic->GetValue());
-  if (!implementor.IsAllocated())
-    return {};
-
-  llvm::Expected<PythonObject> expected_py_return =
-      implementor.CallMethod("get_type_name");
-
-  if (!expected_py_return) {
-    llvm::consumeError(expected_py_return.takeError());
-    return {};
-  }
-
-  PythonObject py_return = std::move(expected_py_return.get());
-  if (!py_return.IsAllocated() || !PythonString::Check(py_return.get()))
-    return {};
-
-  PythonString type_name(PyRefType::Borrowed, py_return.get());
-  return ConstString(type_name.GetString());
-}
-
 bool ScriptInterpreterPythonImpl::RunScriptFormatKeyword(
     const char *impl_function, Process *process, std::string &output,
     Status &error) {
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
index bf7f677401b12..2d5f72e184949 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
@@ -66,10 +66,6 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
   bool GenerateScriptAliasFunction(StringList &input,
                                    std::string &output) override;
 
-  StructuredData::ObjectSP
-  CreateSyntheticScriptedProvider(const char *class_name,
-                                  lldb::ValueObjectSP valobj) override;
-
   StructuredData::ObjectSP
   CreateStructuredDataFromScriptObject(ScriptObject obj) override;
 
@@ -88,6 +84,9 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
   lldb::ScriptedStringSummaryInterfaceSP
   CreateScriptedStringSummaryInterface() override;
 
+  lldb::ScriptedSyntheticChildrenInterfaceSP
+  CreateScriptedSyntheticChildrenInterface() override;
+
   lldb::ScriptedThreadInterfaceSP CreateScriptedThreadInterface() override;
 
   lldb::ScriptedFrameInterfaceSP CreateScriptedFrameInterface() override;
@@ -109,29 +108,6 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
                      const char *setting_name,
                      lldb_private::Status &error) override;
 
-  size_t CalculateNumChildren(const StructuredData::ObjectSP &implementor,
-                              uint32_t max) override;
-
-  lldb::ValueObjectSP
-  GetChildAtIndex(const StructuredData::ObjectSP &implementor,
-                  uint32_t idx) override;
-
-  llvm::Expected<uint32_t>
-  GetIndexOfChildWithName(const StructuredData::ObjectSP &implementor,
-                          const char *child_name) override;
-
-  bool UpdateSynthProviderInstance(
-      const StructuredData::ObjectSP &implementor) override;
-
-  bool MightHaveChildrenSynthProviderInstance(
-      const StructuredData::ObjectSP &implementor) override;
-
-  lldb::ValueObjectSP
-  GetSyntheticValue(const StructuredData::ObjectSP &implementor) override;
-
-  ConstString
-  GetSyntheticTypeName(const StructuredData::ObjectSP &implementor) override;
-
   bool
   RunScriptBasedCommand(const char *impl_function, llvm::StringRef args,
                         ScriptedCommandSynchronicity synchronicity,
diff --git a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
index 808bb6157e5b8..9b6c3af3c9563 100644
--- a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
+++ b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
@@ -67,28 +67,12 @@ bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallTypeScript(
 }
 
 python::PythonObject
-lldb_private::python::SWIGBridge::LLDBSwigPythonCreateSyntheticProvider(
+lldb_private::python::SWIGBridge::LLDBSwigPythonCreateCommandObject(
     const char *python_class_name, const char *session_dictionary_name,
-    const lldb::ValueObjectSP &valobj_sp) {
+    lldb::DebuggerSP debugger_sp) {
   return python::PythonObject();
 }
 
-size_t lldb_private::python::SWIGBridge::LLDBSwigPython_CalculateNumChildren(
-    PyObject *implementor, uint32_t max) {
-  return 0;
-}
-
-PyObject *lldb_private::python::SWIGBridge::LLDBSwigPython_GetChildAtIndex(
-    PyObject *implementor, uint32_t idx) {
-  return nullptr;
-}
-
-uint32_t
-lldb_private::python::SWIGBridge::LLDBSwigPython_GetIndexOfChildWithName(
-    PyObject *implementor, const char *child_name) {
-  return 0;
-}
-
 void *
 lldb_private::python::LLDBSWIGPython_CastPyObjectToSBData(PyObject *data) {
   return nullptr;
@@ -185,23 +169,6 @@ lldb_private::python::SWIGBridge::LLDBSWIGPython_GetValueObjectSPFromSBValue(
   return nullptr;
 }
 
-bool lldb_private::python::SWIGBridge::
-    LLDBSwigPython_UpdateSynthProviderInstance(PyObject *implementor) {
-  return false;
-}
-
-bool lldb_private::python::SWIGBridge::
-    LLDBSwigPython_MightHaveChildrenSynthProviderInstance(
-        PyObject *implementor) {
-  return false;
-}
-
-PyObject *
-lldb_private::python::SWIGBridge::LLDBSwigPython_GetValueSynthProviderInstance(
-    PyObject *implementor) {
-  return nullptr;
-}
-
 bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallCommand(
     const char *python_function_name, const char *session_dictionary_name,
     lldb::DebuggerSP debugger, const char *args,

>From f4502a5af9ccc36d9c75ef4aa1844b498ce438c4 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Thu, 30 Jul 2026 16:11:11 -0700
Subject: [PATCH 05/16] [lldb/docs] Document the remaining scripted-extension
 plugin categories (#213140)

`python_extensions.md` only covered the first five plugin categories.

This commit adds the missing sections for every plugin category added
since: `ScriptedBreakpointResolver`, `ScriptedHook`,
`ScriptedStackFrameRecognizer`, `ScriptedCommand`, `ParsedCommand`,
`ScriptedStringSummary`, and `ScriptedSyntheticChildren`.

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
(cherry picked from commit 71af911facd88e41df273a95d658c3293aa4edee)
---
 lldb/docs/CMakeLists.txt       |  1 +
 lldb/docs/python_extensions.md | 82 ++++++++++++++++++++++++++++++++--
 2 files changed, 80 insertions(+), 3 deletions(-)

diff --git a/lldb/docs/CMakeLists.txt b/lldb/docs/CMakeLists.txt
index 1b172b3c8564e..f6d91daff3841 100644
--- a/lldb/docs/CMakeLists.txt
+++ b/lldb/docs/CMakeLists.txt
@@ -34,6 +34,7 @@ if (LLDB_ENABLE_PYTHON AND SPHINX_FOUND)
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_hook.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_stackframe_recognizer.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_command.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
+      COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/parsed_cmd.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_string_summary.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_synthetic_children.py" "${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMENT "Copying lldb.py to pretend its a Python package.")
diff --git a/lldb/docs/python_extensions.md b/lldb/docs/python_extensions.md
index ae3ca6245f6aa..8d24d65d9af45 100644
--- a/lldb/docs/python_extensions.md
+++ b/lldb/docs/python_extensions.md
@@ -16,6 +16,38 @@ This page describes some of these scripting extensions:
     :skip: ScriptedThread
 ```
 
+## Parsed Command Plugins
+
+```{eval-rst}
+.. automodule:: lldb.plugins.parsed_cmd
+
+.. automodsumm:: lldb.plugins.parsed_cmd
+    :classes-only:
+    :toctree: python_api
+```
+
+## Scripted Breakpoint Resolver Plugins
+
+```{eval-rst}
+.. automodule:: lldb.plugins.scripted_breakpoint
+
+.. automodsumm:: lldb.plugins.scripted_breakpoint
+    :classes-only:
+    :toctree: python_api
+    :skip: ABCMeta
+```
+
+## Scripted Command Plugins
+
+```{eval-rst}
+.. automodule:: lldb.plugins.scripted_command
+
+.. automodsumm:: lldb.plugins.scripted_command
+    :classes-only:
+    :toctree: python_api
+    :skip: ABCMeta
+```
+
 ## Scripted Frame Provider Plugins
 
 ```{eval-rst}
@@ -27,12 +59,12 @@ This page describes some of these scripting extensions:
     :skip: ABCMeta
 ```
 
-## Scripted Process Plugins
+## Scripted Hook Plugins
 
 ```{eval-rst}
-.. automodule:: lldb.plugins.scripted_process
+.. automodule:: lldb.plugins.scripted_hook
 
-.. automodsumm:: lldb.plugins.scripted_process
+.. automodsumm:: lldb.plugins.scripted_hook
     :classes-only:
     :toctree: python_api
     :skip: ABCMeta
@@ -49,6 +81,50 @@ This page describes some of these scripting extensions:
     :skip: ABCMeta
 ```
 
+## Scripted Process Plugins
+
+```{eval-rst}
+.. automodule:: lldb.plugins.scripted_process
+
+.. automodsumm:: lldb.plugins.scripted_process
+    :classes-only:
+    :toctree: python_api
+    :skip: ABCMeta
+```
+
+## Scripted Stack Frame Recognizer Plugins
+
+```{eval-rst}
+.. automodule:: lldb.plugins.scripted_stackframe_recognizer
+
+.. automodsumm:: lldb.plugins.scripted_stackframe_recognizer
+    :classes-only:
+    :toctree: python_api
+    :skip: ABCMeta
+```
+
+## Scripted String Summary Plugins
+
+```{eval-rst}
+.. automodule:: lldb.plugins.scripted_string_summary
+
+.. automodsumm:: lldb.plugins.scripted_string_summary
+    :classes-only:
+    :toctree: python_api
+    :skip: ABCMeta
+```
+
+## Scripted Synthetic Children Plugins
+
+```{eval-rst}
+.. automodule:: lldb.plugins.scripted_synthetic_children
+
+.. automodsumm:: lldb.plugins.scripted_synthetic_children
+    :classes-only:
+    :toctree: python_api
+    :skip: ABCMeta
+```
+
 ## Scripted Thread Plan Plugins
 
 ```{eval-rst}

>From 354d0d8207f7237203ad7504a7cfe69fa25d7241 Mon Sep 17 00:00:00 2001
From: Nerixyz <nerixdev at outlook.de>
Date: Fri, 31 Jul 2026 15:08:03 +0200
Subject: [PATCH 06/16] [lldb] Treat synthetic variables as always in scope
 (#204177)

When the variables in scope are requested, synthetic variables wouldn't
be returned, because `Variable::IsInScope` would return false. With this
PR, we return true for synthetic variables.

There's still one inconsistency between `frame var` and
`SBFrame::GetVariables` where `frame var` shows "re-exported" variables
from real frames (here: `variable_in_main`). Note that
`IsSyntheticValueType` returns false for `variable_in_main`.

(cherry picked from commit f5ffd86a9bd6dd9327669448c3b604ce9b658941)
---
 lldb/source/Symbol/Variable.cpp                           | 5 +++++
 .../scripted_frame_provider/TestScriptedFrameProvider.py  | 8 +++++---
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/lldb/source/Symbol/Variable.cpp b/lldb/source/Symbol/Variable.cpp
index 70a61fc7789c9..1d623da23c3c9 100644
--- a/lldb/source/Symbol/Variable.cpp
+++ b/lldb/source/Symbol/Variable.cpp
@@ -31,6 +31,7 @@
 #include "lldb/Utility/Log.h"
 #include "lldb/Utility/RegularExpression.h"
 #include "lldb/Utility/Stream.h"
+#include "lldb/Utility/ValueType.h"
 #include "lldb/ValueObject/ValueObject.h"
 #include "lldb/ValueObject/ValueObjectVariable.h"
 
@@ -279,6 +280,10 @@ bool Variable::LocationIsValidForAddress(const Address &address) {
 }
 
 bool Variable::IsInScope(StackFrame *frame) {
+  // Synthetic values are always in scope.
+  if (IsSyntheticValueType(m_scope))
+    return true;
+
   switch (m_scope) {
   case eValueTypeRegister:
   case eValueTypeRegisterSet:
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
index ddd9b68a632bb..58eae50a4d76f 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
@@ -855,13 +855,15 @@ def test_get_values(self):
         self.assertEqual(variables.GetValueAtIndex(0).name, "variable_in_main")
         self.assertEqual(variables.GetValueAtIndex(1).name, "_handler_one")
 
-        # FIXME: Synthetic variables are never in scope.
+        # Synthetic variables are always in scope.
         variables = frame0.GetVariables(False, False, False, True)
         self.assertFalse(variables.IsValid())
         self.assertEqual(variables.GetSize(), 0)
         variables = frame0.GetVariables(False, True, False, True)
-        self.assertFalse(variables.IsValid())
-        self.assertEqual(variables.GetSize(), 0)
+        self.assertTrue(variables.IsValid())
+        # We don't see `variable_in_main` here, because it doesn't have the synthetic flag.
+        self.assertEqual(variables.GetSize(), 1)
+        self.assertEqual(variables.GetValueAtIndex(0).name, "_handler_one")
 
         # Check the `frame variable` command(s) handle synthetic variables the
         # way we expect by printing them.

>From d7165b06d7dfe1ec6499edf218530cc47d3c1e78 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Fri, 31 Jul 2026 17:51:12 -0700
Subject: [PATCH 07/16] [lldb/Interpreter] Surface Python exceptions from
 scripted extensions (#198153)

(cherry picked from commit fd999b4c8bb7c02bbd3051832e599683679955f4)
---
 lldb/bindings/python/python-wrapper.swig      |  17 +-
 .../Interfaces/ScriptedBreakpointInterface.h  |   2 +-
 .../ScriptedFrameProviderInterface.h          |   2 +-
 .../Interfaces/ScriptedHookInterface.h        |   2 +-
 .../Interfaces/ScriptedInterface.h            |  26 +-
 .../lldb/Interpreter/ScriptInterpreter.h      |   4 +
 .../Breakpoint/BreakpointResolverScripted.cpp |   8 +-
 .../Python/OperatingSystemPython.cpp          |   8 +-
 .../Process/scripted/ScriptedProcess.cpp      |  56 ++-
 .../Process/scripted/ScriptedProcess.h        |   2 -
 .../Process/scripted/ScriptedThread.cpp       |  23 +-
 .../Interfaces/ScriptedPythonInterface.h      |  96 ++++--
 .../Python/ScriptInterpreterPython.cpp        |   6 +-
 lldb/source/Target/ScriptedThreadPlan.cpp     |   5 +
 lldb/source/Target/StackFrameRecognizer.cpp   |  11 +-
 .../command/script/TestCommandScript.py       |  13 +
 .../scripted_extensions/Makefile              |   3 +
 .../TestScriptedExtensionsDiagnostics.py      | 227 ++++++++++++
 .../scripted_extensions/main.c                |   3 +
 .../malformed_scripted_extensions.py          | 325 ++++++++++++++++++
 .../os_plugin_missing_methods.py              |  16 +
 .../TestFrameProviderRegisterCommandStatus.py |  19 +
 .../register_command_status/frame_provider.py |   6 +
 .../scripted_process/TestScriptedProcess.py   |   6 +-
 .../functionalities/step_scripted/Steps.py    |   5 +
 .../step_scripted/TestStepScripted.py         |  17 +
 26 files changed, 840 insertions(+), 68 deletions(-)
 create mode 100644 lldb/test/API/functionalities/scripted_extensions/Makefile
 create mode 100644 lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py
 create mode 100644 lldb/test/API/functionalities/scripted_extensions/main.c
 create mode 100644 lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py
 create mode 100644 lldb/test/API/functionalities/scripted_extensions/os_plugin_missing_methods.py

diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig
index 868c172880c7d..ebd0245febf54 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -486,7 +486,11 @@ bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallCommand(
     pfunc(debugger_arg, PythonString(args),
           SWIGBridge::ToSWIGWrapper(std::move(exe_ctx_ref_sp)), cmd_retobj_arg.obj(), dict);
 
-  return true;
+  // `pfunc`'s bare call syntax doesn't drain the interpreter's exception
+  // state, so check it directly: a Python runtime error raised from inside
+  // the command body must be reported as a failure (surfaced through
+  // `cmd_retobj`'s error status), not silently treated as success.
+  return !PyErr_Occurred();
 }
 
 PythonObject lldb_private::python::SWIGBridge::LLDBSWIGPythonCreateOSPlugin(
@@ -664,7 +668,11 @@ bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallModuleNewTarget(
 
   pfunc(SWIGBridge::ToSWIGWrapper(std::move(target_sp)), dict);
 
-  return true;
+  // `pfunc`'s bare call syntax doesn't drain the interpreter's exception
+  // state, so check it directly: an exception raised by
+  // `__lldb_module_added_to_target` must be reported as a failure, not
+  // silently treated as success.
+  return !PyErr_Occurred();
 }
 
 bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallModuleInit(
@@ -688,7 +696,10 @@ bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallModuleInit(
 
   pfunc(SWIGBridge::ToSWIGWrapper(std::move(debugger)), dict);
 
-  return true;
+  // Same as above: an exception raised by __lldb_init_module must be
+  // reported as a failure so callers (e.g. `command script import`, or
+  // module auto-loading) don't treat it as a successful import.
+  return !PyErr_Occurred();
 }
 
 lldb::ValueObjectSP lldb_private::python::SWIGBridge::LLDBSWIGPython_GetValueObjectSPFromSBValue(
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h
index 982f231d9f0b2..7328aa26db65d 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h
@@ -15,7 +15,7 @@
 #include "lldb/lldb-private.h"
 
 namespace lldb_private {
-class ScriptedBreakpointInterface : public ScriptedInterface {
+class ScriptedBreakpointInterface : virtual public ScriptedInterface {
 public:
   virtual llvm::Expected<StructuredData::GenericSP>
   CreatePluginObject(const ScriptedMetadata &scripted_metadata,
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
index 757a99cb2387b..8c94df430c103 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
@@ -14,7 +14,7 @@
 #include "ScriptedInterface.h"
 
 namespace lldb_private {
-class ScriptedFrameProviderInterface : public ScriptedInterface {
+class ScriptedFrameProviderInterface : virtual public ScriptedInterface {
 public:
   virtual bool AppliesToThread(llvm::StringRef class_name,
                                lldb::ThreadSP thread_sp) {
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h
index 54d335122300d..55a61a90f3bfd 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedHookInterface.h
@@ -14,7 +14,7 @@
 #include "ScriptedInterface.h"
 
 namespace lldb_private {
-class ScriptedHookInterface : public ScriptedInterface {
+class ScriptedHookInterface : virtual public ScriptedInterface {
 public:
   /// Describes which hook callback methods the Python class implements.
   struct SupportedHookMethods {
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
index 3dbc009a58311..21bb91960f777 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
@@ -60,20 +60,21 @@ class ScriptedInterface {
 
   template <typename Ret>
   static Ret ErrorWithMessage(llvm::StringRef caller_name,
-                              llvm::StringRef error_msg, Status &error,
+                              llvm::StringRef user_msg, Status &error,
                               LLDBLog log_category = LLDBLog::Process) {
     LLDB_LOGF(GetLog(log_category), "%s ERROR = %s", caller_name.data(),
-              error_msg.data());
-    std::string full_error_message =
-        llvm::Twine(caller_name + llvm::Twine(" ERROR = ") +
-                    llvm::Twine(error_msg))
-            .str();
-    if (const char *detailed_error = error.AsCString())
-      full_error_message +=
-          llvm::Twine(llvm::Twine(" (") + llvm::Twine(detailed_error) +
-                      llvm::Twine(")"))
-              .str();
-    error = Status(std::move(full_error_message));
+              user_msg.data());
+
+    // If `error` already has detailed content (e.g. a Python traceback),
+    // prepend this call's friendlier message to it instead of discarding
+    // either one.
+    std::string existing_error = error.Fail() ? error.AsCString() : "";
+    if (existing_error.empty())
+      error = Status::FromErrorString(user_msg.data());
+    else
+      error = Status::FromErrorStringWithFormatv("{0}: {1}", user_msg,
+                                                 existing_error);
+
     return {};
   }
 
@@ -105,4 +106,5 @@ class ScriptedInterface {
   std::optional<ScriptedMetadata> m_scripted_metadata;
 };
 } // namespace lldb_private
+
 #endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 7df5055cfea86..925b7b08e3291 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -579,6 +579,10 @@ class ScriptInterpreter : public PluginInterface {
 
   lldb::TargetSP GetOpaqueTypeFromSBTarget(const lldb::SBTarget &target) const;
 
+  /// Get the debugger associated with this script interpreter.
+  Debugger &GetDebugger() { return m_debugger; }
+  const Debugger &GetDebugger() const { return m_debugger; }
+
 protected:
   Debugger &m_debugger;
   lldb::ScriptLanguage m_script_lang;
diff --git a/lldb/source/Breakpoint/BreakpointResolverScripted.cpp b/lldb/source/Breakpoint/BreakpointResolverScripted.cpp
index 0719c8b634cd3..ea2602ae10e3c 100644
--- a/lldb/source/Breakpoint/BreakpointResolverScripted.cpp
+++ b/lldb/source/Breakpoint/BreakpointResolverScripted.cpp
@@ -20,6 +20,7 @@
 #include "lldb/Target/Target.h"
 #include "lldb/Utility/Log.h"
 #include "lldb/Utility/StreamString.h"
+#include "llvm/Support/FormatVariadic.h"
 
 using namespace lldb;
 using namespace lldb_private;
@@ -94,7 +95,12 @@ void BreakpointResolverScripted::CreateImplementationIfNeeded(
       m_interface_sp->CreatePluginObject(scripted_metadata, breakpoint_sp);
   if (!obj_or_err) {
     m_interface_sp.reset();
-    m_error = Status::FromError(obj_or_err.takeError());
+    std::string msg = llvm::toString(obj_or_err.takeError());
+    Debugger::ReportError(
+        llvm::formatv("failed to create BreakpointResolverScripted: {0}", msg)
+            .str(),
+        target.GetDebugger().GetID());
+    m_error = Status(msg);
     return;
   }
   StructuredData::ObjectSP object_sp = *obj_or_err;
diff --git a/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp b/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
index 49df19906929f..354e7ea30059e 100644
--- a/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
+++ b/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
@@ -33,6 +33,7 @@
 #include "lldb/Utility/StreamString.h"
 #include "lldb/Utility/StructuredData.h"
 #include "lldb/ValueObject/ValueObjectVariable.h"
+#include "llvm/Support/FormatVariadic.h"
 
 #include <memory>
 
@@ -120,7 +121,12 @@ OperatingSystemPython::OperatingSystemPython(lldb_private::Process *process,
       scripted_metadata, exe_ctx, nullptr);
 
   if (!obj_or_err) {
-    llvm::consumeError(obj_or_err.takeError());
+    std::string msg = llvm::toString(obj_or_err.takeError());
+    if (process)
+      Debugger::ReportError(
+          llvm::formatv("failed to create OperatingSystemPython: {0}", msg)
+              .str(),
+          process->GetTarget().GetDebugger().GetID());
     return;
   }
 
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
index 842eef9524328..502c2f1146e7a 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
@@ -27,6 +27,10 @@
 
 #include "Plugins/ObjectFile/Placeholder/ObjectFilePlaceholder.h"
 
+#include "llvm/Support/Error.h"
+
+#include <string>
+
 using namespace lldb;
 using namespace lldb_private;
 
@@ -57,11 +61,24 @@ lldb::ProcessSP ScriptedProcess::CreateInstance(lldb::TargetSP target_sp,
 
   ScriptedMetadata scripted_metadata(target_sp->GetProcessLaunchInfo());
 
+  if (!scripted_metadata)
+    return nullptr;
+
   Status error;
   auto process_sp = std::shared_ptr<ScriptedProcess>(
       new ScriptedProcess(target_sp, listener_sp, scripted_metadata, error));
 
   if (error.Fail() || !process_sp || !process_sp->m_interface_up) {
+    // CreateInstance returns nullptr on failure with no Status output
+    // parameter, so we must report the error via the diagnostic system for
+    // users to see it.
+    if (error.Fail()) {
+      Debugger::ReportError(
+          llvm::formatv("failed to create ScriptedProcess: {0}",
+                        error.AsCString())
+              .str(),
+          target_sp->GetDebugger().GetID());
+    }
     LLDB_LOGF(GetLog(LLDBLog::Process), "%s", error.AsCString());
     return nullptr;
   }
@@ -112,8 +129,9 @@ ScriptedProcess::ScriptedProcess(lldb::TargetSP target_sp,
       GetInterface().CreatePluginObject(m_scripted_metadata, exe_ctx);
 
   if (!obj_or_err) {
-    llvm::consumeError(obj_or_err.takeError());
-    error = Status::FromErrorString("Failed to create script object.");
+    std::string error_msg = llvm::toString(obj_or_err.takeError());
+    error = Status::FromErrorStringWithFormatv(
+        "failed to create script object: {0}", error_msg);
     return;
   }
 
@@ -189,10 +207,13 @@ Status ScriptedProcess::DoResume(RunDirection direction) {
 
 Status ScriptedProcess::DoAttach(const ProcessAttachInfo &attach_info) {
   Status error = GetInterface().Attach(attach_info);
+  if (error.Fail()) {
+    error = Status::FromErrorStringWithFormatv(
+        "failed to attach to scripted process: {0}", error.AsCString());
+    return error;
+  }
   SetPrivateState(eStateRunning);
   SetPrivateState(eStateStopped);
-  if (error.Fail())
-    return error;
   // NOTE: We need to set the PID before finishing to attach otherwise we will
   // hit an assert when calling the attach completion handler.
   DidLaunch();
@@ -224,8 +245,14 @@ size_t ScriptedProcess::DoReadMemory(lldb::addr_t addr, void *buf, size_t size,
   lldb::DataExtractorSP data_extractor_sp =
       GetInterface().ReadMemoryAtAddress(addr, size, error);
 
-  if (!data_extractor_sp || !data_extractor_sp->HasData() || error.Fail())
+  if (!data_extractor_sp || !data_extractor_sp->HasData() || error.Fail()) {
+    if (error.Fail()) {
+      error = Status::FromErrorStringWithFormatv(
+          "Failed to read memory from scripted process at 0x{0:x-}: {1}", addr,
+          error.AsCString());
+    }
     return 0;
+  }
 
   offset_t bytes_copied = data_extractor_sp->CopyByteOrderedData(
       0, data_extractor_sp->GetByteSize(), buf, size, GetByteOrder());
@@ -251,9 +278,15 @@ size_t ScriptedProcess::DoWriteMemory(lldb::addr_t vm_addr, const void *buf,
   lldb::offset_t bytes_written =
       GetInterface().WriteMemoryAtAddress(vm_addr, data_extractor_sp, error);
 
-  if (!bytes_written || bytes_written == LLDB_INVALID_OFFSET)
+  if (!bytes_written || bytes_written == LLDB_INVALID_OFFSET) {
+    if (error.Fail()) {
+      error = Status::FromErrorStringWithFormatv(
+          "failed to write memory to scripted process at 0x{0:x-}: {1}",
+          vm_addr, error.AsCString());
+    }
     return ScriptedInterface::ErrorWithMessage<size_t>(
         LLVM_PRETTY_FUNCTION, "Failed to copy write buffer to memory.", error);
+  }
 
   // FIXME: We should use the diagnostic system to report a warning if the
   // `bytes_written` is different from `size`.
@@ -374,9 +407,14 @@ bool ScriptedProcess::DoUpdateThreadList(ThreadList &old_thread_list,
     auto thread_or_error =
         ScriptedThread::Create(*this, object_sp->GetAsGeneric());
 
-    if (!thread_or_error)
-      return ScriptedInterface::ErrorWithMessage<bool>(
-          LLVM_PRETTY_FUNCTION, toString(thread_or_error.takeError()), error);
+    if (!thread_or_error) {
+      Debugger::ReportError(
+          llvm::formatv("failed to create scripted thread ({0}): {1}", idx,
+                        llvm::toString(thread_or_error.takeError()))
+              .str(),
+          GetTarget().GetDebugger().GetID());
+      return false;
+    }
 
     ThreadSP thread_sp = thread_or_error.get();
     lldbassert(thread_sp && "Couldn't initialize scripted thread.");
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.h b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h
index 8371180734217..9510f2f06dabd 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedProcess.h
+++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h
@@ -17,8 +17,6 @@
 
 #include "ScriptedThread.h"
 
-#include <mutex>
-
 namespace lldb_private {
 class ScriptedProcess : public Process {
 public:
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
index f87b37ca67e09..9ba9beaff66d1 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
@@ -11,6 +11,7 @@
 
 #include "Plugins/Process/Utility/RegisterContextThreadMemory.h"
 #include "Plugins/Process/Utility/StopInfoMachException.h"
+#include "lldb/Core/Debugger.h"
 #include "lldb/Target/OperatingSystem.h"
 #include "lldb/Target/Process.h"
 #include "lldb/Target/RegisterContext.h"
@@ -69,11 +70,8 @@ ScriptedThread::Create(ScriptedProcess &process,
   auto obj_or_err = scripted_thread_interface->CreatePluginObject(
       thread_metadata, exe_ctx, script_object);
 
-  if (!obj_or_err) {
-    llvm::consumeError(obj_or_err.takeError());
-    return llvm::createStringError(llvm::inconvertibleErrorCode(),
-                                   "Failed to create script object.");
-  }
+  if (!obj_or_err)
+    return obj_or_err.takeError();
 
   StructuredData::GenericSP owned_script_object_sp = *obj_or_err;
 
@@ -270,14 +268,15 @@ bool ScriptedThread::LoadArtificialStackFrames() {
       auto frame_from_script_obj_or_err = create_frame_from_script_object(idx);
 
       if (!frame_from_script_obj_or_err) {
-        return ScriptedInterface::ErrorWithMessage<bool>(
-            LLVM_PRETTY_FUNCTION,
-            llvm::Twine(
-                "Couldn't add artificial frame (" + llvm::Twine(idx) +
-                llvm::Twine(") to ScriptedThread StackFrameList: ") +
-                llvm::toString(frame_from_script_obj_or_err.takeError()))
+        llvm::consumeError(frame_from_dict_or_err.takeError());
+        Debugger::ReportError(
+            llvm::formatv(
+                "couldn't add artificial frame ({0}) to ScriptedThread "
+                "StackFrameList: {1}",
+                idx, llvm::toString(frame_from_script_obj_or_err.takeError()))
                 .str(),
-            error, LLDBLog::Thread);
+            GetProcess()->GetTarget().GetDebugger().GetID());
+        return false;
       } else {
         llvm::consumeError(frame_from_dict_or_err.takeError());
         synth_frame_sp = *frame_from_script_obj_or_err;
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index 3223d53e25c62..ce48f2468d380 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -50,7 +50,8 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     };
 
     AbstractMethodCheckerCases checker_case;
-    std::variant<std::monostate, InvalidArgumentCountPayload> payload;
+    std::variant<std::monostate, InvalidArgumentCountPayload, std::string>
+        payload;
   };
 
   llvm::Expected<FileSpec> GetScriptedModulePath() override {
@@ -145,9 +146,10 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
 
       auto arg_info_or_err = callable.GetArgInfo();
       if (!arg_info_or_err) {
-        llvm::consumeError(arg_info_or_err.takeError());
-        SET_CASE_AND_CONTINUE(method_name,
-                              AbstractMethodCheckerCases::eUnknownArgumentCount)
+        checker[method_name] = {
+            AbstractMethodCheckerCases::eUnknownArgumentCount,
+            ExtractPythonError(arg_info_or_err.takeError())};
+        continue;
       }
 
       PythonCallable::ArgInfo arg_info = *arg_info_or_err;
@@ -263,21 +265,28 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
 
         std::apply(
             [&init, &expected_return_object](auto &&...args) {
-              llvm::consumeError(expected_return_object.takeError());
-              expected_return_object = init(args...);
+              if (!expected_return_object)
+                llvm::consumeError(expected_return_object.takeError());
+              expected_return_object = init.Call(args...);
             },
             std::tuple_cat(transformed_args, std::make_tuple(dict)));
       } else {
         std::apply(
             [&init, &expected_return_object](auto &&...args) {
-              llvm::consumeError(expected_return_object.takeError());
-              expected_return_object = init(args...);
+              if (!expected_return_object)
+                llvm::consumeError(expected_return_object.takeError());
+              expected_return_object = init.Call(args...);
             },
             transformed_args);
       }
 
       if (!expected_return_object)
-        return expected_return_object.takeError();
+        // Drain the Python exception into a plain string while the GIL is
+        // still held: `PythonException` owns raw `PyObject*` references, and
+        // `py_lock` (and the GIL it holds) is released as this function
+        // returns, before the caller gets a chance to touch the error.
+        return llvm::createStringError(
+            ExtractPythonError(expected_return_object.takeError()));
       result = expected_return_object.get();
     }
 
@@ -323,13 +332,16 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
                                    obj_class_name.GetString(),
                                    method_checker.first)));
         break;
-      case AbstractMethodCheckerCases::eUnknownArgumentCount:
+      case AbstractMethodCheckerCases::eUnknownArgumentCount: {
+        const std::string *py_error =
+            std::get_if<std::string>(&method_checker.second.payload);
         abstract_method_errors = llvm::joinErrors(
             std::move(abstract_method_errors),
             std::move(create_error(
-                "Abstract method {0}.{1} has unknown argument count.",
-                obj_class_name.GetString(), method_checker.first)));
-        break;
+                "abstract method {0}.{1} has unknown argument count: {2}",
+                obj_class_name.GetString(), method_checker.first,
+                py_error ? *py_error : "<no further information>")));
+      } break;
       case AbstractMethodCheckerCases::eInvalidArgumentCount: {
         auto &payload_variant = method_checker.second.payload;
         if (!std::holds_alternative<
@@ -453,15 +465,26 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
         llvm::createStringError("not initialized");
     std::apply(
         [&method, &expected_return_object](auto &&...args) {
-          llvm::consumeError(expected_return_object.takeError());
-          expected_return_object = method(args...);
+          if (!expected_return_object)
+            llvm::consumeError(expected_return_object.takeError());
+          expected_return_object = method.Call(args...);
         },
         transformed_args);
 
     if (llvm::Error e = expected_return_object.takeError()) {
-      error = Status::FromError(std::move(e));
+      // TODO: Stringify `args` and include them in the message so users
+      // can see what was passed to the failing call (e.g.
+      // `read_memory_at_address(0x500000000, 4)`). Requires a SFINAE
+      // helper that falls back to a placeholder for types without a
+      // format_provider / operator<<.
+      error = Status::FromErrorString(ExtractPythonError(std::move(e)).c_str());
+
       return ErrorWithMessage<T>(
-          caller_signature, "python static method could not be called", error);
+          caller_signature,
+          llvm::formatv("python exception in {0} method '{1}'", class_name,
+                        method_name)
+              .str(),
+          error);
     }
 
     PythonObject py_return = std::move(expected_return_object.get());
@@ -478,6 +501,23 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
   }
 
 protected:
+  /// Extract detailed error message including Python backtrace if available.
+  ///
+  /// This helper processes llvm::Error objects that may contain PythonException
+  /// instances, extracting full Python backtraces when available.
+  ///
+  /// \param error The llvm::Error to extract information from.
+  /// \return A string containing the error message, including full Python
+  ///         backtrace if the error was a PythonException.
+  static std::string ExtractPythonError(llvm::Error error) {
+    std::string error_msg;
+    llvm::handleAllErrors(
+        std::move(error),
+        [&](python::PythonException &E) { error_msg = E.ReadBacktrace(); },
+        [&](const llvm::ErrorInfoBase &E) { error_msg = E.message(); });
+    return error_msg;
+  }
+
   template <typename T = StructuredData::ObjectSP>
   T ExtractValueFromPythonObject(python::PythonObject &p, Status &error) {
     return p.CreateStructuredObject();
@@ -538,15 +578,29 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
                   std::make_index_sequence<sizeof...(Args) + 1>{},
                   [&implementor, &method_name,
                    &expected_return_object](auto &&...call_args) {
-                    llvm::consumeError(expected_return_object.takeError());
+                    if (!expected_return_object)
+                      llvm::consumeError(expected_return_object.takeError());
                     expected_return_object = implementor.CallMethod(
                         method_name.data(), call_args...);
                   });
 
     if (llvm::Error e = expected_return_object.takeError()) {
-      error = Status::FromError(std::move(e));
-      return ErrorWithMessage<T>(caller_signature,
-                                 "python method could not be called", error);
+      // TODO: Stringify `args` and include them in the message so users
+      // can see what was passed to the failing call (e.g.
+      // `read_memory_at_address(0x500000000, 4)`). Requires a SFINAE
+      // helper that falls back to a placeholder for types without a
+      // format_provider / operator<<.
+      error = Status::FromErrorString(ExtractPythonError(std::move(e)).c_str());
+
+      return ErrorWithMessage<T>(
+          caller_signature,
+          llvm::formatv("python exception in {0} method '{1}'",
+                        GetScriptedMetadata()
+                            ? GetScriptedMetadata()->GetClassName()
+                            : "<unknown>",
+                        method_name)
+              .str(),
+          error);
     }
 
     PythonObject py_return = std::move(expected_return_object.get());
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index c7065df8212a4..010a0ad015c12 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -2745,9 +2745,11 @@ bool ScriptInterpreterPythonImpl::RunScriptBasedCommand(
         cmd_retobj, exe_ctx_ref_sp);
   }
 
-  if (!ret_val)
+  if (!ret_val) {
     error = Status::FromErrorString("unable to execute script function");
-  else if (cmd_retobj.GetStatus() == eReturnStatusFailed)
+    return false;
+  }
+  if (cmd_retobj.GetStatus() == eReturnStatusFailed)
     return false;
 
   error.Clear();
diff --git a/lldb/source/Target/ScriptedThreadPlan.cpp b/lldb/source/Target/ScriptedThreadPlan.cpp
index 7b0a6bd3a7279..499a6df21f1d9 100644
--- a/lldb/source/Target/ScriptedThreadPlan.cpp
+++ b/lldb/source/Target/ScriptedThreadPlan.cpp
@@ -20,6 +20,7 @@
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/Log.h"
 #include "lldb/Utility/State.h"
+#include "llvm/Support/FormatVariadic.h"
 
 using namespace lldb;
 using namespace lldb_private;
@@ -89,6 +90,10 @@ void ScriptedThreadPlan::DidPush() {
                                                       this->shared_from_this());
     if (!obj_or_err) {
       m_error_str = llvm::toString(obj_or_err.takeError());
+      Debugger::ReportError(
+          llvm::formatv("failed to create ScriptedThreadPlan: {0}", m_error_str)
+              .str(),
+          m_process.GetTarget().GetDebugger().GetID());
       SetPlanComplete(false);
     } else
       m_implementation_sp = *obj_or_err;
diff --git a/lldb/source/Target/StackFrameRecognizer.cpp b/lldb/source/Target/StackFrameRecognizer.cpp
index 589f3805e3a67..e1faa7b3e8728 100644
--- a/lldb/source/Target/StackFrameRecognizer.cpp
+++ b/lldb/source/Target/StackFrameRecognizer.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "lldb/Target/StackFrameRecognizer.h"
+#include "lldb/Core/Debugger.h"
 #include "lldb/Core/Module.h"
 #include "lldb/Interpreter/Interfaces/ScriptedStackFrameRecognizerInterface.h"
 #include "lldb/Interpreter/ScriptInterpreter.h"
@@ -14,6 +15,7 @@
 #include "lldb/Target/StackFrame.h"
 #include "lldb/Utility/RegularExpression.h"
 #include "lldb/Utility/ScriptedMetadata.h"
+#include "llvm/Support/FormatVariadic.h"
 
 using namespace lldb;
 using namespace lldb_private;
@@ -53,7 +55,14 @@ ScriptedStackFrameRecognizer::ScriptedStackFrameRecognizer(
   ScriptedMetadata scripted_metadata(m_python_class, nullptr);
   auto obj_or_err = m_interface_sp->CreatePluginObject(scripted_metadata);
   if (!obj_or_err) {
-    llvm::consumeError(obj_or_err.takeError());
+    // CreatePluginObject has no error-return channel back to the command
+    // handler that requested this recognizer, so report the detailed error
+    // (which may include a Python backtrace) via the diagnostic system.
+    Debugger::ReportError(
+        llvm::formatv("failed to create ScriptedStackFrameRecognizer: {0}",
+                      llvm::toString(obj_or_err.takeError()))
+            .str(),
+        interpreter->GetDebugger().GetID());
     m_interface_sp.reset();
   }
 }
diff --git a/lldb/test/API/commands/command/script/TestCommandScript.py b/lldb/test/API/commands/command/script/TestCommandScript.py
index eb1584c64c90d..05fbb632ab197 100644
--- a/lldb/test/API/commands/command/script/TestCommandScript.py
+++ b/lldb/test/API/commands/command/script/TestCommandScript.py
@@ -201,6 +201,19 @@ def cleanup():
             substrs=[bad_class_name],
         )
 
+        # `-f` doesn't validate the function reference until the command is
+        # actually invoked. Make sure a bogus reference (e.g. a typo) produces
+        # a visible error at invocation time instead of silently doing
+        # nothing.
+        self.runCmd(
+            "command script add -f nonexistent_module.nonexistent_function typo_cmd"
+        )
+        self.expect(
+            "typo_cmd",
+            error=True,
+            substrs=["unable to execute script function"],
+        )
+
     def test_persistence(self):
         """
         Ensure that function arguments meaningfully persist (and do not crash!)
diff --git a/lldb/test/API/functionalities/scripted_extensions/Makefile b/lldb/test/API/functionalities/scripted_extensions/Makefile
new file mode 100644
index 0000000000000..10495940055b6
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py b/lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py
new file mode 100644
index 0000000000000..0812483621f2d
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py
@@ -0,0 +1,227 @@
+"""
+Verify that exceptions raised inside scripted extension affordance methods
+(or missing required abstract methods) are surfaced to the user.
+
+For entry points with no return-channel for errors
+(`ScriptedProcess::CreateInstance`, `OperatingSystemPython` ctor,
+`ScriptedThreadPlan::DidPush`, `BreakpointResolverScripted` ctor,
+`ScriptedStackFrameRecognizer` ctor) the diagnostic is broadcast via
+`Debugger::ReportError` and asserted on a listener.
+
+`ScriptedThread::Create` and `ScriptedFrame::Create` also return an
+`llvm::Expected`, but their only callers
+(`ScriptedProcess::DoUpdateThreadList` and
+`ScriptedThread::LoadArtificialStackFrames`, respectively) have no
+return-channel of their own, so those errors are likewise broadcast via
+`Debugger::ReportError` rather than propagated further.
+
+Entry points that already return an `llvm::Expected` / `Status` all the way
+to a user-visible surface (`ScriptedFrameProvider::CreateInstance`,
+`StopHookScripted::SetScriptCallback`) propagate the detailed error through
+their return type; tests for those are tracked as follow-up.
+"""
+
+import os
+
+import lldb
+import lldbsuite.test.lldbutil as lldbutil
+from lldbsuite.test.decorators import expectedFailureAll
+from lldbsuite.test.lldbtest import TestBase
+
+
+class TestScriptedExtensionsDiagnostics(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def setUp(self):
+        TestBase.setUp(self)
+        self.broadcaster = self.dbg.GetBroadcaster()
+        self.listener = lldbutil.start_listening_from(
+            self.broadcaster,
+            lldb.SBDebugger.eBroadcastBitWarning | lldb.SBDebugger.eBroadcastBitError,
+        )
+        script_path = os.path.join(
+            self.getSourceDir(), "malformed_scripted_extensions.py"
+        )
+        self.runCmd("command script import " + script_path)
+
+    def assert_diagnostic(self, expected_substring):
+        event = lldbutil.fetch_next_event(self, self.listener, self.broadcaster)
+        data = lldb.SBDebugger.GetDiagnosticFromEvent(event)
+        self.assertTrue(data.IsValid(), "event has diagnostic data")
+        message = data.GetValueForKey("message").GetStringValue(4096)
+        self.assertIn(expected_substring, message)
+
+    def create_target(self):
+        self.build()
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target, "valid target")
+        return target
+
+    # ------------------------------------------------------------------
+    # ScriptedProcess - reports via ScriptedProcess::CreateInstance
+    # ------------------------------------------------------------------
+
+    def test_scripted_process_missing_methods(self):
+        """A ScriptedProcess missing abstract methods should emit a
+        diagnostic naming the missing method."""
+        target = self.create_target()
+        launch_info = lldb.SBLaunchInfo(None)
+        launch_info.SetProcessPluginName("ScriptedProcess")
+        launch_info.SetScriptedProcessClassName(
+            "malformed_scripted_extensions.MissingMethodsScriptedProcess"
+        )
+        error = lldb.SBError()
+        target.Launch(launch_info, error)
+        self.assertTrue(error.Fail(), "launch should fail")
+        self.assert_diagnostic("read_memory_at_address")
+
+    # ------------------------------------------------------------------
+    # ScriptedProcess::launch() - reports via the normal SBError return
+    # channel (Process::DoLaunch), unlike CreateInstance's construction
+    # failures above, which have no return channel and go through
+    # Debugger::ReportError instead.
+    # ------------------------------------------------------------------
+
+    def test_scripted_process_launch_exception(self):
+        """An explicitly raised exception from `launch()` should surface
+        through the normal SBError return channel."""
+        target = self.create_target()
+        launch_info = lldb.SBLaunchInfo(None)
+        launch_info.SetProcessPluginName("ScriptedProcess")
+        launch_info.SetScriptedProcessClassName(
+            "malformed_scripted_extensions.ExceptionScriptedProcess"
+        )
+        error = lldb.SBError()
+        target.Launch(launch_info, error)
+        self.assertTrue(error.Fail(), "launch should fail")
+        self.assertIn("intentional exception from launch()", error.GetCString())
+
+    def test_scripted_process_launch_runtime_error(self):
+        """A natural Python runtime error (e.g. a typo'd name), as opposed
+        to a deliberately raised exception, should surface just as
+        readably: this is the other half of what users hit in practice --
+        either an implementation function returns something that doesn't
+        make sense, or (this case) they mistyped a name and Python
+        complains about it at runtime."""
+        target = self.create_target()
+        launch_info = lldb.SBLaunchInfo(None)
+        launch_info.SetProcessPluginName("ScriptedProcess")
+        launch_info.SetScriptedProcessClassName(
+            "malformed_scripted_extensions.TypoScriptedProcess"
+        )
+        error = lldb.SBError()
+        target.Launch(launch_info, error)
+        self.assertTrue(error.Fail(), "launch should fail")
+        self.assertIn("NameError", error.GetCString())
+        self.assertIn("this_name_is_never_defined", error.GetCString())
+
+    # ------------------------------------------------------------------
+    # BreakpointResolverScripted - reports via
+    # BreakpointResolverScripted::CreateImplementationIfNeeded.
+    # `m_error` is set but never surfaced to the user, so ReportError is
+    # the only user-visible channel.
+    # ------------------------------------------------------------------
+
+    def test_scripted_breakpoint_resolver_init_failure(self):
+        target = self.create_target()
+        target.BreakpointCreateFromScript(
+            "malformed_scripted_extensions.ExceptionInitScriptedBreakpointResolver",
+            lldb.SBStructuredData(),
+            lldb.SBFileSpecList(),
+            lldb.SBFileSpecList(),
+        )
+        self.assert_diagnostic("intentional exception from __init__()")
+
+    # ------------------------------------------------------------------
+    # ScriptedThreadPlan - reports via ScriptedThreadPlan::DidPush. The
+    # plan stores the error in m_error_str but never surfaces it; the
+    # diagnostic is the user-visible channel.
+    # ------------------------------------------------------------------
+
+    def test_scripted_thread_plan_init_failure(self):
+        target = self.create_target()
+        process = target.LaunchSimple(None, None, self.get_process_working_directory())
+        self.assertTrue(process, "valid process")
+        thread = process.GetSelectedThread()
+        self.assertTrue(thread, "valid thread")
+        thread.StepUsingScriptedThreadPlan(
+            "malformed_scripted_extensions.ExceptionInitScriptedThreadPlan"
+        )
+        self.assert_diagnostic("intentional exception from __init__()")
+
+    # ------------------------------------------------------------------
+    # ScriptedThread - reports via ScriptedProcess::DoUpdateThreadList,
+    # which propagates ScriptedThread::Create's Expected error through
+    # Debugger::ReportError (DoUpdateThreadList has no return-channel back
+    # to its caller).
+    # ------------------------------------------------------------------
+
+    def test_scripted_thread_missing_methods(self):
+        """A scripted thread object returned from `get_threads_info()` that
+        is missing a required abstract method should emit a diagnostic
+        naming the missing method."""
+        target = self.create_target()
+        launch_info = lldb.SBLaunchInfo(None)
+        launch_info.SetProcessPluginName("ScriptedProcess")
+        launch_info.SetScriptedProcessClassName(
+            "malformed_scripted_extensions.ThreadListScriptedProcess"
+        )
+        error = lldb.SBError()
+        target.Launch(launch_info, error)
+        self.assert_diagnostic("get_stop_reason")
+
+    # ------------------------------------------------------------------
+    # ScriptedFrame - reports via ScriptedThread::LoadArtificialStackFrames,
+    # which propagates ScriptedFrame::Create's Expected error through
+    # Debugger::ReportError (LoadArtificialStackFrames' return value is
+    # discarded by its only caller, RefreshStateAfterStop).
+    # ------------------------------------------------------------------
+
+    def test_scripted_frame_missing_methods(self):
+        """A scripted frame object returned from a thread's
+        `get_stackframes()` that is missing a required abstract method
+        should emit a diagnostic naming the missing method."""
+        target = self.create_target()
+        launch_info = lldb.SBLaunchInfo(None)
+        launch_info.SetProcessPluginName("ScriptedProcess")
+        launch_info.SetScriptedProcessClassName(
+            "malformed_scripted_extensions.StackFrameScriptedProcess"
+        )
+        error = lldb.SBError()
+        target.Launch(launch_info, error)
+        self.assert_diagnostic("get_id")
+
+    # ------------------------------------------------------------------
+    # ScriptedStackFrameRecognizer - reports via
+    # ScriptedStackFrameRecognizer's constructor, which has no
+    # error-return channel back to `frame recognizer add`.
+    # ------------------------------------------------------------------
+
+    def test_scripted_stack_frame_recognizer_init_failure(self):
+        target = self.create_target()
+        self.runCmd(
+            "frame recognizer add -l "
+            "malformed_scripted_extensions.ExceptionScriptedStackFrameRecognizer "
+            "-s a.out -n main"
+        )
+        self.assert_diagnostic("intentional exception from __init__()")
+
+    # ------------------------------------------------------------------
+    # The remaining entry point has no plugin implementation yet.
+    # ------------------------------------------------------------------
+
+    def test_operating_system_missing_methods(self):
+        self.build()
+        lldbutil.run_to_source_breakpoint(self, "break here", lldb.SBFileSpec("main.c"))
+        os_plugin_path = os.path.join(
+            self.getSourceDir(), "os_plugin_missing_methods.py"
+        )
+        self.runCmd(
+            "settings set target.process.python-os-plugin-path " + os_plugin_path
+        )
+        self.runCmd("thread list")
+        self.assert_diagnostic("get_thread_info")
+
+    @expectedFailureAll(bugnumber="ScriptedPlatform has no plugin implementation yet")
+    def test_scripted_platform_missing_methods(self):
+        self.assert_diagnostic("list_processes")
diff --git a/lldb/test/API/functionalities/scripted_extensions/main.c b/lldb/test/API/functionalities/scripted_extensions/main.c
new file mode 100644
index 0000000000000..ba45ee316cd42
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/main.c
@@ -0,0 +1,3 @@
+int main() {
+  return 0; // break here
+}
diff --git a/lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py b/lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py
new file mode 100644
index 0000000000000..d980e46792733
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py
@@ -0,0 +1,325 @@
+"""
+Intentionally malformed scripted extensions used by
+TestScriptedExtensionsDiagnostics.
+
+Each class either omits a required abstract method or raises a Python
+exception from one of its affordance methods. The corresponding test asserts
+that LLDB surfaces those errors to the user instead of silently swallowing
+them.
+"""
+
+import lldb
+from lldb.plugins.scripted_process import ScriptedProcess, ScriptedThread
+
+# ---------------------------------------------------------------------------
+# Scripted Process
+# ---------------------------------------------------------------------------
+
+
+class MissingMethodsScriptedProcess:
+    """Missing required abstract method `read_memory_at_address`."""
+
+    def __init__(self, exe_ctx, args):
+        self.exe_ctx = exe_ctx
+        self.args = args
+
+    def get_scripted_thread_plugin(self):
+        return None
+
+    def is_alive(self):
+        return True
+
+
+class ExceptionScriptedProcess:
+    """All abstract methods present, but `launch` raises."""
+
+    def __init__(self, exe_ctx, args):
+        self.exe_ctx = exe_ctx
+        self.args = args
+
+    def get_scripted_thread_plugin(self):
+        return None
+
+    def is_alive(self):
+        return True
+
+    def read_memory_at_address(self, addr, size, error):
+        return None
+
+    def launch(self):
+        raise RuntimeError("intentional exception from launch()")
+
+
+class TypoScriptedProcess:
+    """All abstract methods present, but `launch` has a typo (references an
+    undefined name) instead of an explicit `raise`, exercising a natural
+    Python runtime error (NameError) rather than a deliberately raised
+    exception."""
+
+    def __init__(self, exe_ctx, args):
+        self.exe_ctx = exe_ctx
+        self.args = args
+
+    def get_scripted_thread_plugin(self):
+        return None
+
+    def is_alive(self):
+        return True
+
+    def read_memory_at_address(self, addr, size, error):
+        return None
+
+    def launch(self):
+        return this_name_is_never_defined
+
+
+# ---------------------------------------------------------------------------
+# Scripted Thread
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedThread:
+    """Missing required abstract method `get_stop_reason`."""
+
+    def __init__(self, process, args):
+        self.process = process
+        self.args = args
+
+    def get_thread_id(self):
+        raise ValueError("intentional exception from get_thread_id()")
+
+    def get_register_context(self):
+        return ""
+
+    def get_name(self):
+        return "ExceptionScriptedThread"
+
+    def get_state(self):
+        return 0
+
+
+class ThreadListScriptedProcess:
+    """All abstract methods present, but `get_threads_info` hands back a
+    thread object (`ExceptionScriptedThread`) missing a required abstract
+    method."""
+
+    def __init__(self, exe_ctx, args):
+        self.exe_ctx = exe_ctx
+        self.args = args
+
+    def get_scripted_thread_plugin(self):
+        return None
+
+    def is_alive(self):
+        return True
+
+    def read_memory_at_address(self, addr, size, error):
+        return None
+
+    def launch(self):
+        return lldb.SBError()
+
+    def get_threads_info(self):
+        return {1: ExceptionScriptedThread(self, None)}
+
+
+class MissingMethodsScriptedFrame:
+    """Missing required abstract method `get_id`."""
+
+    def __init__(self, thread, args):
+        self.thread = thread
+        self.args = args
+
+
+class StackFrameScriptedThread(ScriptedThread):
+    """A real, valid scripted thread (borrows the base class's default
+    register-info plumbing so construction succeeds), but `get_stackframes`
+    returns a scripted frame object (`MissingMethodsScriptedFrame`) missing a
+    required abstract method."""
+
+    def __init__(self, process, args):
+        super().__init__(process, args)
+
+    def get_stop_reason(self):
+        return {"type": lldb.eStopReasonTrace, "data": {}}
+
+    def get_register_context(self):
+        total_bytes = sum(
+            r["bitsize"] // 8 for r in self.get_register_info()["registers"]
+        )
+        return "\x00" * total_bytes
+
+    def get_stackframes(self):
+        return [MissingMethodsScriptedFrame(self, None)]
+
+
+class StackFrameScriptedProcess(ScriptedProcess):
+    """All abstract methods present, but `get_threads_info` hands back a
+    thread (`StackFrameScriptedThread`) whose `get_stackframes` yields a
+    malformed scripted frame."""
+
+    def __init__(self, exe_ctx, args):
+        super().__init__(exe_ctx, args)
+
+    def get_scripted_thread_plugin(self):
+        return None
+
+    def is_alive(self):
+        return True
+
+    def read_memory_at_address(self, addr, size, error):
+        return None
+
+    def get_threads_info(self):
+        return {1: StackFrameScriptedThread(self, None)}
+
+
+# ---------------------------------------------------------------------------
+# Scripted Platform
+# ---------------------------------------------------------------------------
+
+
+class MissingMethodsScriptedPlatform:
+    """Missing required abstract method `list_processes`."""
+
+    def __init__(self, exe_ctx, args):
+        self.exe_ctx = exe_ctx
+        self.args = args
+
+
+class ExceptionScriptedPlatform:
+    def __init__(self, exe_ctx, args):
+        self.exe_ctx = exe_ctx
+        self.args = args
+
+    def list_processes(self):
+        raise RuntimeError("intentional exception from list_processes()")
+
+    def get_process_info(self, pid):
+        return None
+
+    def launch_process(self, launch_info):
+        return None
+
+    def kill_process(self, pid):
+        return None
+
+
+# ---------------------------------------------------------------------------
+# Scripted Frame Provider
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedFrameProvider:
+    def __init__(self, frames, args):
+        self.frames = frames
+        self.args = args
+
+    def get_num_frames(self):
+        raise RuntimeError("intentional exception from get_num_frames()")
+
+    def get_frame_at_index(self, idx):
+        return None
+
+
+# ---------------------------------------------------------------------------
+# Scripted Thread Plan
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedThreadPlan:
+    def __init__(self, thread_plan, args):
+        self.thread_plan = thread_plan
+        self.args = args
+
+    def explains_stop(self, event):
+        raise RuntimeError("intentional exception from explains_stop()")
+
+    def should_stop(self, event):
+        return True
+
+    def is_stale(self):
+        return False
+
+
+class ExceptionInitScriptedThreadPlan:
+    """`__init__` raises."""
+
+    def __init__(self, thread_plan, args):
+        raise RuntimeError("intentional exception from __init__()")
+
+
+# ---------------------------------------------------------------------------
+# Scripted Breakpoint Resolver
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedBreakpointResolver:
+    def __init__(self, bkpt, args):
+        self.bkpt = bkpt
+        self.args = args
+
+    def __callback__(self, sym_ctx):
+        raise RuntimeError("intentional exception from __callback__()")
+
+    def get_short_help(self):
+        return "Exception breakpoint resolver"
+
+
+class ExceptionInitScriptedBreakpointResolver:
+    """`__init__` raises."""
+
+    def __init__(self, bkpt, args):
+        raise RuntimeError("intentional exception from __init__()")
+
+
+# ---------------------------------------------------------------------------
+# Scripted Stop Hook
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedStopHook:
+    def __init__(self, target, args):
+        self.target = target
+        self.args = args
+
+    def handle_stop(self, exe_ctx, stream):
+        raise RuntimeError("intentional exception from handle_stop()")
+
+
+# ---------------------------------------------------------------------------
+# Scripted Stack Frame Recognizer
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedStackFrameRecognizer:
+    """`__init__` raises."""
+
+    def __init__(self):
+        raise RuntimeError("intentional exception from __init__()")
+
+
+# ---------------------------------------------------------------------------
+# Operating System
+# ---------------------------------------------------------------------------
+
+
+class MissingMethodsOperatingSystem:
+    """Missing required abstract method `get_thread_info`."""
+
+    def __init__(self, process):
+        self.process = process
+
+
+class ExceptionOperatingSystem:
+    def __init__(self, process):
+        self.process = process
+
+    def get_thread_info(self):
+        raise RuntimeError("intentional exception from get_thread_info()")
+
+    def get_register_info(self):
+        return {}
+
+    def get_register_data(self, tid):
+        return b""
diff --git a/lldb/test/API/functionalities/scripted_extensions/os_plugin_missing_methods.py b/lldb/test/API/functionalities/scripted_extensions/os_plugin_missing_methods.py
new file mode 100644
index 0000000000000..a559d611855e1
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/os_plugin_missing_methods.py
@@ -0,0 +1,16 @@
+"""
+Standalone OperatingSystem plugin module for
+TestScriptedExtensionsDiagnostics.test_operating_system_missing_methods.
+
+`OperatingSystemPython` always resolves the plugin class as
+`<module>.OperatingSystemPlugIn`, derived from the
+`target.process.python-os-plugin-path` setting's basename, so this scenario
+needs its own file rather than a class in malformed_scripted_extensions.py.
+"""
+
+
+class OperatingSystemPlugIn:
+    """Missing required abstract method `get_thread_info`."""
+
+    def __init__(self, process):
+        self.process = process
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/register_command_status/TestFrameProviderRegisterCommandStatus.py b/lldb/test/API/functionalities/scripted_frame_provider/register_command_status/TestFrameProviderRegisterCommandStatus.py
index 82c2d46ef40a2..184dbd7ac199c 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/register_command_status/TestFrameProviderRegisterCommandStatus.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/register_command_status/TestFrameProviderRegisterCommandStatus.py
@@ -37,3 +37,22 @@ def test_register_command_succeeds(self):
             "target frame-provider register -C frame_provider.MinimalProvider",
             substrs=["successfully registered scripted frame provider"],
         )
+
+    def test_static_method_exception_does_not_escape(self):
+        target = self.dbg.CreateTarget(None)
+        self.assertTrue(target.IsValid())
+
+        provider_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+        self.runCmd("command script import " + provider_path)
+        self.runCmd(
+            "target frame-provider register "
+            "-C frame_provider.FailingDescriptionProvider"
+        )
+
+        result = lldb.SBCommandReturnObject()
+        self.dbg.GetCommandInterpreter().HandleCommand(
+            "target frame-provider list", result
+        )
+
+        self.assertTrue(result.Succeeded(), result.GetError())
+        self.assertIn("FailingDescriptionProvider", result.GetOutput())
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/register_command_status/frame_provider.py b/lldb/test/API/functionalities/scripted_frame_provider/register_command_status/frame_provider.py
index b66060bc8a77f..4998e1beab428 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/register_command_status/frame_provider.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/register_command_status/frame_provider.py
@@ -14,3 +14,9 @@ def get_description():
 
     def get_frame_at_index(self, index):
         return None
+
+
+class FailingDescriptionProvider(MinimalProvider):
+    @staticmethod
+    def get_description():
+        raise ValueError("scripted frame provider description failed")
diff --git a/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py b/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py
index e8449c0faa928..f90a7422d270a 100644
--- a/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py
+++ b/lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py
@@ -158,7 +158,11 @@ def cleanup():
         buff = process.ReadMemory(addr, 4, error)
         self.assertEqual(buff, None)
         self.assertTrue(error.Fail())
-        self.assertEqual(error.GetCString(), "This is an invalid scripted process!")
+        self.assertEqual(
+            error.GetCString(),
+            "Failed to read memory from scripted process at 0x500000000: "
+            "This is an invalid scripted process!",
+        )
 
         with open(log_file, "r") as f:
             log = f.read()
diff --git a/lldb/test/API/functionalities/step_scripted/Steps.py b/lldb/test/API/functionalities/step_scripted/Steps.py
index e2a03c9988111..b36bb5f5f9048 100644
--- a/lldb/test/API/functionalities/step_scripted/Steps.py
+++ b/lldb/test/API/functionalities/step_scripted/Steps.py
@@ -37,6 +37,11 @@ def queue_child_thread_plan(self):
         return self.thread_plan.QueueThreadPlanForStepOut(0)
 
 
+class FailingConstructor:
+    def __init__(self, thread_plan, args_data):
+        raise ValueError("scripted plan construction failed")
+
+
 class StepScripted(StepWithChild):
     def __init__(self, thread_plan, dict):
         StepWithChild.__init__(self, thread_plan)
diff --git a/lldb/test/API/functionalities/step_scripted/TestStepScripted.py b/lldb/test/API/functionalities/step_scripted/TestStepScripted.py
index 343236a9e4e3c..a08a39a710596 100644
--- a/lldb/test/API/functionalities/step_scripted/TestStepScripted.py
+++ b/lldb/test/API/functionalities/step_scripted/TestStepScripted.py
@@ -28,6 +28,23 @@ def test_scripted_step_out(self):
         self.build()
         self.step_out_with_scripted_plan("Steps.StepScripted")
 
+    def test_constructor_error_preserves_traceback(self):
+        self.build()
+        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
+            self, "Set a breakpoint here", self.main_source_file
+        )
+
+        result = lldb.SBCommandReturnObject()
+        self.dbg.GetCommandInterpreter().HandleCommand(
+            "thread step-scripted -C Steps.FailingConstructor", result
+        )
+
+        self.assertFalse(result.Succeeded())
+        self.assertIn("Traceback (most recent call last)", result.GetError())
+        self.assertIn(
+            "ValueError: scripted plan construction failed", result.GetError()
+        )
+
     def step_out_with_scripted_plan(self, name):
         (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
             self, "Set a breakpoint here", self.main_source_file

>From ab2263ab42b85e625b48677f8b15068f45a023d6 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Sat, 1 Aug 2026 16:23:21 -0700
Subject: [PATCH 08/16] [lldb] Reimplement PythonCallable::GetArgInfo without
 executing Python code (#213378)

`b05a5d0a` added an arity-trimming step to the shared
`ScriptedPythonInterface::Dispatch`: extensions are now allowed to
define methods with trailing parameters as optional
(`num_children(self)` vs. `num_children(self, max_count)`), so before
calling into a method, `Dispatch` needs to know how many positional
arguments it actually accepts and drop any trailing ones we'd otherwise
pass.

That check calls `PythonCallable::GetArgInfo`, which ran a whole
embedded Python script through `inspect.signature` on every call, since
every scripted-extension dispatch goes through it.

For the common case `GetArgInfo()` actually needs to handle fast (plain
Python functions/methods, classes used as constructors, and callable
instances defining `__call__`, i.e. everything `Dispatch<T>()` and
`CreatePluginObject()` ever pass it), the answer is available as plain
data attributes, with no Python bytecode execution required:
`__func__`/`__self__` to unwrap bound methods, and `__code__`'s
`co_argcount`/`co_flags` for the actual positional-argument count and
varargs bit. For a class, that means `__init__` -- except a class may
customize `__new__` instead and leave `__init__` untouched, in which
case `object.__init__` becomes lenient about extra arguments.

Anything else still lacking `__code__` can fall back to the original
`inspect.signature()`-based implementation, now exposed as
`PythonCallable::GetArgInfoFromInspectSignature()` so it can be used
separately.

rdar://183776556

---------

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
(cherry picked from commit 1ac9b999f8b521b5d6d82cf1a19858bc40a18c6a)
---
 .../Python/PythonDataObjects.cpp              | 108 ++++++++++++++++--
 .../Python/PythonDataObjects.h                |   9 ++
 .../Python/ScriptInterpreterPython.cpp        |   8 ++
 .../Python/PythonDataObjectsTests.cpp         |  37 +++++-
 4 files changed, 154 insertions(+), 8 deletions(-)

diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp
index fba98abf9e83b..b423cc68a2230 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.cpp
@@ -840,22 +840,116 @@ def main(f):
     return ArgInfo(count, varargs)
 )";
 
-Expected<PythonCallable::ArgInfo> PythonCallable::GetArgInfo() const {
-  ArgInfo result = {};
-  if (!IsValid())
-    return nullDeref();
-
+// inspect.signature() is deeply recursive and expensive in C-stack terms;
+// reentrant scripted callbacks dispatched through GetArgInfo() can turn
+// that into a fatal stack overflow instead of a catchable Python
+// RecursionError. GetArgInfo() never calls this itself; callers fall back
+// to it explicitly when they need to handle callables its cheaper,
+// attribute-only approach can't (e.g. builtins).
+Expected<PythonCallable::ArgInfo>
+PythonCallable::GetArgInfoFromInspectSignature(const PythonCallable &callable) {
+  PythonCallable::ArgInfo result = {};
   // no need to synchronize access to this global, we already have the GIL
   static PythonScript get_arg_info(get_arg_info_script);
-  Expected<PythonObject> pyarginfo = get_arg_info(*this);
+  Expected<PythonObject> pyarginfo = get_arg_info(callable);
   if (!pyarginfo)
     return pyarginfo.takeError();
   long long count =
       cantFail(As<long long>(pyarginfo.get().GetAttribute("count")));
   bool has_varargs =
       cantFail(As<bool>(pyarginfo.get().GetAttribute("has_varargs")));
-  result.max_positional_args = has_varargs ? ArgInfo::UNBOUNDED : count;
+  result.max_positional_args =
+      has_varargs ? PythonCallable::ArgInfo::UNBOUNDED : count;
+  return result;
+}
+
+// GetArgInfo()'s branches, top to bottom (`func` is what each branch ends up
+// introspecting; the final step is always func.__code__.co_argcount/co_flags):
+//
+//   callable
+//   |-- has __self__          -> func = __func__           (bound method;
+//   |                             fails for slot wrappers, e.g. (1).__add__)
+//   |-- has __code__ already  -> func = callable            (plain function)
+//   `-- neither
+//       |-- is a class
+//       |   |-- __init__ has __code__ -> func = __init__
+//       |   `-- else: check __new__ too (object.__init__ is lenient about
+//       |       extra args once __new__ is overridden)
+//       |       |-- __new__ has __code__ -> func = __new__
+//       |       `-- else                  -> ArgInfo{0} (object's defaults)
+//       `-- is an instance
+//           `-- func = __call__ (unwrap __func__ if bound)
+//               `-- no __code__ -> error (e.g. a builtin)
+Expected<PythonCallable::ArgInfo> PythonCallable::GetArgInfo() const {
+  if (!IsValid())
+    return nullDeref();
 
+  PythonObject func = *this;
+  bool implicit_first_arg = false;
+  if (HasAttribute("__self__")) {
+    implicit_first_arg = true;
+    Expected<PythonObject> func_or_err = GetAttribute("__func__");
+    if (!func_or_err)
+      return func_or_err.takeError();
+    func = *func_or_err;
+  } else if (!HasAttribute("__code__")) {
+    implicit_first_arg = true;
+    if (PyType_Check(m_py_obj)) {
+      Expected<PythonObject> init_or_err = GetAttribute("__init__");
+      if (!init_or_err)
+        return init_or_err.takeError();
+      func = *init_or_err;
+      if (!func.HasAttribute("__code__")) {
+        // __init__ is still object.__init__. A class may customize
+        // __new__ instead and leave __init__ untouched, which makes
+        // object.__init__ lenient about extra arguments -- so check
+        // __new__ too before concluding there are none.
+        Expected<PythonObject> new_or_err = GetAttribute("__new__");
+        if (!new_or_err)
+          return new_or_err.takeError();
+        func = *new_or_err;
+        if (!func.HasAttribute("__code__"))
+          return ArgInfo{0};
+      }
+    } else {
+      Expected<PythonObject> call_or_err = GetAttribute("__call__");
+      if (!call_or_err)
+        return call_or_err.takeError();
+      func = *call_or_err;
+      if (func.HasAttribute("__self__")) {
+        Expected<PythonObject> inner_or_err = func.GetAttribute("__func__");
+        if (!inner_or_err)
+          return inner_or_err.takeError();
+        func = *inner_or_err;
+      }
+      if (!func.HasAttribute("__code__"))
+        return llvm::createStringError("__call__ has no __code__");
+    }
+  }
+
+  Expected<PythonObject> code_or_err = func.GetAttribute("__code__");
+  if (!code_or_err)
+    return code_or_err.takeError();
+  PythonObject code = *code_or_err;
+
+  Expected<long long> argcount =
+      As<long long>(code.GetAttribute("co_argcount"));
+  if (!argcount)
+    return argcount.takeError();
+  Expected<long long> flags = As<long long>(code.GetAttribute("co_flags"));
+  if (!flags)
+    return flags.takeError();
+
+  ArgInfo result = {};
+  // Mirrors CPython's CO_VARARGS from <code.h>, which isn't reliably
+  // visible across the Python versions/platforms this file builds against.
+  constexpr long long kCoFlagVarArgs = 0x04;
+  if (*flags & kCoFlagVarArgs) {
+    result.max_positional_args = ArgInfo::UNBOUNDED;
+  } else {
+    long long count = *argcount - (implicit_first_arg ? 1 : 0);
+    result.max_positional_args = count > 0 ? static_cast<unsigned>(count) : 0;
+  }
   return result;
 }
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.h b/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.h
index 3f2b869bcfb0a..7b29d002fa503 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/PythonDataObjects.h
@@ -615,6 +615,15 @@ class PythonCallable : public TypedPythonObject<PythonCallable> {
 
   llvm::Expected<ArgInfo> GetArgInfo() const;
 
+  // Always derives ArgInfo via a Python-level inspect.signature() call,
+  // regardless of whether the callable's argument count/varargs bit could
+  // have been read directly off its data attributes. GetArgInfo() prefers
+  // the cheaper attribute-based path and only falls back to this for
+  // callables it can't introspect that way (e.g. builtins); exposed
+  // separately so that fallback behavior can be tested directly.
+  static llvm::Expected<ArgInfo>
+  GetArgInfoFromInspectSignature(const PythonCallable &callable);
+
   PythonObject operator()();
 
   PythonObject operator()(std::initializer_list<PyObject *> args);
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index 010a0ad015c12..8abdf41cb112e 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -1300,6 +1300,14 @@ ScriptInterpreterPythonImpl::GetMaxPositionalArgumentsForCallable(
                                    callable_name.str().c_str());
   }
   llvm::Expected<PythonCallable::ArgInfo> arg_info = pfunc.GetArgInfo();
+  if (!arg_info) {
+    // `-f` may point at a builtin, unlike other GetArgInfo() callers.
+    LLDB_LOG_ERROR(GetLog(LLDBLog::Script), arg_info.takeError(),
+                   "GetArgInfo failed for callable {1}, falling back to "
+                   "inspect.signature: {0}",
+                   callable_name);
+    arg_info = PythonCallable::GetArgInfoFromInspectSignature(pfunc);
+  }
   if (!arg_info)
     return arg_info.takeError();
   return arg_info.get().max_positional_args;
diff --git a/lldb/unittests/ScriptInterpreter/Python/PythonDataObjectsTests.cpp b/lldb/unittests/ScriptInterpreter/Python/PythonDataObjectsTests.cpp
index 24ed721049e67..b46d672656f21 100644
--- a/lldb/unittests/ScriptInterpreter/Python/PythonDataObjectsTests.cpp
+++ b/lldb/unittests/ScriptInterpreter/Python/PythonDataObjectsTests.cpp
@@ -735,6 +735,17 @@ class NewStyle(object):
   def __init__(self, one, two, three):
     pass
 
+class NoConstructorAtAll:
+  pass
+
+class NewOnlyVarArgs:
+  def __new__(cls, *args, **kwargs):
+    return super().__new__(cls)
+
+class NewOnlyFixedArgs:
+  def __new__(cls, a, b):
+    return super().__new__(cls)
+
 )";
     PyObject *o =
         RunString(script, Py_file_input, globals.get(), globals.get());
@@ -782,13 +793,37 @@ class NewStyle(object):
     arginfo = newstyle.get().GetArgInfo();
     ASSERT_THAT_EXPECTED(arginfo, llvm::Succeeded());
     EXPECT_EQ(arginfo.get().max_positional_args, 3u);
+
+    // Neither __init__ nor __new__ overridden: object.__init__ truly takes
+    // no extra arguments here.
+    auto no_ctor = As<PythonCallable>(globals.GetItem("NoConstructorAtAll"));
+    ASSERT_THAT_EXPECTED(no_ctor, llvm::Succeeded());
+    arginfo = no_ctor.get().GetArgInfo();
+    ASSERT_THAT_EXPECTED(arginfo, llvm::Succeeded());
+    EXPECT_EQ(arginfo.get().max_positional_args, 0u);
+
+    // __init__ not overridden, but __new__ is: object.__init__ becomes
+    // lenient about extra arguments once __new__ is overridden, so the
+    // argument count has to come from __new__, not from __init__ alone.
+    auto new_varargs = As<PythonCallable>(globals.GetItem("NewOnlyVarArgs"));
+    ASSERT_THAT_EXPECTED(new_varargs, llvm::Succeeded());
+    arginfo = new_varargs.get().GetArgInfo();
+    ASSERT_THAT_EXPECTED(arginfo, llvm::Succeeded());
+    EXPECT_EQ(arginfo.get().max_positional_args,
+              PythonCallable::ArgInfo::UNBOUNDED);
+
+    auto new_fixed = As<PythonCallable>(globals.GetItem("NewOnlyFixedArgs"));
+    ASSERT_THAT_EXPECTED(new_fixed, llvm::Succeeded());
+    arginfo = new_fixed.get().GetArgInfo();
+    ASSERT_THAT_EXPECTED(arginfo, llvm::Succeeded());
+    EXPECT_EQ(arginfo.get().max_positional_args, 2u);
   }
 
   {
     auto builtins = PythonModule::BuiltinsModule();
     auto hex = As<PythonCallable>(builtins.GetAttribute("hex"));
     ASSERT_THAT_EXPECTED(hex, llvm::Succeeded());
-    auto arginfo = hex.get().GetArgInfo();
+    auto arginfo = PythonCallable::GetArgInfoFromInspectSignature(hex.get());
     ASSERT_THAT_EXPECTED(arginfo, llvm::Succeeded());
     EXPECT_EQ(arginfo.get().max_positional_args, 1u);
   }

>From 27af0f4e2789c0a1307fb107e626435e5557959f Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Mon, 3 Aug 2026 00:22:55 -0700
Subject: [PATCH 09/16] [lldb] Skip flaky
 test_circular_dependency_evaluate_expression_in_get_frame (#213610)

This provider's identity-forwarding pattern intermittently hits a known
frame-identity-aliasing bug in ScriptedFrameProvider::GetFrameAtIndex,
tracked in https://github.com/llvm/llvm-project/pull/208992.

(cherry picked from commit 3be1dff0910dc71df932380c9091a10a54ca9fe0)
---
 .../circular_dependency/TestFrameProviderCircularDependency.py   | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lldb/test/API/functionalities/scripted_frame_provider/circular_dependency/TestFrameProviderCircularDependency.py b/lldb/test/API/functionalities/scripted_frame_provider/circular_dependency/TestFrameProviderCircularDependency.py
index af8e567f4d5e7..46413105a80ce 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/circular_dependency/TestFrameProviderCircularDependency.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/circular_dependency/TestFrameProviderCircularDependency.py
@@ -164,6 +164,7 @@ def test_circular_dependency_handle_command_in_init(self):
             )
 
     @expectedFailureWindowsAndNoLLDBServer(bugnumber="llvm.org/pr24778")
+    @skipIf(bugnumber="https://github.com/llvm/llvm-project/pull/208992")
     def test_circular_dependency_evaluate_expression_in_get_frame(self):
         """
         Test that calling EvaluateExpression in get_frame_at_index doesn't

>From a5c3237a418fb4d6ed3da7ad10cb4a537ab8b110 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Mon, 3 Aug 2026 21:55:39 -0700
Subject: [PATCH 10/16] [lldb/Interpreter] Remove Interpreter's layering
 dependency on API (#213754)

(cherry picked from commit ed3d4a78ba8b23ab5d3fb629029695a03818d254)
---
 lldb/include/lldb/API/SBAttachInfo.h          |   2 +-
 lldb/include/lldb/API/SBBreakpoint.h          |   2 +-
 lldb/include/lldb/API/SBBreakpointLocation.h  |   2 +-
 lldb/include/lldb/API/SBCommandReturnObject.h |   2 +-
 lldb/include/lldb/API/SBData.h                |   2 +-
 lldb/include/lldb/API/SBDebugger.h            |   2 +-
 lldb/include/lldb/API/SBError.h               |   2 +-
 lldb/include/lldb/API/SBEvent.h               |   2 +-
 lldb/include/lldb/API/SBExecutionContext.h    |   2 +-
 lldb/include/lldb/API/SBFrame.h               |   2 +-
 lldb/include/lldb/API/SBFrameList.h           |   2 +-
 lldb/include/lldb/API/SBLaunchInfo.h          |   2 +-
 lldb/include/lldb/API/SBMemoryRegionInfo.h    |   2 +-
 lldb/include/lldb/API/SBStream.h              |   2 +-
 lldb/include/lldb/API/SBSymbolContext.h       |   4 +-
 lldb/include/lldb/API/SBTarget.h              |   2 +-
 lldb/include/lldb/API/SBThread.h              |   2 +-
 lldb/include/lldb/API/SBValue.h               |   2 +-
 .../Interfaces/ScriptedFrameInterface.h       |   1 -
 .../lldb/Interpreter/ScriptInterpreter.h      |  64 --------
 lldb/include/lldb/Utility/StreamString.h      |   2 +-
 lldb/include/lldb/lldb-forward.h              |   1 +
 lldb/source/API/CMakeLists.txt                |   1 +
 lldb/source/API/ScriptInterpreterBridge.cpp   | 147 ++++++++++++++++++
 lldb/source/API/ScriptInterpreterBridge.h     |  78 ++++++++++
 lldb/source/Interpreter/CMakeLists.txt        |  14 ++
 lldb/source/Interpreter/ScriptInterpreter.cpp | 117 --------------
 .../Interfaces/ScriptedPythonInterface.cpp    |  44 +++---
 28 files changed, 283 insertions(+), 224 deletions(-)
 create mode 100644 lldb/source/API/ScriptInterpreterBridge.cpp
 create mode 100644 lldb/source/API/ScriptInterpreterBridge.h

diff --git a/lldb/include/lldb/API/SBAttachInfo.h b/lldb/include/lldb/API/SBAttachInfo.h
index c18655fee77e0..94f687cd56649 100644
--- a/lldb/include/lldb/API/SBAttachInfo.h
+++ b/lldb/include/lldb/API/SBAttachInfo.h
@@ -199,7 +199,7 @@ class LLDB_API SBAttachInfo {
   friend class SBTarget;
   friend class SBPlatform;
 
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
 
   lldb_private::ProcessAttachInfo &ref();
 
diff --git a/lldb/include/lldb/API/SBBreakpoint.h b/lldb/include/lldb/API/SBBreakpoint.h
index fe19ba998ea67..95c32fbb583bc 100644
--- a/lldb/include/lldb/API/SBBreakpoint.h
+++ b/lldb/include/lldb/API/SBBreakpoint.h
@@ -171,7 +171,7 @@ class LLDB_API SBBreakpoint {
   friend class SBBreakpointName;
   friend class SBTarget;
 
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
   friend class lldb_private::python::SWIGBridge;
 
   SBBreakpoint(const lldb::BreakpointSP &bp_sp);
diff --git a/lldb/include/lldb/API/SBBreakpointLocation.h b/lldb/include/lldb/API/SBBreakpointLocation.h
index 9b0d4839aca82..3255a51d9269f 100644
--- a/lldb/include/lldb/API/SBBreakpointLocation.h
+++ b/lldb/include/lldb/API/SBBreakpointLocation.h
@@ -24,7 +24,7 @@ class SWIGBridge;
 namespace lldb {
 
 class LLDB_API SBBreakpointLocation {
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
 
 public:
   SBBreakpointLocation();
diff --git a/lldb/include/lldb/API/SBCommandReturnObject.h b/lldb/include/lldb/API/SBCommandReturnObject.h
index b80a11b52c77f..4a7ea3f955305 100644
--- a/lldb/include/lldb/API/SBCommandReturnObject.h
+++ b/lldb/include/lldb/API/SBCommandReturnObject.h
@@ -145,7 +145,7 @@ class LLDB_API SBCommandReturnObject {
 
   friend class lldb_private::CommandPluginInterfaceImplementation;
   friend class lldb_private::python::SWIGBridge;
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
 
   SBCommandReturnObject(lldb_private::CommandReturnObject &ref);
 
diff --git a/lldb/include/lldb/API/SBData.h b/lldb/include/lldb/API/SBData.h
index 89a699f2f713a..d2fe33a3e0b83 100644
--- a/lldb/include/lldb/API/SBData.h
+++ b/lldb/include/lldb/API/SBData.h
@@ -154,7 +154,7 @@ class LLDB_API SBData {
   friend class SBTarget;
   friend class SBValue;
 
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
 
   lldb::DataExtractorSP m_opaque_sp;
 };
diff --git a/lldb/include/lldb/API/SBDebugger.h b/lldb/include/lldb/API/SBDebugger.h
index 3e302f121f5ec..bb13413e7a556 100644
--- a/lldb/include/lldb/API/SBDebugger.h
+++ b/lldb/include/lldb/API/SBDebugger.h
@@ -678,7 +678,7 @@ class LLDB_API SBDebugger {
 protected:
   friend class lldb_private::CommandPluginInterfaceImplementation;
   friend class lldb_private::python::SWIGBridge;
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
   friend class lldb_private::SystemInitializerFull;
 
   SBDebugger(const lldb::DebuggerSP &debugger_sp);
diff --git a/lldb/include/lldb/API/SBError.h b/lldb/include/lldb/API/SBError.h
index dd8c0f939775f..5f2717120006a 100644
--- a/lldb/include/lldb/API/SBError.h
+++ b/lldb/include/lldb/API/SBError.h
@@ -109,7 +109,7 @@ class LLDB_API SBError {
   friend class SBValueList;
   friend class SBWatchpoint;
 
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
   friend class lldb_private::python::SWIGBridge;
 
   SBError(lldb_private::Status &&error);
diff --git a/lldb/include/lldb/API/SBEvent.h b/lldb/include/lldb/API/SBEvent.h
index 85b401ca8cc10..99f13fc90124d 100644
--- a/lldb/include/lldb/API/SBEvent.h
+++ b/lldb/include/lldb/API/SBEvent.h
@@ -74,7 +74,7 @@ class LLDB_API SBEvent {
   friend class SBThread;
   friend class SBWatchpoint;
 
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
   friend class lldb_private::python::SWIGBridge;
 
   SBEvent(lldb::EventSP &event_sp);
diff --git a/lldb/include/lldb/API/SBExecutionContext.h b/lldb/include/lldb/API/SBExecutionContext.h
index 20584271ff36c..3b2d0e0aa139d 100644
--- a/lldb/include/lldb/API/SBExecutionContext.h
+++ b/lldb/include/lldb/API/SBExecutionContext.h
@@ -57,7 +57,7 @@ class LLDB_API SBExecutionContext {
 protected:
   friend class SBInstructionList;
   friend class lldb_private::python::SWIGBridge;
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
 
   lldb_private::ExecutionContextRef *get() const;
 
diff --git a/lldb/include/lldb/API/SBFrame.h b/lldb/include/lldb/API/SBFrame.h
index 37c85b0732613..4d67ac74eab12 100644
--- a/lldb/include/lldb/API/SBFrame.h
+++ b/lldb/include/lldb/API/SBFrame.h
@@ -234,7 +234,7 @@ class LLDB_API SBFrame {
   friend class SBThread;
   friend class SBValue;
 
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
   friend class lldb_private::python::SWIGBridge;
   friend class lldb_private::lua::SWIGBridge;
 
diff --git a/lldb/include/lldb/API/SBFrameList.h b/lldb/include/lldb/API/SBFrameList.h
index 0039ffb1f863f..ef38510d9fa66 100644
--- a/lldb/include/lldb/API/SBFrameList.h
+++ b/lldb/include/lldb/API/SBFrameList.h
@@ -78,7 +78,7 @@ class LLDB_API SBFrameList {
 
   friend class lldb_private::python::SWIGBridge;
   friend class lldb_private::lua::SWIGBridge;
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
 
 private:
   SBFrameList(const lldb::StackFrameListSP &frame_list_sp);
diff --git a/lldb/include/lldb/API/SBLaunchInfo.h b/lldb/include/lldb/API/SBLaunchInfo.h
index 06e72efc30f9f..043a9a54734a1 100644
--- a/lldb/include/lldb/API/SBLaunchInfo.h
+++ b/lldb/include/lldb/API/SBLaunchInfo.h
@@ -210,7 +210,7 @@ class LLDB_API SBLaunchInfo {
   friend class SBPlatform;
   friend class SBTarget;
 
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
 
   const lldb_private::ProcessLaunchInfo &ref() const;
   void set_ref(const lldb_private::ProcessLaunchInfo &info);
diff --git a/lldb/include/lldb/API/SBMemoryRegionInfo.h b/lldb/include/lldb/API/SBMemoryRegionInfo.h
index dc5aa0858e1e3..034279f6e5593 100644
--- a/lldb/include/lldb/API/SBMemoryRegionInfo.h
+++ b/lldb/include/lldb/API/SBMemoryRegionInfo.h
@@ -132,7 +132,7 @@ class LLDB_API SBMemoryRegionInfo {
   friend class SBProcess;
   friend class SBMemoryRegionInfoList;
   friend class SBSaveCoreOptions;
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
 
   lldb_private::MemoryRegionInfo &ref();
 
diff --git a/lldb/include/lldb/API/SBStream.h b/lldb/include/lldb/API/SBStream.h
index 21f9d21e0e717..1ba375b600df6 100644
--- a/lldb/include/lldb/API/SBStream.h
+++ b/lldb/include/lldb/API/SBStream.h
@@ -108,7 +108,7 @@ class LLDB_API SBStream {
   friend class SBValue;
   friend class SBWatchpoint;
 
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
 
   lldb_private::Stream *operator->();
 
diff --git a/lldb/include/lldb/API/SBSymbolContext.h b/lldb/include/lldb/API/SBSymbolContext.h
index 19f29c629d094..c67f5ba0e0658 100644
--- a/lldb/include/lldb/API/SBSymbolContext.h
+++ b/lldb/include/lldb/API/SBSymbolContext.h
@@ -66,7 +66,7 @@ class LLDB_API SBSymbolContext {
   friend class SBTarget;
   friend class SBSymbolContextList;
 
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
   friend class lldb_private::python::SWIGBridge;
 
   SBSymbolContext(const lldb_private::SymbolContext &sc_ptr);
@@ -81,8 +81,6 @@ class LLDB_API SBSymbolContext {
 
   lldb_private::SymbolContext *get() const;
 
-  friend class lldb_private::ScriptInterpreter;
-
 private:
   std::unique_ptr<lldb_private::SymbolContext> m_opaque_up;
 };
diff --git a/lldb/include/lldb/API/SBTarget.h b/lldb/include/lldb/API/SBTarget.h
index fd795c843330e..84cbcfb4e69d2 100644
--- a/lldb/include/lldb/API/SBTarget.h
+++ b/lldb/include/lldb/API/SBTarget.h
@@ -1067,7 +1067,7 @@ class LLDB_API SBTarget {
 
   friend class lldb_private::python::SWIGBridge;
   friend class lldb_private::lua::SWIGBridge;
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
 
   // Constructors are private, use static Target::Create function to create an
   // instance of this class.
diff --git a/lldb/include/lldb/API/SBThread.h b/lldb/include/lldb/API/SBThread.h
index 97d3b838492fb..a5edb529c2c6a 100644
--- a/lldb/include/lldb/API/SBThread.h
+++ b/lldb/include/lldb/API/SBThread.h
@@ -256,7 +256,7 @@ class LLDB_API SBThread {
   friend class SBThreadPlan;
   friend class SBTrace;
 
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
   friend class lldb_private::python::SWIGBridge;
 
   SBThread(const lldb::ThreadSP &lldb_object_sp);
diff --git a/lldb/include/lldb/API/SBValue.h b/lldb/include/lldb/API/SBValue.h
index 16a9f0837454c..ae8374881cf15 100644
--- a/lldb/include/lldb/API/SBValue.h
+++ b/lldb/include/lldb/API/SBValue.h
@@ -516,7 +516,7 @@ class LLDB_API SBValue {
              bool use_synthetic, const char *name);
 
 protected:
-  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::ScriptInterpreterBridge;
 
 private:
   typedef std::shared_ptr<lldb_private::ValueImpl> ValueImplSP;
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h
index 43914ef705dbf..b2a37bf497504 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h
@@ -10,7 +10,6 @@
 #define LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEINTERFACE_H
 
 #include "ScriptedInterface.h"
-#include "lldb/API/SBValueList.h"
 #include "lldb/Core/StructuredDataImpl.h"
 #include "lldb/Symbol/SymbolContext.h"
 #include "lldb/lldb-private.h"
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 925b7b08e3291..7ad530b3233f2 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -9,21 +9,6 @@
 #ifndef LLDB_INTERPRETER_SCRIPTINTERPRETER_H
 #define LLDB_INTERPRETER_SCRIPTINTERPRETER_H
 
-#include "lldb/API/SBAttachInfo.h"
-#include "lldb/API/SBBreakpoint.h"
-#include "lldb/API/SBBreakpointLocation.h"
-#include "lldb/API/SBCommandReturnObject.h"
-#include "lldb/API/SBData.h"
-#include "lldb/API/SBDebugger.h"
-#include "lldb/API/SBError.h"
-#include "lldb/API/SBEvent.h"
-#include "lldb/API/SBExecutionContext.h"
-#include "lldb/API/SBFrameList.h"
-#include "lldb/API/SBLaunchInfo.h"
-#include "lldb/API/SBMemoryRegionInfo.h"
-#include "lldb/API/SBStream.h"
-#include "lldb/API/SBSymbolContext.h"
-#include "lldb/API/SBThread.h"
 #include "lldb/Breakpoint/BreakpointOptions.h"
 #include "lldb/Core/PluginInterface.h"
 #include "lldb/Core/SearchFilter.h"
@@ -37,7 +22,6 @@
 #include "lldb/Interpreter/Interfaces/ScriptedProcessInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
 #include "lldb/Interpreter/ScriptObject.h"
-#include "lldb/Symbol/SymbolContext.h"
 #include "lldb/Utility/Broadcaster.h"
 #include "lldb/Utility/Status.h"
 #include "lldb/Utility/StructuredData.h"
@@ -531,54 +515,6 @@ class ScriptInterpreter : public PluginInterface {
   virtual SanitizedScriptingModuleName
   GetSanitizedScriptingModuleName(llvm::StringRef name);
 
-  lldb::DataExtractorSP
-  GetDataExtractorFromSBData(const lldb::SBData &data) const;
-
-  Status GetStatusFromSBError(const lldb::SBError &error) const;
-
-  Event *GetOpaqueTypeFromSBEvent(const lldb::SBEvent &event) const;
-
-  lldb::StreamSP GetOpaqueTypeFromSBStream(const lldb::SBStream &stream) const;
-
-  lldb::ThreadSP GetOpaqueTypeFromSBThread(const lldb::SBThread &exe_ctx) const;
-
-  lldb::StackFrameSP GetOpaqueTypeFromSBFrame(const lldb::SBFrame &frame) const;
-
-  SymbolContext
-  GetOpaqueTypeFromSBSymbolContext(const lldb::SBSymbolContext &sym_ctx) const;
-
-  lldb::BreakpointSP
-  GetOpaqueTypeFromSBBreakpoint(const lldb::SBBreakpoint &breakpoint) const;
-
-  lldb::BreakpointLocationSP GetOpaqueTypeFromSBBreakpointLocation(
-      const lldb::SBBreakpointLocation &break_loc) const;
-
-  CommandReturnObject *GetOpaqueTypeFromSBCommandReturnObject(
-      const lldb::SBCommandReturnObject &cmd_retobj) const;
-
-  lldb::DebuggerSP
-  GetOpaqueTypeFromSBDebugger(const lldb::SBDebugger &debugger) const;
-
-  lldb::ProcessAttachInfoSP
-  GetOpaqueTypeFromSBAttachInfo(const lldb::SBAttachInfo &attach_info) const;
-
-  lldb::ProcessLaunchInfoSP
-  GetOpaqueTypeFromSBLaunchInfo(const lldb::SBLaunchInfo &launch_info) const;
-
-  std::optional<MemoryRegionInfo> GetOpaqueTypeFromSBMemoryRegionInfo(
-      const lldb::SBMemoryRegionInfo &mem_region) const;
-
-  lldb::ExecutionContextRefSP GetOpaqueTypeFromSBExecutionContext(
-      const lldb::SBExecutionContext &exe_ctx) const;
-
-  lldb::StackFrameListSP
-  GetOpaqueTypeFromSBFrameList(const lldb::SBFrameList &exe_ctx) const;
-
-  lldb::ValueObjectSP
-  GetOpaqueTypeFromSBValue(const lldb::SBValue &value) const;
-
-  lldb::TargetSP GetOpaqueTypeFromSBTarget(const lldb::SBTarget &target) const;
-
   /// Get the debugger associated with this script interpreter.
   Debugger &GetDebugger() { return m_debugger; }
   const Debugger &GetDebugger() const { return m_debugger; }
diff --git a/lldb/include/lldb/Utility/StreamString.h b/lldb/include/lldb/Utility/StreamString.h
index 1a6444fc29c24..5fcda832d4cf8 100644
--- a/lldb/include/lldb/Utility/StreamString.h
+++ b/lldb/include/lldb/Utility/StreamString.h
@@ -47,7 +47,7 @@ class StreamString : public Stream {
   void FillLastLineToColumn(uint32_t column, char fill_char);
 
 protected:
-  friend class ScriptInterpreter;
+  friend class ScriptInterpreterBridge;
 
   std::string m_packet;
   size_t WriteImpl(const void *s, size_t length) override;
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index 2a4044e9a9b88..47362915d6a56 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -188,6 +188,7 @@ class RichManglingContext;
 class SaveCoreOptions;
 class Scalar;
 class ScriptInterpreter;
+class ScriptInterpreterBridge;
 class ScriptInterpreterLocker;
 class ScriptedFrameInterface;
 class ScriptedFrameProviderInterface;
diff --git a/lldb/source/API/CMakeLists.txt b/lldb/source/API/CMakeLists.txt
index 15c52aab41368..1d2d2aa2a6f5d 100644
--- a/lldb/source/API/CMakeLists.txt
+++ b/lldb/source/API/CMakeLists.txt
@@ -115,6 +115,7 @@ add_lldb_library(liblldb SHARED ${option_framework}
   SBVariablesOptions.cpp
   SBWatchpoint.cpp
   SBWatchpointOptions.cpp
+  ScriptInterpreterBridge.cpp
   SystemInitializerFull.cpp
 
   ADDITIONAL_HEADER_DIRS
diff --git a/lldb/source/API/ScriptInterpreterBridge.cpp b/lldb/source/API/ScriptInterpreterBridge.cpp
new file mode 100644
index 0000000000000..e818a0c424d16
--- /dev/null
+++ b/lldb/source/API/ScriptInterpreterBridge.cpp
@@ -0,0 +1,147 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "ScriptInterpreterBridge.h"
+#include "API/SBCommandReturnObjectImpl.h"
+#include "lldb/API/SBAttachInfo.h"
+#include "lldb/API/SBBreakpoint.h"
+#include "lldb/API/SBBreakpointLocation.h"
+#include "lldb/API/SBCommandReturnObject.h"
+#include "lldb/API/SBData.h"
+#include "lldb/API/SBDebugger.h"
+#include "lldb/API/SBError.h"
+#include "lldb/API/SBEvent.h"
+#include "lldb/API/SBExecutionContext.h"
+#include "lldb/API/SBFrame.h"
+#include "lldb/API/SBFrameList.h"
+#include "lldb/API/SBLaunchInfo.h"
+#include "lldb/API/SBMemoryRegionInfo.h"
+#include "lldb/API/SBStream.h"
+#include "lldb/API/SBSymbolContext.h"
+#include "lldb/API/SBTarget.h"
+#include "lldb/API/SBThread.h"
+#include "lldb/API/SBValue.h"
+#include "lldb/Host/ProcessLaunchInfo.h"
+#include "lldb/Interpreter/CommandReturnObject.h"
+#include "lldb/Target/ExecutionContext.h"
+#include "lldb/Utility/StreamString.h"
+#include "lldb/ValueObject/ValueObject.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+lldb::DataExtractorSP
+ScriptInterpreterBridge::GetDataExtractor(const lldb::SBData &data) {
+  return data.m_opaque_sp;
+}
+
+lldb::BreakpointSP
+ScriptInterpreterBridge::GetBreakpoint(const lldb::SBBreakpoint &breakpoint) {
+  return breakpoint.m_opaque_wp.lock();
+}
+
+lldb::BreakpointLocationSP ScriptInterpreterBridge::GetBreakpointLocation(
+    const lldb::SBBreakpointLocation &break_loc) {
+  return break_loc.m_opaque_wp.lock();
+}
+
+CommandReturnObject *ScriptInterpreterBridge::GetCommandReturnObject(
+    const lldb::SBCommandReturnObject &cmd_retobj) {
+  return cmd_retobj.m_opaque_up->get();
+}
+
+lldb::DebuggerSP
+ScriptInterpreterBridge::GetDebugger(const lldb::SBDebugger &debugger) {
+  return debugger.m_opaque_sp;
+}
+
+lldb::ProcessAttachInfoSP ScriptInterpreterBridge::GetProcessAttachInfo(
+    const lldb::SBAttachInfo &attach_info) {
+  return attach_info.m_opaque_sp;
+}
+
+lldb::ProcessLaunchInfoSP ScriptInterpreterBridge::GetProcessLaunchInfo(
+    const lldb::SBLaunchInfo &launch_info) {
+  return std::make_shared<ProcessLaunchInfo>(
+      *reinterpret_cast<ProcessLaunchInfo *>(launch_info.m_opaque_sp.get()));
+}
+
+Status ScriptInterpreterBridge::GetStatus(const lldb::SBError &error) {
+  if (error.m_opaque_up)
+    return error.m_opaque_up->Clone();
+
+  return Status();
+}
+
+lldb::ThreadSP
+ScriptInterpreterBridge::GetThread(const lldb::SBThread &thread) {
+  if (thread.m_opaque_sp)
+    return thread.m_opaque_sp->GetThreadSP();
+  return nullptr;
+}
+
+lldb::StackFrameSP
+ScriptInterpreterBridge::GetStackFrame(const lldb::SBFrame &frame) {
+  if (frame.m_opaque_sp)
+    return frame.m_opaque_sp->GetFrameSP();
+  return nullptr;
+}
+
+Event *ScriptInterpreterBridge::GetEvent(const lldb::SBEvent &event) {
+  return event.m_opaque_ptr;
+}
+
+lldb::StreamSP
+ScriptInterpreterBridge::GetStream(const lldb::SBStream &stream) {
+  if (stream.m_opaque_up) {
+    lldb::StreamSP s = std::make_shared<lldb_private::StreamString>();
+    *s << reinterpret_cast<StreamString *>(stream.m_opaque_up.get())->m_packet;
+    return s;
+  }
+
+  return nullptr;
+}
+
+SymbolContext ScriptInterpreterBridge::GetSymbolContext(
+    const lldb::SBSymbolContext &sb_sym_ctx) {
+  if (sb_sym_ctx.m_opaque_up)
+    return *sb_sym_ctx.m_opaque_up;
+  return {};
+}
+
+std::optional<lldb_private::MemoryRegionInfo>
+ScriptInterpreterBridge::GetMemoryRegionInfo(
+    const lldb::SBMemoryRegionInfo &mem_region) {
+  if (!mem_region.m_opaque_up)
+    return std::nullopt;
+  return *mem_region.m_opaque_up.get();
+}
+
+lldb::ExecutionContextRefSP ScriptInterpreterBridge::GetExecutionContextRef(
+    const lldb::SBExecutionContext &exe_ctx) {
+  return exe_ctx.m_exe_ctx_sp;
+}
+
+lldb::StackFrameListSP ScriptInterpreterBridge::GetStackFrameList(
+    const lldb::SBFrameList &frame_list) {
+  return frame_list.m_opaque_sp;
+}
+
+lldb::TargetSP
+ScriptInterpreterBridge::GetTarget(const lldb::SBTarget &target) {
+  return target.m_opaque_sp;
+}
+
+lldb::ValueObjectSP
+ScriptInterpreterBridge::GetValueObject(const lldb::SBValue &value) {
+  if (!value.m_opaque_sp)
+    return lldb::ValueObjectSP();
+
+  lldb_private::ValueLocker locker;
+  return locker.GetLockedSP(*value.m_opaque_sp);
+}
diff --git a/lldb/source/API/ScriptInterpreterBridge.h b/lldb/source/API/ScriptInterpreterBridge.h
new file mode 100644
index 0000000000000..d0dbb049a6aff
--- /dev/null
+++ b/lldb/source/API/ScriptInterpreterBridge.h
@@ -0,0 +1,78 @@
+//===-- ScriptInterpreterBridge.h ------------------------------*- C++ -*-===//
+//
+// 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_SOURCE_API_SCRIPTINTERPRETERBRIDGE_H
+#define LLDB_SOURCE_API_SCRIPTINTERPRETERBRIDGE_H
+
+#include "lldb/API/SBDefines.h"
+#include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Target/MemoryRegionInfo.h"
+#include "lldb/Utility/Status.h"
+#include "lldb/lldb-forward.h"
+#include <optional>
+
+namespace lldb_private {
+
+class CommandReturnObject;
+class Event;
+
+/// Unwraps the opaque internal object held by an SB (public API) instance,
+/// for scripting-language plugins that need to convert values passed across
+/// the script callback boundary back to their lldb_private form. Every SB
+/// class this needs to reach into grants friendship to this class alone, so
+/// that access to API internals stays confined to this single bridge rather
+/// than spreading across the Interpreter layer.
+class ScriptInterpreterBridge {
+public:
+  static lldb::DataExtractorSP GetDataExtractor(const lldb::SBData &data);
+
+  static Status GetStatus(const lldb::SBError &error);
+
+  static Event *GetEvent(const lldb::SBEvent &event);
+
+  static lldb::StreamSP GetStream(const lldb::SBStream &stream);
+
+  static lldb::ThreadSP GetThread(const lldb::SBThread &thread);
+
+  static lldb::StackFrameSP GetStackFrame(const lldb::SBFrame &frame);
+
+  static SymbolContext GetSymbolContext(const lldb::SBSymbolContext &sym_ctx);
+
+  static lldb::BreakpointSP GetBreakpoint(const lldb::SBBreakpoint &breakpoint);
+
+  static lldb::BreakpointLocationSP
+  GetBreakpointLocation(const lldb::SBBreakpointLocation &break_loc);
+
+  static CommandReturnObject *
+  GetCommandReturnObject(const lldb::SBCommandReturnObject &cmd_retobj);
+
+  static lldb::DebuggerSP GetDebugger(const lldb::SBDebugger &debugger);
+
+  static lldb::ProcessAttachInfoSP
+  GetProcessAttachInfo(const lldb::SBAttachInfo &attach_info);
+
+  static lldb::ProcessLaunchInfoSP
+  GetProcessLaunchInfo(const lldb::SBLaunchInfo &launch_info);
+
+  static std::optional<MemoryRegionInfo>
+  GetMemoryRegionInfo(const lldb::SBMemoryRegionInfo &mem_region);
+
+  static lldb::ExecutionContextRefSP
+  GetExecutionContextRef(const lldb::SBExecutionContext &exe_ctx);
+
+  static lldb::StackFrameListSP
+  GetStackFrameList(const lldb::SBFrameList &frame_list);
+
+  static lldb::ValueObjectSP GetValueObject(const lldb::SBValue &value);
+
+  static lldb::TargetSP GetTarget(const lldb::SBTarget &target);
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_API_SCRIPTINTERPRETERBRIDGE_H
diff --git a/lldb/source/Interpreter/CMakeLists.txt b/lldb/source/Interpreter/CMakeLists.txt
index 4bb59d7550b0d..8a9605b568865 100644
--- a/lldb/source/Interpreter/CMakeLists.txt
+++ b/lldb/source/Interpreter/CMakeLists.txt
@@ -65,12 +65,26 @@ add_lldb_library(lldbInterpreter NO_PLUGIN_DEPENDENCIES
     Support
   LINK_LIBS
     lldbInterpreterInterfaces
+    lldbBreakpoint
     lldbCommands
     lldbCore
     lldbDataFormatters
     lldbHost
+    lldbSymbol
     lldbTarget
     lldbUtility
+    lldbValueObject
+  ALLOWED_INTERNAL_DEPENDENCIES
+    lldbInterpreterInterfaces
+    lldbBreakpoint
+    lldbCommands
+    lldbCore
+    lldbDataFormatters
+    lldbHost
+    lldbSymbol
+    lldbTarget
+    lldbUtility
+    lldbValueObject
   )
 
 add_dependencies(lldbInterpreter
diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp
index c16716bbc00fb..a53f3b744f02e 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 #include "lldb/Interpreter/ScriptInterpreter.h"
-#include "API/SBCommandReturnObjectImpl.h"
 #include "lldb/Core/Debugger.h"
 #include "lldb/Host/ConnectionFileDescriptor.h"
 #include "lldb/Host/Pipe.h"
@@ -17,7 +16,6 @@
 #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)
 #include "lldb/Host/windows/ConnectionGenericFileWindows.h"
@@ -81,121 +79,6 @@ std::string ScriptInterpreter::LanguageToString(lldb::ScriptLanguage language) {
   llvm_unreachable("Unhandled ScriptInterpreter!");
 }
 
-lldb::DataExtractorSP
-ScriptInterpreter::GetDataExtractorFromSBData(const lldb::SBData &data) const {
-  return data.m_opaque_sp;
-}
-
-lldb::BreakpointSP ScriptInterpreter::GetOpaqueTypeFromSBBreakpoint(
-    const lldb::SBBreakpoint &breakpoint) const {
-  return breakpoint.m_opaque_wp.lock();
-}
-
-lldb::BreakpointLocationSP
-ScriptInterpreter::GetOpaqueTypeFromSBBreakpointLocation(
-    const lldb::SBBreakpointLocation &break_loc) const {
-  return break_loc.m_opaque_wp.lock();
-}
-
-CommandReturnObject *ScriptInterpreter::GetOpaqueTypeFromSBCommandReturnObject(
-    const lldb::SBCommandReturnObject &cmd_retobj) const {
-  return cmd_retobj.m_opaque_up->get();
-}
-
-lldb::DebuggerSP ScriptInterpreter::GetOpaqueTypeFromSBDebugger(
-    const lldb::SBDebugger &debugger) const {
-  return debugger.m_opaque_sp;
-}
-
-lldb::ProcessAttachInfoSP ScriptInterpreter::GetOpaqueTypeFromSBAttachInfo(
-    const lldb::SBAttachInfo &attach_info) const {
-  return attach_info.m_opaque_sp;
-}
-
-lldb::ProcessLaunchInfoSP ScriptInterpreter::GetOpaqueTypeFromSBLaunchInfo(
-    const lldb::SBLaunchInfo &launch_info) const {
-  return std::make_shared<ProcessLaunchInfo>(
-      *reinterpret_cast<ProcessLaunchInfo *>(launch_info.m_opaque_sp.get()));
-}
-
-Status
-ScriptInterpreter::GetStatusFromSBError(const lldb::SBError &error) const {
-  if (error.m_opaque_up)
-    return error.m_opaque_up->Clone();
-
-  return Status();
-}
-
-lldb::ThreadSP ScriptInterpreter::GetOpaqueTypeFromSBThread(
-    const lldb::SBThread &thread) const {
-  if (thread.m_opaque_sp)
-    return thread.m_opaque_sp->GetThreadSP();
-  return nullptr;
-}
-
-lldb::StackFrameSP
-ScriptInterpreter::GetOpaqueTypeFromSBFrame(const lldb::SBFrame &frame) const {
-  if (frame.m_opaque_sp)
-    return frame.m_opaque_sp->GetFrameSP();
-  return nullptr;
-}
-
-Event *
-ScriptInterpreter::GetOpaqueTypeFromSBEvent(const lldb::SBEvent &event) const {
-  return event.m_opaque_ptr;
-}
-
-lldb::StreamSP ScriptInterpreter::GetOpaqueTypeFromSBStream(
-    const lldb::SBStream &stream) const {
-  if (stream.m_opaque_up) {
-    lldb::StreamSP s = std::make_shared<lldb_private::StreamString>();
-    *s << reinterpret_cast<StreamString *>(stream.m_opaque_up.get())->m_packet;
-    return s;
-  }
-
-  return nullptr;
-}
-
-SymbolContext ScriptInterpreter::GetOpaqueTypeFromSBSymbolContext(
-    const lldb::SBSymbolContext &sb_sym_ctx) const {
-  if (sb_sym_ctx.m_opaque_up)
-    return *sb_sym_ctx.m_opaque_up;
-  return {};
-}
-
-std::optional<lldb_private::MemoryRegionInfo>
-ScriptInterpreter::GetOpaqueTypeFromSBMemoryRegionInfo(
-    const lldb::SBMemoryRegionInfo &mem_region) const {
-  if (!mem_region.m_opaque_up)
-    return std::nullopt;
-  return *mem_region.m_opaque_up.get();
-}
-
-lldb::ExecutionContextRefSP
-ScriptInterpreter::GetOpaqueTypeFromSBExecutionContext(
-    const lldb::SBExecutionContext &exe_ctx) const {
-  return exe_ctx.m_exe_ctx_sp;
-}
-
-lldb::StackFrameListSP ScriptInterpreter::GetOpaqueTypeFromSBFrameList(
-    const lldb::SBFrameList &frame_list) const {
-  return frame_list.m_opaque_sp;
-}
-
-lldb::TargetSP ScriptInterpreter::GetOpaqueTypeFromSBTarget(
-    const lldb::SBTarget &target) const {
-  return target.m_opaque_sp;
-}
-
-lldb::ValueObjectSP
-ScriptInterpreter::GetOpaqueTypeFromSBValue(const lldb::SBValue &value) const {
-  if (!value.m_opaque_sp)
-    return lldb::ValueObjectSP();
-
-  lldb_private::ValueLocker locker;
-  return locker.GetLockedSP(*value.m_opaque_sp);
-}
-
 lldb::ScriptLanguage
 ScriptInterpreter::StringToLanguage(const llvm::StringRef &language) {
   if (language.equals_insensitive(LanguageToString(eScriptLanguageNone)))
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
index 96391f7be8da0..e08b4795b9297 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
@@ -8,7 +8,9 @@
 
 #include "../lldb-python.h"
 
-#include "lldb/API/SBDebugger.h"
+#include "API/ScriptInterpreterBridge.h"
+#include "lldb/API/SBValue.h"
+#include "lldb/API/SBValueList.h"
 #include "lldb/Host/Config.h"
 #include "lldb/Utility/Log.h"
 #include "lldb/lldb-enumerations.h"
@@ -47,7 +49,7 @@ Status ScriptedPythonInterface::ExtractValueFromPythonObject<Status>(
     python::PythonObject &p, Status &error) {
   if (lldb::SBError *sb_error = reinterpret_cast<lldb::SBError *>(
           python::LLDBSWIGPython_CastPyObjectToSBError(p.get())))
-    return m_interpreter.GetStatusFromSBError(*sb_error);
+    return ScriptInterpreterBridge::GetStatus(*sb_error);
   error =
       Status::FromErrorString("Couldn't cast lldb::SBError to lldb::Status.");
 
@@ -59,7 +61,7 @@ Event *ScriptedPythonInterface::ExtractValueFromPythonObject<Event *>(
     python::PythonObject &p, Status &error) {
   if (lldb::SBEvent *sb_event = reinterpret_cast<lldb::SBEvent *>(
           python::LLDBSWIGPython_CastPyObjectToSBEvent(p.get())))
-    return m_interpreter.GetOpaqueTypeFromSBEvent(*sb_event);
+    return ScriptInterpreterBridge::GetEvent(*sb_event);
   error = Status::FromErrorString(
       "Couldn't cast lldb::SBEvent to lldb_private::Event.");
 
@@ -74,7 +76,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<CommandReturnObject *>(
           reinterpret_cast<lldb::SBCommandReturnObject *>(
               python::LLDBSWIGPython_CastPyObjectToSBCommandReturnObject(
                   p.get())))
-    return m_interpreter.GetOpaqueTypeFromSBCommandReturnObject(*sb_cmd_retobj);
+    return ScriptInterpreterBridge::GetCommandReturnObject(*sb_cmd_retobj);
   error =
       Status::FromErrorString("couldn't cast lldb::SBCommandReturnObject to "
                               "lldb_private::CommandReturnObject.");
@@ -87,7 +89,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::StreamSP>(
     python::PythonObject &p, Status &error) {
   if (lldb::SBStream *sb_stream = reinterpret_cast<lldb::SBStream *>(
           python::LLDBSWIGPython_CastPyObjectToSBStream(p.get())))
-    return m_interpreter.GetOpaqueTypeFromSBStream(*sb_stream);
+    return ScriptInterpreterBridge::GetStream(*sb_stream);
   error = Status::FromErrorString(
       "Couldn't cast lldb::SBStream to lldb_private::Stream.");
 
@@ -100,7 +102,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::StackFrameSP>(
     python::PythonObject &p, Status &error) {
   if (lldb::SBFrame *sb_frame = reinterpret_cast<lldb::SBFrame *>(
           python::LLDBSWIGPython_CastPyObjectToSBFrame(p.get())))
-    return m_interpreter.GetOpaqueTypeFromSBFrame(*sb_frame);
+    return ScriptInterpreterBridge::GetStackFrame(*sb_frame);
   error = Status::FromErrorString(
       "Couldn't cast lldb::SBFrame to lldb_private::StackFrame.");
 
@@ -113,7 +115,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ThreadSP>(
     python::PythonObject &p, Status &error) {
   if (lldb::SBThread *sb_thread = reinterpret_cast<lldb::SBThread *>(
           python::LLDBSWIGPython_CastPyObjectToSBThread(p.get())))
-    return m_interpreter.GetOpaqueTypeFromSBThread(*sb_thread);
+    return ScriptInterpreterBridge::GetThread(*sb_thread);
   error = Status::FromErrorString(
       "Couldn't cast lldb::SBThread to lldb_private::Thread.");
 
@@ -127,7 +129,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<SymbolContext>(
   if (lldb::SBSymbolContext *sb_symbol_context =
           reinterpret_cast<lldb::SBSymbolContext *>(
               python::LLDBSWIGPython_CastPyObjectToSBSymbolContext(p.get())))
-    return m_interpreter.GetOpaqueTypeFromSBSymbolContext(*sb_symbol_context);
+    return ScriptInterpreterBridge::GetSymbolContext(*sb_symbol_context);
   error = Status::FromErrorString(
       "Couldn't cast lldb::SBSymbolContext to lldb_private::SymbolContext.");
 
@@ -147,7 +149,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::DataExtractorSP>(
     return nullptr;
   }
 
-  return m_interpreter.GetDataExtractorFromSBData(*sb_data);
+  return ScriptInterpreterBridge::GetDataExtractor(*sb_data);
 }
 
 template <>
@@ -163,7 +165,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::BreakpointSP>(
     return nullptr;
   }
 
-  return m_interpreter.GetOpaqueTypeFromSBBreakpoint(*sb_breakpoint);
+  return ScriptInterpreterBridge::GetBreakpoint(*sb_breakpoint);
 }
 
 template <>
@@ -181,7 +183,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<
     return nullptr;
   }
 
-  return m_interpreter.GetOpaqueTypeFromSBBreakpointLocation(*sb_break_loc);
+  return ScriptInterpreterBridge::GetBreakpointLocation(*sb_break_loc);
 }
 
 template <>
@@ -196,7 +198,7 @@ lldb::ProcessAttachInfoSP ScriptedPythonInterface::ExtractValueFromPythonObject<
     return nullptr;
   }
 
-  return m_interpreter.GetOpaqueTypeFromSBAttachInfo(*sb_attach_info);
+  return ScriptInterpreterBridge::GetProcessAttachInfo(*sb_attach_info);
 }
 
 template <>
@@ -211,7 +213,7 @@ lldb::ProcessLaunchInfoSP ScriptedPythonInterface::ExtractValueFromPythonObject<
     return nullptr;
   }
 
-  return m_interpreter.GetOpaqueTypeFromSBLaunchInfo(*sb_launch_info);
+  return ScriptInterpreterBridge::GetProcessLaunchInfo(*sb_launch_info);
 }
 
 template <>
@@ -230,7 +232,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<
     return {};
   }
 
-  return m_interpreter.GetOpaqueTypeFromSBMemoryRegionInfo(*sb_mem_reg_info);
+  return ScriptInterpreterBridge::GetMemoryRegionInfo(*sb_mem_reg_info);
 }
 
 template <>
@@ -249,7 +251,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<
     return {};
   }
 
-  return m_interpreter.GetOpaqueTypeFromSBExecutionContext(*sb_exe_ctx);
+  return ScriptInterpreterBridge::GetExecutionContextRef(*sb_exe_ctx);
 }
 
 template <>
@@ -284,7 +286,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::StackFrameListSP>(
     return {};
   }
 
-  return m_interpreter.GetOpaqueTypeFromSBFrameList(*sb_frame_list);
+  return ScriptInterpreterBridge::GetStackFrameList(*sb_frame_list);
 }
 
 template <>
@@ -299,7 +301,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ValueObjectSP>(
     return {};
   }
 
-  return m_interpreter.GetOpaqueTypeFromSBValue(*sb_value);
+  return ScriptInterpreterBridge::GetValueObject(*sb_value);
 }
 
 template <>
@@ -314,7 +316,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::TargetSP>(
     return {};
   }
 
-  return m_interpreter.GetOpaqueTypeFromSBTarget(*sb_target);
+  return ScriptInterpreterBridge::GetTarget(*sb_target);
 }
 
 template <>
@@ -330,7 +332,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ValueObjectListSP>(
           python::LLDBSWIGPython_CastPyObjectToSBValueList(p.get()))) {
     for (uint32_t i = 0, e = sb_value_list->GetSize(); i < e; ++i) {
       SBValue value = sb_value_list->GetValueAtIndex(i);
-      out->Append(m_interpreter.GetOpaqueTypeFromSBValue(value));
+      out->Append(ScriptInterpreterBridge::GetValueObject(value));
     }
     return out;
   }
@@ -359,7 +361,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ValueObjectListSP>(
           python::LLDBSWIGPython_CastPyObjectToSBValue(
               static_cast<PyObject *>(generic->GetValue())));
       if (sb_value)
-        if (auto valobj_sp = m_interpreter.GetOpaqueTypeFromSBValue(*sb_value))
+        if (auto valobj_sp = ScriptInterpreterBridge::GetValueObject(*sb_value))
           out->Append(valobj_sp);
       ++index;
       return true;
@@ -403,7 +405,7 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::DebuggerSP>(
     python::PythonObject &p, Status &error) {
   if (lldb::SBDebugger *sb_dbg = reinterpret_cast<lldb::SBDebugger *>(
           python::LLDBSWIGPython_CastPyObjectToSBDebugger(p.get())))
-    return m_interpreter.GetOpaqueTypeFromSBDebugger(*sb_dbg);
+    return ScriptInterpreterBridge::GetDebugger(*sb_dbg);
   error = Status::FromErrorString(
       "couldn't cast lldb::SBDebugger to lldb::DebuggerSP.");
   return {};

>From ba09d16be8349278f0dc293b93fc76f3678d021f Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Tue, 11 Aug 2026 08:38:22 -0700
Subject: [PATCH 11/16] [lldb/test] Retry the remote platform connection in
 dotest (#215425)

Connecting to a remote platform through port forwarding can fail
transiently even when the device and the tunnel are healthy, which
aborts the entire test suite run before a single test executes.

Retry the `ConnectRemote` call a few times with a short backoff before
giving up. Each failed attempt is still printed with its attempt number,
and a device that is genuinely unreachable fails with the same error on
every attempt and then exits as before, so this does not mask a broken
connection, it only adds a few seconds in that case.

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
(cherry picked from commit 2bef9576d0c37f71dbe6cd50d8fce69e2f27ea18)
---
 lldb/packages/Python/lldbsuite/test/dotest.py | 28 +++++++++++++++----
 1 file changed, 23 insertions(+), 5 deletions(-)

diff --git a/lldb/packages/Python/lldbsuite/test/dotest.py b/lldb/packages/Python/lldbsuite/test/dotest.py
index b1145f8f96078..90f8831c9ae46 100644
--- a/lldb/packages/Python/lldbsuite/test/dotest.py
+++ b/lldb/packages/Python/lldbsuite/test/dotest.py
@@ -31,6 +31,7 @@
 import subprocess
 import sys
 import tempfile
+import time
 
 # Third-party modules
 import unittest
@@ -1130,14 +1131,31 @@ def run_suite():
             platform_connect_options = lldb.SBPlatformConnectOptions(
                 configuration.lldb_platform_url
             )
-            err = lldb.remote_platform.ConnectRemote(platform_connect_options)
+            # Connecting to a remote platform through a port forward can fail
+            # transiently while the connection itself is perfectly healthy, so
+            # retry a few times before giving up. Every attempt is reported, and
+            # a device that is genuinely unreachable still fails quickly with the
+            # same error on each attempt, so this doesn't hide a broken device.
+            max_connect_attempts = 4
+            for attempt in range(1, max_connect_attempts + 1):
+                err = lldb.remote_platform.ConnectRemote(platform_connect_options)
+                if err.Success():
+                    break
+                print(
+                    "error: failed to connect to remote platform using URL "
+                    "'%s': %s (attempt %d of %d)"
+                    % (
+                        configuration.lldb_platform_url,
+                        err,
+                        attempt,
+                        max_connect_attempts,
+                    )
+                )
+                if attempt < max_connect_attempts:
+                    time.sleep(attempt)
             if err.Success():
                 print("Connected.")
             else:
-                print(
-                    "error: failed to connect to remote platform using URL '%s': %s"
-                    % (configuration.lldb_platform_url, err)
-                )
                 exitTestSuite(1)
         else:
             configuration.lldb_platform_url = None

>From 876fe767b57c1e123e1fbf1130712cf29ce7a05a Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Fri, 14 Aug 2026 15:30:17 -0700
Subject: [PATCH 12/16] [lldb] Wrap Target::GetAPIMutex() into a Lockable
 handle (NFC) (#212872)

While implementing #208242, we realized that we needed a Lockable
wrapper for the Target's API Mutex that could skip the locking on
re-entrant threads (when a command (i.e `bt`) triggers scripted
extension (i.e `ScriptedFrameProvider`) that uses SBAPI (i.e.
`SBFrame`), and still behave as a normal mutex otherwise. However, since
`Target::GetAPIMutex()` returns a `std::recursive_mutex&`, that can't
represent "no synchronization at all".

This is why this PR introduces `TargetAPILock`, a small Lockable type
that behaves exactly like `std::recursive_mutex`, with no RAII of its
own, so callers can wrap it in `std::lock_guard`/`std::unique_lock` like
they would any other Lockable.

A `TargetAPILock` is bound to a `Target` rather than to a specific
mutex, so `lock()`/`try_lock()` resolve which real mutex to use fresh on
every call instead of caching one resolution for the handle's lifetime.
`unlock()` replays whatever the matching `lock()`/`try_lock()` resolved
rather than re-resolving, so a policy change between the two calls can't
release the wrong mutex. That's also what lets a handle be constructed
on one thread and locked/unlocked on another, which the deferred-lock
`SBMutex` needs.

`GetAPIMutex()` now returns `TargetAPILock` by value, so every caller
that took its old `std::recursive_mutex&` materializes the handle into a
local and wraps that in a `lock_guard`/`unique_lock` instead. A few
places where the lock outlives its constructing statement
(`CommandObject`'s `m_api_locker`, `ValueLocker`,
`StoppedExecutionContext`) keep the resolved handle as a member and
release it explicitly instead.

`SBMutex`'s opaque member becomes a `std::variant<std::recursive_mutex,
TargetAPILock>` rather than two separate members, so the no-target case
still owns a real mutex directly without growing `SBMutex`'s size across
the ABI boundary.

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
(cherry picked from commit 251ab4a3f2a2e0e8def833067064d5ad2b7efad9)
---
 lldb/include/lldb/API/SBMutex.h               |   5 +-
 lldb/include/lldb/Interpreter/CommandObject.h |   3 +-
 lldb/include/lldb/Target/ExecutionContext.h   |  28 ++-
 lldb/include/lldb/Target/Target.h             |   9 +-
 lldb/include/lldb/Target/TargetAPIMutex.h     |  69 +++++++
 lldb/include/lldb/ValueObject/ValueObject.h   |   8 +-
 lldb/source/API/SBAddress.cpp                 |   3 +-
 lldb/source/API/SBBreakpoint.cpp              | 148 ++++++-------
 lldb/source/API/SBBreakpointLocation.cpp      | 104 +++++-----
 lldb/source/API/SBBreakpointName.cpp          |  98 +++++----
 lldb/source/API/SBCommandInterpreter.cpp      |  30 ++-
 lldb/source/API/SBDebugger.cpp                |  12 +-
 lldb/source/API/SBFunction.cpp                |   6 +-
 lldb/source/API/SBInstruction.cpp             |  24 ++-
 lldb/source/API/SBMutex.cpp                   |  39 +++-
 lldb/source/API/SBProcess.cpp                 | 173 ++++++++--------
 lldb/source/API/SBSymbol.cpp                  |   6 +-
 lldb/source/API/SBTarget.cpp                  | 111 ++++++----
 lldb/source/API/SBThread.cpp                  |   3 +-
 lldb/source/API/SBValue.cpp                   |  10 +-
 lldb/source/API/SBWatchpoint.cpp              |  59 +++---
 lldb/source/Interpreter/CommandObject.cpp     |   7 +-
 lldb/source/Target/CMakeLists.txt             |   1 +
 lldb/source/Target/ExecutionContext.cpp       |  12 +-
 lldb/source/Target/Target.cpp                 |   9 +-
 lldb/source/Target/TargetAPIMutex.cpp         |  36 ++++
 lldb/source/ValueObject/ValueObject.cpp       |  10 +-
 lldb/tools/lldb-dap/ProtocolUtils.cpp         |   1 +
 lldb/unittests/Target/CMakeLists.txt          |   1 +
 lldb/unittests/Target/TargetAPIMutexTest.cpp  | 195 ++++++++++++++++++
 30 files changed, 815 insertions(+), 405 deletions(-)
 create mode 100644 lldb/include/lldb/Target/TargetAPIMutex.h
 create mode 100644 lldb/source/Target/TargetAPIMutex.cpp
 create mode 100644 lldb/unittests/Target/TargetAPIMutexTest.cpp

diff --git a/lldb/include/lldb/API/SBMutex.h b/lldb/include/lldb/API/SBMutex.h
index 826ad077f159f..856c648cf96ab 100644
--- a/lldb/include/lldb/API/SBMutex.h
+++ b/lldb/include/lldb/API/SBMutex.h
@@ -11,7 +11,7 @@
 
 #include "lldb/API/SBDefines.h"
 #include "lldb/lldb-forward.h"
-#include <mutex>
+#include <memory>
 
 namespace lldb {
 
@@ -41,7 +41,8 @@ class LLDB_API SBMutex {
   SBMutex(lldb::TargetSP target_sp);
   friend class SBTarget;
 
-  std::shared_ptr<std::recursive_mutex> m_opaque_sp;
+  class MutexVariant;
+  std::shared_ptr<MutexVariant> m_opaque_sp;
 };
 
 } // namespace lldb
diff --git a/lldb/include/lldb/Interpreter/CommandObject.h b/lldb/include/lldb/Interpreter/CommandObject.h
index 925377159d749..2b08db93c1f9c 100644
--- a/lldb/include/lldb/Interpreter/CommandObject.h
+++ b/lldb/include/lldb/Interpreter/CommandObject.h
@@ -410,7 +410,8 @@ class CommandObject : public std::enable_shared_from_this<CommandObject> {
 
   CommandInterpreter &m_interpreter;
   ExecutionContext m_exe_ctx;
-  std::unique_lock<std::recursive_mutex> m_api_locker;
+  TargetAPIMutex m_api_mutex;
+  std::unique_lock<TargetAPIMutex> m_api_locker;
   std::string m_cmd_name;
   std::string m_cmd_help_short;
   std::string m_cmd_help_long;
diff --git a/lldb/include/lldb/Target/ExecutionContext.h b/lldb/include/lldb/Target/ExecutionContext.h
index bf976f4db8c87..0386ff3c511de 100644
--- a/lldb/include/lldb/Target/ExecutionContext.h
+++ b/lldb/include/lldb/Target/ExecutionContext.h
@@ -14,6 +14,7 @@
 #include "lldb/Host/ProcessRunLock.h"
 #include "lldb/Target/StackID.h"
 #include "lldb/Target/SyntheticFrameProvider.h"
+#include "lldb/Target/TargetAPIMutex.h"
 #include "lldb/lldb-private.h"
 
 namespace lldb_private {
@@ -565,16 +566,24 @@ class ExecutionContext {
 /// The locks are private by design: to unlock them, destroy the
 /// StoppedExecutionContext.
 struct StoppedExecutionContext : ExecutionContext {
+  /// `locker` is consumed purely for its side effect: moving a
+  /// std::unique_lock disarms the source, so the caller's own locker (which
+  /// referenced `api_mutex` before it was moved here) stops believing it
+  /// owns the lock, without an explicit release() call at the call site.
+  /// `m_api_locker` re-adopts the lock against this object's own
+  /// `m_api_mutex`, since a unique_lock can't be retargeted by moving it.
   StoppedExecutionContext(lldb::TargetSP &target_sp,
                           lldb::ProcessSP &process_sp,
                           lldb::ThreadSP &thread_sp,
                           lldb::StackFrameSP &frame_sp,
-                          std::unique_lock<std::recursive_mutex> api_lock,
+                          TargetAPIMutex api_mutex,
+                          std::unique_lock<TargetAPIMutex> locker,
                           ProcessRunLock::ProcessRunLocker stop_locker)
-      : m_api_lock(std::move(api_lock)), m_stop_locker(std::move(stop_locker)) {
+      : m_api_mutex(std::move(api_mutex)),
+        m_api_locker(m_api_mutex, std::adopt_lock),
+        m_stop_locker(std::move(stop_locker)) {
     assert(target_sp);
     assert(process_sp);
-    assert(m_api_lock.owns_lock());
     assert(m_stop_locker.IsLocked());
     SetTargetSP(target_sp);
     SetProcessSP(process_sp);
@@ -585,20 +594,21 @@ struct StoppedExecutionContext : ExecutionContext {
   /// Transfers ownership of the locks from `other` to `this`, making `other`
   /// unusable.
   StoppedExecutionContext(StoppedExecutionContext &&other)
-      : StoppedExecutionContext(other.m_target_sp, other.m_process_sp,
-                                other.m_thread_sp, other.m_frame_sp,
-                                std::move(other.m_api_lock),
-                                std::move(other.m_stop_locker)) {
+      : StoppedExecutionContext(
+            other.m_target_sp, other.m_process_sp, other.m_thread_sp,
+            other.m_frame_sp, std::move(other.m_api_mutex),
+            std::move(other.m_api_locker), std::move(other.m_stop_locker)) {
     other.Clear();
   }
 
   /// Clears this context, unlocking the ProcessRunLock and returning the
   /// locked API lock, allowing callers to resume the process. Similar to
   /// a move operation, this object is no longer usable.
-  [[nodiscard]] std::unique_lock<std::recursive_mutex> AllowResume();
+  [[nodiscard]] TargetAPIMutex AllowResume();
 
 private:
-  std::unique_lock<std::recursive_mutex> m_api_lock;
+  TargetAPIMutex m_api_mutex;
+  std::unique_lock<TargetAPIMutex> m_api_locker;
   ProcessRunLock::ProcessRunLocker m_stop_locker;
 };
 
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index fb43f432a08da..853a1770345f4 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -12,6 +12,7 @@
 #include <list>
 #include <map>
 #include <memory>
+#include <optional>
 #include <string>
 #include <vector>
 
@@ -33,6 +34,7 @@
 #include "lldb/Target/SectionLoadHistory.h"
 #include "lldb/Target/Statistics.h"
 #include "lldb/Target/SyntheticFrameProvider.h"
+#include "lldb/Target/TargetAPIMutex.h"
 #include "lldb/Target/ThreadSpec.h"
 #include "lldb/Utility/ArchSpec.h"
 #include "lldb/Utility/Broadcaster.h"
@@ -581,6 +583,7 @@ class Target : public std::enable_shared_from_this<Target>,
 public:
   friend class TargetList;
   friend class Debugger;
+  friend class TargetAPIMutex;
 
   /// Broadcaster event bits definitions.
   enum {
@@ -761,7 +764,11 @@ class Target : public std::enable_shared_from_this<Target>,
 
   static TargetProperties &GetGlobalProperties();
 
-  std::recursive_mutex &GetAPIMutex();
+  /// Returns a handle resolved to the mutex to serialize on before
+  /// touching the target through the SB API. The handle isn't locked yet;
+  /// lock()/try_lock() it (typically via std::lock_guard<TargetAPIMutex>/
+  /// std::unique_lock<TargetAPIMutex>) to actually acquire it.
+  TargetAPIMutex GetAPIMutex();
 
   void DeleteCurrentProcess();
 
diff --git a/lldb/include/lldb/Target/TargetAPIMutex.h b/lldb/include/lldb/Target/TargetAPIMutex.h
new file mode 100644
index 0000000000000..d822329842264
--- /dev/null
+++ b/lldb/include/lldb/Target/TargetAPIMutex.h
@@ -0,0 +1,69 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_TARGET_TARGETAPIMUTEX_H
+#define LLDB_TARGET_TARGETAPIMUTEX_H
+
+#include "lldb/lldb-forward.h"
+#include <memory>
+#include <mutex>
+
+namespace lldb_private {
+
+/// A Lockable handle over a Target's API mutex, returned by
+/// Target::GetAPIMutex() and backing the public lldb::SBMutex.
+///
+/// Behaves like std::recursive_mutex: lock()/try_lock()/unlock() drive
+/// the actual synchronization, with the same contract (unlock() without
+/// a matching successful lock()/try_lock() is caller error). It carries
+/// no RAII of its own; wrap it in std::lock_guard<TargetAPIMutex> or
+/// std::unique_lock<TargetAPIMutex> for scope-based locking, exactly as
+/// with any other Lockable.
+///
+/// A handle may be constructed on one thread and then locked/unlocked
+/// on a different one, so lock()/try_lock() (re-)resolve which real
+/// mutex to use fresh on every call, rather than caching a single
+/// resolution for the handle's lifetime. The matching unlock() replays
+/// the exact resolution that call produced, rather than re-resolving,
+/// so the calling thread's policy at unlock() time can't cause it to
+/// release the wrong mutex (or fail to release the one it actually
+/// holds).
+///
+/// Default-constructed (or moved-from) handles are a genuine no-op: no
+/// synchronization primitive is touched at all.
+class TargetAPIMutex {
+public:
+  TargetAPIMutex() = default;
+  explicit TargetAPIMutex(lldb::TargetSP target_sp)
+      : m_target_sp(std::move(target_sp)) {}
+
+  TargetAPIMutex(TargetAPIMutex &&other) noexcept = default;
+  TargetAPIMutex &operator=(TargetAPIMutex &&other) noexcept = default;
+
+  TargetAPIMutex(const TargetAPIMutex &) = delete;
+  TargetAPIMutex &operator=(const TargetAPIMutex &) = delete;
+
+  void lock();
+  bool try_lock();
+  void unlock() {
+    if (m_mutex)
+      m_mutex->unlock();
+  }
+
+private:
+  /// An aliasing shared_ptr into m_target_sp's own mutex, resolved fresh
+  /// on every lock()/try_lock() call. Shares m_target_sp's control block
+  /// (keeping the Target alive) while pointing at the mutex living inside
+  /// it. Null when this handle is a genuine no-op.
+  std::shared_ptr<std::recursive_mutex> m_mutex;
+  lldb::TargetSP m_target_sp;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_TARGET_TARGETAPIMUTEX_H
diff --git a/lldb/include/lldb/ValueObject/ValueObject.h b/lldb/include/lldb/ValueObject/ValueObject.h
index 6e4d93b815761..170e577259b65 100644
--- a/lldb/include/lldb/ValueObject/ValueObject.h
+++ b/lldb/include/lldb/ValueObject/ValueObject.h
@@ -1269,7 +1269,8 @@ class ValueImpl {
   lldb::ValueObjectSP GetRootSP() { return m_valobj_sp; }
 
   lldb::ValueObjectSP GetSP(Process::StopLocker &stop_locker,
-                            std::unique_lock<std::recursive_mutex> &lock,
+                            TargetAPIMutex &api_mutex,
+                            std::unique_lock<TargetAPIMutex> &lock,
                             Status &error);
 
   void SetUseDynamic(lldb::DynamicValueType use_dynamic) {
@@ -1314,14 +1315,15 @@ class ValueLocker {
   ValueLocker() = default;
 
   lldb::ValueObjectSP GetLockedSP(ValueImpl &in_value) {
-    return in_value.GetSP(m_stop_locker, m_lock, m_lock_error);
+    return in_value.GetSP(m_stop_locker, m_api_mutex, m_lock, m_lock_error);
   }
 
   Status &GetError() { return m_lock_error; }
 
 private:
   Process::StopLocker m_stop_locker;
-  std::unique_lock<std::recursive_mutex> m_lock;
+  TargetAPIMutex m_api_mutex;
+  std::unique_lock<TargetAPIMutex> m_lock;
   Status m_lock_error;
 };
 
diff --git a/lldb/source/API/SBAddress.cpp b/lldb/source/API/SBAddress.cpp
index 78acc2e34564d..5015b6d81d732 100644
--- a/lldb/source/API/SBAddress.cpp
+++ b/lldb/source/API/SBAddress.cpp
@@ -110,7 +110,8 @@ lldb::addr_t SBAddress::GetLoadAddress(const SBTarget &target) const {
   TargetSP target_sp(target.GetSP());
   if (target_sp) {
     if (m_opaque_up->IsValid()) {
-      std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+      TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       addr = m_opaque_up->GetLoadAddress(target_sp.get());
     }
   }
diff --git a/lldb/source/API/SBBreakpoint.cpp b/lldb/source/API/SBBreakpoint.cpp
index bcef2bd366f73..75d54cabfed22 100644
--- a/lldb/source/API/SBBreakpoint.cpp
+++ b/lldb/source/API/SBBreakpoint.cpp
@@ -119,8 +119,8 @@ void SBBreakpoint::ClearAllBreakpointSites() {
 
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     bkpt_sp->ClearAllBreakpointSites();
   }
 }
@@ -133,8 +133,8 @@ SBBreakpointLocation SBBreakpoint::FindLocationByAddress(addr_t vm_addr) {
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
     if (vm_addr != LLDB_INVALID_ADDRESS) {
-      std::lock_guard<std::recursive_mutex> guard(
-          bkpt_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       Address address;
       Target &target = bkpt_sp->GetTarget();
       if (!target.ResolveLoadAddress(vm_addr, address)) {
@@ -153,8 +153,8 @@ break_id_t SBBreakpoint::FindLocationIDByAddress(addr_t vm_addr) {
   BreakpointSP bkpt_sp = GetSP();
 
   if (bkpt_sp && vm_addr != LLDB_INVALID_ADDRESS) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     Address address;
     Target &target = bkpt_sp->GetTarget();
     if (!target.ResolveLoadAddress(vm_addr, address)) {
@@ -173,8 +173,8 @@ SBBreakpointLocation SBBreakpoint::FindLocationByID(break_id_t bp_loc_id) {
   BreakpointSP bkpt_sp = GetSP();
 
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     sb_bp_location.SetLocation(bkpt_sp->FindLocationByID(bp_loc_id));
   }
 
@@ -188,8 +188,8 @@ SBBreakpointLocation SBBreakpoint::GetLocationAtIndex(uint32_t index) {
   BreakpointSP bkpt_sp = GetSP();
 
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     sb_bp_location.SetLocation(bkpt_sp->GetLocationAtIndex(index));
   }
 
@@ -202,8 +202,8 @@ void SBBreakpoint::SetEnabled(bool enable) {
   BreakpointSP bkpt_sp = GetSP();
 
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     bkpt_sp->SetEnabled(enable);
   }
 }
@@ -213,8 +213,8 @@ bool SBBreakpoint::IsEnabled() {
 
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return bkpt_sp->IsEnabled();
   } else
     return false;
@@ -226,8 +226,8 @@ void SBBreakpoint::SetOneShot(bool one_shot) {
   BreakpointSP bkpt_sp = GetSP();
 
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     bkpt_sp->SetOneShot(one_shot);
   }
 }
@@ -237,8 +237,8 @@ bool SBBreakpoint::IsOneShot() const {
 
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return bkpt_sp->IsOneShot();
   } else
     return false;
@@ -249,8 +249,8 @@ bool SBBreakpoint::IsInternal() {
 
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return bkpt_sp->IsInternal();
   } else
     return false;
@@ -262,8 +262,8 @@ void SBBreakpoint::SetIgnoreCount(uint32_t count) {
   BreakpointSP bkpt_sp = GetSP();
 
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     bkpt_sp->SetIgnoreCount(count);
   }
 }
@@ -273,8 +273,8 @@ void SBBreakpoint::SetCondition(const char *condition) {
 
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     // Treat a null pointer as resetting the condition.
     if (!condition)
       bkpt_sp->SetCondition(StopCondition());
@@ -290,8 +290,8 @@ const char *SBBreakpoint::GetCondition() {
   if (!bkpt_sp)
     return nullptr;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      bkpt_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   StopCondition cond = bkpt_sp->GetCondition();
   if (!cond)
     return nullptr;
@@ -303,8 +303,8 @@ void SBBreakpoint::SetAutoContinue(bool auto_continue) {
 
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     bkpt_sp->SetAutoContinue(auto_continue);
   }
 }
@@ -314,8 +314,8 @@ bool SBBreakpoint::GetAutoContinue() {
 
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return bkpt_sp->IsAutoContinue();
   }
   return false;
@@ -327,8 +327,8 @@ uint32_t SBBreakpoint::GetHitCount() const {
   uint32_t count = 0;
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     count = bkpt_sp->GetHitCount();
   }
 
@@ -341,8 +341,8 @@ uint32_t SBBreakpoint::GetIgnoreCount() const {
   uint32_t count = 0;
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     count = bkpt_sp->GetIgnoreCount();
   }
 
@@ -354,8 +354,8 @@ void SBBreakpoint::SetThreadID(lldb::tid_t tid) {
 
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     bkpt_sp->SetThreadID(tid);
   }
 }
@@ -366,8 +366,8 @@ lldb::tid_t SBBreakpoint::GetThreadID() {
   lldb::tid_t tid = LLDB_INVALID_THREAD_ID;
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     tid = bkpt_sp->GetThreadID();
   }
 
@@ -379,8 +379,8 @@ void SBBreakpoint::SetThreadIndex(uint32_t index) {
 
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     bkpt_sp->GetOptions().GetThreadSpec()->SetIndex(index);
   }
 }
@@ -391,8 +391,8 @@ uint32_t SBBreakpoint::GetThreadIndex() const {
   uint32_t thread_idx = UINT32_MAX;
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     const ThreadSpec *thread_spec =
         bkpt_sp->GetOptions().GetThreadSpecNoCreate();
     if (thread_spec != nullptr)
@@ -408,8 +408,8 @@ void SBBreakpoint::SetThreadName(const char *thread_name) {
   BreakpointSP bkpt_sp = GetSP();
 
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     bkpt_sp->GetOptions().GetThreadSpec()->SetName(thread_name);
   }
 }
@@ -421,8 +421,8 @@ const char *SBBreakpoint::GetThreadName() const {
   if (!bkpt_sp)
     return nullptr;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      bkpt_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   if (const ThreadSpec *thread_spec =
           bkpt_sp->GetOptions().GetThreadSpecNoCreate())
     return ConstString(thread_spec->GetName()).GetCString();
@@ -435,8 +435,8 @@ void SBBreakpoint::SetQueueName(const char *queue_name) {
 
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     bkpt_sp->GetOptions().GetThreadSpec()->SetQueueName(queue_name);
   }
 }
@@ -448,8 +448,8 @@ const char *SBBreakpoint::GetQueueName() const {
   if (!bkpt_sp)
     return nullptr;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      bkpt_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   if (const ThreadSpec *thread_spec =
           bkpt_sp->GetOptions().GetThreadSpecNoCreate())
     return ConstString(thread_spec->GetQueueName()).GetCString();
@@ -463,8 +463,8 @@ size_t SBBreakpoint::GetNumResolvedLocations() const {
   size_t num_resolved = 0;
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     num_resolved = bkpt_sp->GetNumResolvedLocations();
   }
   return num_resolved;
@@ -476,8 +476,8 @@ size_t SBBreakpoint::GetNumLocations() const {
   BreakpointSP bkpt_sp = GetSP();
   size_t num_locs = 0;
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     num_locs = bkpt_sp->GetNumLocations();
   }
   return num_locs;
@@ -492,8 +492,8 @@ void SBBreakpoint::SetCommandLineCommands(SBStringList &commands) {
   if (commands.GetSize() == 0)
     return;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      bkpt_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   std::unique_ptr<BreakpointOptions::CommandData> cmd_data_up(
       new BreakpointOptions::CommandData(*commands, eScriptLanguageNone));
 
@@ -525,8 +525,8 @@ bool SBBreakpoint::GetDescription(SBStream &s, bool include_locations) {
 
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     s.Printf("SBBreakpoint: id = %i, ", bkpt_sp->GetID());
     bkpt_sp->GetResolverDescription(s.get());
     bkpt_sp->GetFilterDescription(s.get());
@@ -603,8 +603,8 @@ void SBBreakpoint::SetCallback(SBBreakpointHitCallback callback, void *baton) {
   BreakpointSP bkpt_sp = GetSP();
 
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     BatonSP baton_sp(new SBBreakpointCallbackBaton(callback, baton));
     bkpt_sp->SetCallback(SBBreakpointCallbackBaton
       ::PrivateBreakpointHitCallback, baton_sp,
@@ -628,8 +628,8 @@ SBError SBBreakpoint::SetScriptCallbackFunction(
 
   if (bkpt_sp) {
     Status error;
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     BreakpointOptions &bp_options = bkpt_sp->GetOptions();
     error = bkpt_sp->GetTarget()
         .GetDebugger()
@@ -652,8 +652,8 @@ SBError SBBreakpoint::SetScriptCallbackBody(const char *callback_body_text) {
 
   SBError sb_error;
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     BreakpointOptions &bp_options = bkpt_sp->GetOptions();
     Status error =
         bkpt_sp->GetTarget()
@@ -682,8 +682,8 @@ SBError SBBreakpoint::AddNameWithErrorHandling(const char *new_name) {
 
   SBError status;
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     Status error;
     bkpt_sp->GetTarget().AddNameToBreakpoint(bkpt_sp, new_name, error);
     status.SetError(std::move(error));
@@ -700,8 +700,8 @@ void SBBreakpoint::RemoveName(const char *name_to_remove) {
   BreakpointSP bkpt_sp = GetSP();
 
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     bkpt_sp->GetTarget().RemoveNameFromBreakpoint(
         bkpt_sp, llvm::StringRef(name_to_remove));
   }
@@ -713,8 +713,8 @@ bool SBBreakpoint::MatchesName(const char *name) {
   BreakpointSP bkpt_sp = GetSP();
 
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return bkpt_sp->MatchesName(name);
   }
 
@@ -727,8 +727,8 @@ void SBBreakpoint::GetNames(SBStringList &names) {
   BreakpointSP bkpt_sp = GetSP();
 
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     std::vector<std::string> names_vec;
     bkpt_sp->GetNames(names_vec);
     for (const std::string &name : names_vec) {
@@ -802,8 +802,8 @@ lldb::SBError SBBreakpoint::SetIsHardware(bool is_hardware) {
 
   BreakpointSP bkpt_sp = GetSP();
   if (bkpt_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        bkpt_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = bkpt_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return SBError(Status::FromError(bkpt_sp->SetIsHardware(is_hardware)));
   }
   return SBError();
diff --git a/lldb/source/API/SBBreakpointLocation.cpp b/lldb/source/API/SBBreakpointLocation.cpp
index 2feaa5c805a15..729721943ebdd 100644
--- a/lldb/source/API/SBBreakpointLocation.cpp
+++ b/lldb/source/API/SBBreakpointLocation.cpp
@@ -87,8 +87,8 @@ addr_t SBBreakpointLocation::GetLoadAddress() {
   BreakpointLocationSP loc_sp = GetSP();
 
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     ret_addr = loc_sp->GetLoadAddress();
   }
 
@@ -100,8 +100,8 @@ void SBBreakpointLocation::SetEnabled(bool enabled) {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     llvm::consumeError(loc_sp->SetEnabled(enabled));
   }
 }
@@ -111,8 +111,8 @@ bool SBBreakpointLocation::IsEnabled() {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return loc_sp->IsEnabled();
   } else
     return false;
@@ -123,8 +123,8 @@ uint32_t SBBreakpointLocation::GetHitCount() {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return loc_sp->GetHitCount();
   } else
     return 0;
@@ -135,8 +135,8 @@ uint32_t SBBreakpointLocation::GetIgnoreCount() {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return loc_sp->GetIgnoreCount();
   } else
     return 0;
@@ -147,8 +147,8 @@ void SBBreakpointLocation::SetIgnoreCount(uint32_t n) {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     loc_sp->SetIgnoreCount(n);
   }
 }
@@ -158,8 +158,8 @@ void SBBreakpointLocation::SetCondition(const char *condition) {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     // Treat a nullptr as clearing the condition
     if (!condition)
       loc_sp->SetCondition(StopCondition());
@@ -175,8 +175,8 @@ const char *SBBreakpointLocation::GetCondition() {
   if (!loc_sp)
     return nullptr;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      loc_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   StopCondition cond = loc_sp->GetCondition();
   if (!cond)
     return nullptr;
@@ -188,8 +188,8 @@ void SBBreakpointLocation::SetAutoContinue(bool auto_continue) {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     loc_sp->SetAutoContinue(auto_continue);
   }
 }
@@ -199,8 +199,8 @@ bool SBBreakpointLocation::GetAutoContinue() {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return loc_sp->IsAutoContinue();
   }
   return false;
@@ -213,8 +213,8 @@ void SBBreakpointLocation::SetCallback(SBBreakpointHitCallback callback,
   BreakpointLocationSP loc_sp = GetSP();
 
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     BatonSP baton_sp(new SBBreakpointCallbackBaton(callback, baton));
     loc_sp->SetCallback(SBBreakpointCallbackBaton::PrivateBreakpointHitCallback,
                         baton_sp, false);
@@ -235,8 +235,8 @@ SBError SBBreakpointLocation::SetScriptCallbackFunction(
 
   if (loc_sp) {
     Status error;
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     BreakpointOptions &bp_options = loc_sp->GetLocationOptions();
     error = loc_sp->GetBreakpoint()
         .GetTarget()
@@ -261,8 +261,8 @@ SBBreakpointLocation::SetScriptCallbackBody(const char *callback_body_text) {
 
   SBError sb_error;
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     BreakpointOptions &bp_options = loc_sp->GetLocationOptions();
     Status error =
         loc_sp->GetBreakpoint()
@@ -287,8 +287,8 @@ void SBBreakpointLocation::SetCommandLineCommands(SBStringList &commands) {
   if (commands.GetSize() == 0)
     return;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      loc_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   std::unique_ptr<BreakpointOptions::CommandData> cmd_data_up(
       new BreakpointOptions::CommandData(*commands, eScriptLanguageNone));
 
@@ -314,8 +314,8 @@ void SBBreakpointLocation::SetThreadID(lldb::tid_t thread_id) {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     loc_sp->SetThreadID(thread_id);
   }
 }
@@ -326,8 +326,8 @@ lldb::tid_t SBBreakpointLocation::GetThreadID() {
   lldb::tid_t tid = LLDB_INVALID_THREAD_ID;
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return loc_sp->GetThreadID();
   }
   return tid;
@@ -338,8 +338,8 @@ void SBBreakpointLocation::SetThreadIndex(uint32_t index) {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     loc_sp->SetThreadIndex(index);
   }
 }
@@ -350,8 +350,8 @@ uint32_t SBBreakpointLocation::GetThreadIndex() const {
   uint32_t thread_idx = UINT32_MAX;
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return loc_sp->GetThreadIndex();
   }
   return thread_idx;
@@ -362,8 +362,8 @@ void SBBreakpointLocation::SetThreadName(const char *thread_name) {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     loc_sp->SetThreadName(thread_name);
   }
 }
@@ -375,8 +375,8 @@ const char *SBBreakpointLocation::GetThreadName() const {
   if (!loc_sp)
     return nullptr;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      loc_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   return ConstString(loc_sp->GetThreadName()).GetCString();
 }
 
@@ -385,8 +385,8 @@ void SBBreakpointLocation::SetQueueName(const char *queue_name) {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     loc_sp->SetQueueName(queue_name);
   }
 }
@@ -398,8 +398,8 @@ const char *SBBreakpointLocation::GetQueueName() const {
   if (!loc_sp)
     return nullptr;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      loc_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   return ConstString(loc_sp->GetQueueName()).GetCString();
 }
 
@@ -408,8 +408,8 @@ bool SBBreakpointLocation::IsResolved() {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return loc_sp->IsResolved();
   }
   return false;
@@ -429,8 +429,8 @@ bool SBBreakpointLocation::GetDescription(SBStream &description,
   BreakpointLocationSP loc_sp = GetSP();
 
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     loc_sp->GetDescription(&strm, level);
     strm.EOL();
   } else
@@ -444,8 +444,8 @@ break_id_t SBBreakpointLocation::GetID() {
 
   BreakpointLocationSP loc_sp = GetSP();
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return loc_sp->GetID();
   } else
     return LLDB_INVALID_BREAK_ID;
@@ -458,8 +458,8 @@ SBBreakpoint SBBreakpointLocation::GetBreakpoint() {
 
   SBBreakpoint sb_bp;
   if (loc_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        loc_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = loc_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     sb_bp = loc_sp->GetBreakpoint().shared_from_this();
   }
 
diff --git a/lldb/source/API/SBBreakpointName.cpp b/lldb/source/API/SBBreakpointName.cpp
index 1dcbecaf6da76..f57a0918743ea 100644
--- a/lldb/source/API/SBBreakpointName.cpp
+++ b/lldb/source/API/SBBreakpointName.cpp
@@ -209,8 +209,8 @@ void SBBreakpointName::SetEnabled(bool enable) {
   if (!bp_name)
     return;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   bp_name->GetOptions().SetEnabled(enable);
   UpdateName(*bp_name);
@@ -234,8 +234,8 @@ bool SBBreakpointName::IsEnabled() {
   if (!bp_name)
     return false;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   return bp_name->GetOptions().IsEnabled();
 }
@@ -247,8 +247,8 @@ void SBBreakpointName::SetOneShot(bool one_shot) {
   if (!bp_name)
     return;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   bp_name->GetOptions().SetOneShot(one_shot);
   UpdateName(*bp_name);
@@ -261,8 +261,8 @@ bool SBBreakpointName::IsOneShot() const {
   if (!bp_name)
     return false;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   return bp_name->GetOptions().IsOneShot();
 }
@@ -274,8 +274,8 @@ void SBBreakpointName::SetIgnoreCount(uint32_t count) {
   if (!bp_name)
     return;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   bp_name->GetOptions().SetIgnoreCount(count);
   UpdateName(*bp_name);
@@ -288,8 +288,8 @@ uint32_t SBBreakpointName::GetIgnoreCount() const {
   if (!bp_name)
     return false;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   return bp_name->GetOptions().GetIgnoreCount();
 }
@@ -301,8 +301,8 @@ void SBBreakpointName::SetCondition(const char *condition) {
   if (!bp_name)
     return;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   bp_name->GetOptions().SetCondition(StopCondition(condition));
   UpdateName(*bp_name);
@@ -315,8 +315,8 @@ const char *SBBreakpointName::GetCondition() {
   if (!bp_name)
     return nullptr;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   return ConstString(bp_name->GetOptions().GetCondition().GetText())
       .GetCString();
@@ -329,8 +329,8 @@ void SBBreakpointName::SetAutoContinue(bool auto_continue) {
   if (!bp_name)
     return;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   bp_name->GetOptions().SetAutoContinue(auto_continue);
   UpdateName(*bp_name);
@@ -343,8 +343,8 @@ bool SBBreakpointName::GetAutoContinue() {
   if (!bp_name)
     return false;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   return bp_name->GetOptions().IsAutoContinue();
 }
@@ -356,8 +356,8 @@ void SBBreakpointName::SetThreadID(lldb::tid_t tid) {
   if (!bp_name)
     return;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   bp_name->GetOptions().SetThreadID(tid);
   UpdateName(*bp_name);
@@ -370,8 +370,8 @@ lldb::tid_t SBBreakpointName::GetThreadID() {
   if (!bp_name)
     return LLDB_INVALID_THREAD_ID;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   return bp_name->GetOptions().GetThreadSpec()->GetTID();
 }
@@ -383,8 +383,8 @@ void SBBreakpointName::SetThreadIndex(uint32_t index) {
   if (!bp_name)
     return;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   bp_name->GetOptions().GetThreadSpec()->SetIndex(index);
   UpdateName(*bp_name);
@@ -397,8 +397,8 @@ uint32_t SBBreakpointName::GetThreadIndex() const {
   if (!bp_name)
     return LLDB_INVALID_THREAD_ID;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   return bp_name->GetOptions().GetThreadSpec()->GetIndex();
 }
@@ -410,8 +410,8 @@ void SBBreakpointName::SetThreadName(const char *thread_name) {
   if (!bp_name)
     return;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   bp_name->GetOptions().GetThreadSpec()->SetName(thread_name);
   UpdateName(*bp_name);
@@ -424,8 +424,8 @@ const char *SBBreakpointName::GetThreadName() const {
   if (!bp_name)
     return nullptr;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   return ConstString(bp_name->GetOptions().GetThreadSpec()->GetName())
       .GetCString();
@@ -438,8 +438,8 @@ void SBBreakpointName::SetQueueName(const char *queue_name) {
   if (!bp_name)
     return;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   bp_name->GetOptions().GetThreadSpec()->SetQueueName(queue_name);
   UpdateName(*bp_name);
@@ -452,8 +452,8 @@ const char *SBBreakpointName::GetQueueName() const {
   if (!bp_name)
     return nullptr;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   return ConstString(bp_name->GetOptions().GetThreadSpec()->GetQueueName())
       .GetCString();
@@ -468,9 +468,8 @@ void SBBreakpointName::SetCommandLineCommands(SBStringList &commands) {
   if (commands.GetSize() == 0)
     return;
 
-
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   std::unique_ptr<BreakpointOptions::CommandData> cmd_data_up(
       new BreakpointOptions::CommandData(*commands, eScriptLanguageNone));
 
@@ -510,9 +509,8 @@ void SBBreakpointName::SetHelpString(const char *help_string) {
   if (!bp_name)
     return;
 
-
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   bp_name->SetHelp(help_string);
 }
 
@@ -526,8 +524,8 @@ bool SBBreakpointName::GetDescription(SBStream &s) {
     return false;
   }
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   bp_name->GetDescription(s.get(), eDescriptionLevelFull);
   return true;
 }
@@ -539,8 +537,8 @@ void SBBreakpointName::SetCallback(SBBreakpointHitCallback callback,
   BreakpointName *bp_name = GetBreakpointName();
   if (!bp_name)
     return;
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   BatonSP baton_sp(new SBBreakpointCallbackBaton(callback, baton));
   bp_name->GetOptions().SetCallback(SBBreakpointCallbackBaton
@@ -568,8 +566,8 @@ SBError SBBreakpointName::SetScriptCallbackFunction(
     return sb_error;
   }
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   BreakpointOptions &bp_options = bp_name->GetOptions();
   Status error = m_impl_up->GetTarget()
@@ -592,8 +590,8 @@ SBBreakpointName::SetScriptCallbackBody(const char *callback_body_text) {
   if (!bp_name)
     return sb_error;
 
-  std::lock_guard<std::recursive_mutex> guard(
-        m_impl_up->GetTarget()->GetAPIMutex());
+  TargetAPIMutex api_lock = m_impl_up->GetTarget()->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   BreakpointOptions &bp_options = bp_name->GetOptions();
   Status error = m_impl_up->GetTarget()
diff --git a/lldb/source/API/SBCommandInterpreter.cpp b/lldb/source/API/SBCommandInterpreter.cpp
index ae6b3d3418655..57c6ae375c0ac 100644
--- a/lldb/source/API/SBCommandInterpreter.cpp
+++ b/lldb/source/API/SBCommandInterpreter.cpp
@@ -385,7 +385,8 @@ SBProcess SBCommandInterpreter::GetProcess() {
   if (IsValid()) {
     TargetSP target_sp(m_opaque_ptr->GetSelectedTarget());
     if (target_sp) {
-      std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+      TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       process_sp = target_sp->GetProcessSP();
       sb_process.SetSP(process_sp);
     }
@@ -472,9 +473,12 @@ void SBCommandInterpreter::SourceInitFileInGlobalDirectory(
   result.Clear();
   if (IsValid()) {
     TargetSP target_sp(m_opaque_ptr->GetSelectedTarget());
-    std::unique_lock<std::recursive_mutex> lock;
-    if (target_sp)
-      lock = std::unique_lock<std::recursive_mutex>(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock;
+    std::unique_lock<TargetAPIMutex> guard;
+    if (target_sp) {
+      api_lock = TargetAPIMutex(target_sp->GetAPIMutex());
+      guard = std::unique_lock<TargetAPIMutex>(api_lock);
+    }
     m_opaque_ptr->SourceInitFileGlobal(result.ref());
   } else {
     result->AppendError("SBCommandInterpreter is not valid");
@@ -495,9 +499,12 @@ void SBCommandInterpreter::SourceInitFileInHomeDirectory(
   result.Clear();
   if (IsValid()) {
     TargetSP target_sp(m_opaque_ptr->GetSelectedTarget());
-    std::unique_lock<std::recursive_mutex> lock;
-    if (target_sp)
-      lock = std::unique_lock<std::recursive_mutex>(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock;
+    std::unique_lock<TargetAPIMutex> guard;
+    if (target_sp) {
+      api_lock = TargetAPIMutex(target_sp->GetAPIMutex());
+      guard = std::unique_lock<TargetAPIMutex>(api_lock);
+    }
     m_opaque_ptr->SourceInitFileHome(result.ref(), is_repl);
   } else {
     result->AppendError("SBCommandInterpreter is not valid");
@@ -511,9 +518,12 @@ void SBCommandInterpreter::SourceInitFileInCurrentWorkingDirectory(
   result.Clear();
   if (IsValid()) {
     TargetSP target_sp(m_opaque_ptr->GetSelectedTarget());
-    std::unique_lock<std::recursive_mutex> lock;
-    if (target_sp)
-      lock = std::unique_lock<std::recursive_mutex>(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock;
+    std::unique_lock<TargetAPIMutex> guard;
+    if (target_sp) {
+      api_lock = TargetAPIMutex(target_sp->GetAPIMutex());
+      guard = std::unique_lock<TargetAPIMutex>(api_lock);
+    }
     m_opaque_ptr->SourceInitFileCwd(result.ref());
   } else {
     result->AppendError("SBCommandInterpreter is not valid");
diff --git a/lldb/source/API/SBDebugger.cpp b/lldb/source/API/SBDebugger.cpp
index 95c4f761c9963..71cd0f5841654 100644
--- a/lldb/source/API/SBDebugger.cpp
+++ b/lldb/source/API/SBDebugger.cpp
@@ -531,9 +531,12 @@ void SBDebugger::HandleCommand(const char *command) {
   if (m_opaque_sp) {
     TargetSP target_sp(
         m_opaque_sp->GetCommandInterpreter().GetSelectedTarget());
-    std::unique_lock<std::recursive_mutex> lock;
-    if (target_sp)
-      lock = std::unique_lock<std::recursive_mutex>(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock;
+    std::unique_lock<TargetAPIMutex> guard;
+    if (target_sp) {
+      api_lock = TargetAPIMutex(target_sp->GetAPIMutex());
+      guard = std::unique_lock<TargetAPIMutex>(api_lock);
+    }
 
     SBCommandInterpreter sb_interpreter(GetCommandInterpreter());
     SBCommandReturnObject result;
@@ -606,7 +609,8 @@ void SBDebugger::HandleProcessEvent(const SBProcess &process,
   char stdio_buffer[1024];
   size_t len;
 
-  std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+  TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   if (event_type &
       (Process::eBroadcastBitSTDOUT | Process::eBroadcastBitStateChanged)) {
diff --git a/lldb/source/API/SBFunction.cpp b/lldb/source/API/SBFunction.cpp
index c3cdba64417c8..2029069cdb3cb 100644
--- a/lldb/source/API/SBFunction.cpp
+++ b/lldb/source/API/SBFunction.cpp
@@ -129,10 +129,12 @@ SBInstructionList SBFunction::GetInstructions(SBTarget target,
   SBInstructionList sb_instructions;
   if (m_opaque_ptr) {
     TargetSP target_sp(target.GetSP());
-    std::unique_lock<std::recursive_mutex> lock;
+    TargetAPIMutex api_lock;
+    std::unique_lock<TargetAPIMutex> guard;
     ModuleSP module_sp(m_opaque_ptr->GetAddress().GetModule());
     if (target_sp && module_sp) {
-      lock = std::unique_lock<std::recursive_mutex>(target_sp->GetAPIMutex());
+      api_lock = TargetAPIMutex(target_sp->GetAPIMutex());
+      guard = std::unique_lock<TargetAPIMutex>(api_lock);
       const bool force_live_memory = true;
       sb_instructions.SetDisassembler(Disassembler::DisassembleRange(
           module_sp->GetArchitecture(), nullptr, flavor,
diff --git a/lldb/source/API/SBInstruction.cpp b/lldb/source/API/SBInstruction.cpp
index dc4d475f5fb3d..ad66ebec3abcf 100644
--- a/lldb/source/API/SBInstruction.cpp
+++ b/lldb/source/API/SBInstruction.cpp
@@ -117,9 +117,11 @@ const char *SBInstruction::GetMnemonic(SBTarget target) {
 
   ExecutionContext exe_ctx;
   TargetSP target_sp(target.GetSP());
-  std::unique_lock<std::recursive_mutex> lock;
+  TargetAPIMutex api_lock;
+  std::unique_lock<TargetAPIMutex> guard;
   if (target_sp) {
-    lock = std::unique_lock<std::recursive_mutex>(target_sp->GetAPIMutex());
+    api_lock = TargetAPIMutex(target_sp->GetAPIMutex());
+    guard = std::unique_lock<TargetAPIMutex>(api_lock);
 
     target_sp->CalculateExecutionContext(exe_ctx);
     exe_ctx.SetProcessSP(target_sp->GetProcessSP());
@@ -136,9 +138,11 @@ const char *SBInstruction::GetOperands(SBTarget target) {
 
   ExecutionContext exe_ctx;
   TargetSP target_sp(target.GetSP());
-  std::unique_lock<std::recursive_mutex> lock;
+  TargetAPIMutex api_lock;
+  std::unique_lock<TargetAPIMutex> guard;
   if (target_sp) {
-    lock = std::unique_lock<std::recursive_mutex>(target_sp->GetAPIMutex());
+    api_lock = TargetAPIMutex(target_sp->GetAPIMutex());
+    guard = std::unique_lock<TargetAPIMutex>(api_lock);
 
     target_sp->CalculateExecutionContext(exe_ctx);
     exe_ctx.SetProcessSP(target_sp->GetProcessSP());
@@ -155,9 +159,11 @@ const char *SBInstruction::GetComment(SBTarget target) {
 
   ExecutionContext exe_ctx;
   TargetSP target_sp(target.GetSP());
-  std::unique_lock<std::recursive_mutex> lock;
+  TargetAPIMutex api_lock;
+  std::unique_lock<TargetAPIMutex> guard;
   if (target_sp) {
-    lock = std::unique_lock<std::recursive_mutex>(target_sp->GetAPIMutex());
+    api_lock = TargetAPIMutex(target_sp->GetAPIMutex());
+    guard = std::unique_lock<TargetAPIMutex>(api_lock);
 
     target_sp->CalculateExecutionContext(exe_ctx);
     exe_ctx.SetProcessSP(target_sp->GetProcessSP());
@@ -173,9 +179,11 @@ SBInstruction::GetControlFlowKind(lldb::SBTarget target) {
   if (inst_sp) {
     ExecutionContext exe_ctx;
     TargetSP target_sp(target.GetSP());
-    std::unique_lock<std::recursive_mutex> lock;
+    TargetAPIMutex api_lock;
+    std::unique_lock<TargetAPIMutex> guard;
     if (target_sp) {
-      lock = std::unique_lock<std::recursive_mutex>(target_sp->GetAPIMutex());
+      api_lock = TargetAPIMutex(target_sp->GetAPIMutex());
+      guard = std::unique_lock<TargetAPIMutex>(api_lock);
 
       target_sp->CalculateExecutionContext(exe_ctx);
       exe_ctx.SetProcessSP(target_sp->GetProcessSP());
diff --git a/lldb/source/API/SBMutex.cpp b/lldb/source/API/SBMutex.cpp
index c7844dec658cc..c28cf9374cb21 100644
--- a/lldb/source/API/SBMutex.cpp
+++ b/lldb/source/API/SBMutex.cpp
@@ -7,16 +7,41 @@
 //===----------------------------------------------------------------------===//
 
 #include "lldb/API/SBMutex.h"
-#include "lldb/Target/Target.h"
+#include "lldb/Target/TargetAPIMutex.h"
 #include "lldb/Utility/Instrumentation.h"
 #include "lldb/lldb-forward.h"
 #include <memory>
 #include <mutex>
+#include <variant>
 
 using namespace lldb;
 using namespace lldb_private;
 
-SBMutex::SBMutex() : m_opaque_sp(std::make_shared<std::recursive_mutex>()) {
+/// Holds either a standalone std::recursive_mutex (default-constructed
+/// SBMutex, no Target to resolve through) or a TargetAPIMutex (constructed
+/// from a Target). Kept out of SBMutex.h since std::variant is a C++17
+/// feature and the public SB headers must stay usable from a C++11 client.
+class SBMutex::MutexVariant {
+public:
+  MutexVariant() : m_variant(std::in_place_type<std::recursive_mutex>) {}
+  explicit MutexVariant(lldb::TargetSP target_sp)
+      : m_variant(std::in_place_type<TargetAPIMutex>, std::move(target_sp)) {}
+
+  void lock() {
+    std::visit([](auto &mutex) { mutex.lock(); }, m_variant);
+  }
+  void unlock() {
+    std::visit([](auto &mutex) { mutex.unlock(); }, m_variant);
+  }
+  bool try_lock() {
+    return std::visit([](auto &mutex) { return mutex.try_lock(); }, m_variant);
+  }
+
+private:
+  std::variant<std::recursive_mutex, TargetAPIMutex> m_variant;
+};
+
+SBMutex::SBMutex() : m_opaque_sp(std::make_shared<MutexVariant>()) {
   LLDB_INSTRUMENT_VA(this);
 }
 
@@ -32,8 +57,7 @@ const SBMutex &SBMutex::operator=(const SBMutex &rhs) {
 }
 
 SBMutex::SBMutex(lldb::TargetSP target_sp)
-    : m_opaque_sp(std::shared_ptr<std::recursive_mutex>(
-          target_sp, &target_sp->GetAPIMutex())) {
+    : m_opaque_sp(std::make_shared<MutexVariant>(target_sp)) {
   LLDB_INSTRUMENT_VA(this, target_sp);
 }
 
@@ -62,8 +86,7 @@ void SBMutex::unlock() const {
 bool SBMutex::try_lock() const {
   LLDB_INSTRUMENT_VA(this);
 
-  if (m_opaque_sp)
-    return m_opaque_sp->try_lock();
-
-  return false;
+  if (!m_opaque_sp)
+    return false;
+  return m_opaque_sp->try_lock();
 }
diff --git a/lldb/source/API/SBProcess.cpp b/lldb/source/API/SBProcess.cpp
index 08e39f754cf85..0984daa0e92b9 100644
--- a/lldb/source/API/SBProcess.cpp
+++ b/lldb/source/API/SBProcess.cpp
@@ -136,8 +136,8 @@ bool SBProcess::RemoteLaunch(char const **argv, char const **envp,
 
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     if (process_sp->GetState() == eStateConnected) {
       if (stop_at_entry)
         launch_flags |= eLaunchFlagStopAtEntry;
@@ -169,8 +169,8 @@ bool SBProcess::RemoteAttachToProcessWithID(lldb::pid_t pid,
 
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     if (process_sp->GetState() == eStateConnected) {
       ProcessAttachInfo attach_info;
       attach_info.SetProcessID(pid);
@@ -195,8 +195,8 @@ uint32_t SBProcess::GetNumThreads() {
     Process::StopLocker stop_locker;
 
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       num_threads = process_sp->GetThreadList().GetSize();
     }
   }
@@ -211,8 +211,8 @@ SBThread SBProcess::GetSelectedThread() const {
   ThreadSP thread_sp;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     thread_sp = process_sp->GetThreadList().GetSelectedThread();
     sb_thread.SetThread(thread_sp);
   }
@@ -228,8 +228,8 @@ SBThread SBProcess::CreateOSPluginThread(lldb::tid_t tid,
   ThreadSP thread_sp;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     thread_sp = process_sp->CreateOSPluginThread(tid, context);
     sb_thread.SetThread(thread_sp);
   }
@@ -352,8 +352,8 @@ bool SBProcess::SetSelectedThread(const SBThread &thread) {
 
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return process_sp->GetThreadList().SetSelectedThreadByID(
         thread.GetThreadID());
   }
@@ -366,8 +366,8 @@ bool SBProcess::SetSelectedThreadByID(lldb::tid_t tid) {
   bool ret_val = false;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     ret_val = process_sp->GetThreadList().SetSelectedThreadByID(tid);
   }
 
@@ -380,8 +380,8 @@ bool SBProcess::SetSelectedThreadByIndexID(uint32_t index_id) {
   bool ret_val = false;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     ret_val = process_sp->GetThreadList().SetSelectedThreadByIndexID(index_id);
   }
 
@@ -397,8 +397,8 @@ SBThread SBProcess::GetThreadAtIndex(size_t index) {
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       thread_sp = process_sp->GetThreadList().GetThreadAtIndex(index, false);
       sb_thread.SetThread(thread_sp);
     }
@@ -415,8 +415,8 @@ uint32_t SBProcess::GetNumQueues() {
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       num_queues = process_sp->GetQueueList().GetSize();
     }
   }
@@ -433,8 +433,8 @@ SBQueue SBProcess::GetQueueAtIndex(size_t index) {
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       queue_sp = process_sp->GetQueueList().GetQueueAtIndex(index);
       sb_queue.SetQueue(queue_sp);
     }
@@ -448,8 +448,8 @@ uint32_t SBProcess::GetStopID(bool include_expression_stops) {
 
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     if (include_expression_stops)
       return process_sp->GetStopID();
     else
@@ -465,8 +465,8 @@ SBEvent SBProcess::GetStopEventForStopID(uint32_t stop_id) {
   EventSP event_sp;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     event_sp = process_sp->GetStopEventForStopID(stop_id);
     sb_event.reset(event_sp);
   }
@@ -478,8 +478,8 @@ void SBProcess::ForceScriptedState(StateType new_state) {
   LLDB_INSTRUMENT_VA(this, new_state);
 
   if (ProcessSP process_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     process_sp->ForceScriptedState(new_state);
   }
 }
@@ -490,8 +490,8 @@ StateType SBProcess::GetState() {
   StateType ret_val = eStateInvalid;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     ret_val = process_sp->GetState();
   }
 
@@ -504,8 +504,8 @@ int SBProcess::GetExitStatus() {
   int exit_status = 0;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     exit_status = process_sp->GetExitStatus();
   }
 
@@ -519,8 +519,8 @@ const char *SBProcess::GetExitDescription() {
   if (!process_sp)
     return nullptr;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      process_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   return ConstString(process_sp->GetExitDescription()).GetCString();
 }
 
@@ -574,8 +574,8 @@ SBError SBProcess::Continue() {
   ProcessSP process_sp(GetSP());
 
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
 
     if (process_sp->GetTarget().GetDebugger().GetAsyncExecution())
       sb_error.ref() = process_sp->Resume();
@@ -605,8 +605,8 @@ SBError SBProcess::Destroy() {
   SBError sb_error;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     sb_error.SetError(process_sp->Destroy(false));
   } else
     sb_error = Status::FromErrorString("SBProcess is invalid");
@@ -620,8 +620,8 @@ SBError SBProcess::Stop() {
   SBError sb_error;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     sb_error.SetError(process_sp->Halt());
   } else
     sb_error = Status::FromErrorString("SBProcess is invalid");
@@ -635,8 +635,8 @@ SBError SBProcess::Kill() {
   SBError sb_error;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     sb_error.SetError(process_sp->Destroy(true));
   } else
     sb_error = Status::FromErrorString("SBProcess is invalid");
@@ -658,8 +658,8 @@ SBError SBProcess::Detach(bool keep_stopped) {
   SBError sb_error;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     sb_error.SetError(process_sp->Detach(keep_stopped));
   } else
     sb_error = Status::FromErrorString("SBProcess is invalid");
@@ -673,8 +673,8 @@ SBError SBProcess::Signal(int signo) {
   SBError sb_error;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     sb_error.SetError(process_sp->Signal(signo));
   } else
     sb_error = Status::FromErrorString("SBProcess is invalid");
@@ -709,8 +709,8 @@ SBThread SBProcess::GetThreadByID(tid_t tid) {
   if (process_sp) {
     Process::StopLocker stop_locker;
     const bool can_update = stop_locker.TryLock(&process_sp->GetRunLock());
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     thread_sp = process_sp->GetThreadList().FindThreadByID(tid, can_update);
     sb_thread.SetThread(thread_sp);
   }
@@ -727,8 +727,8 @@ SBThread SBProcess::GetThreadByIndexID(uint32_t index_id) {
   if (process_sp) {
     Process::StopLocker stop_locker;
     const bool can_update = stop_locker.TryLock(&process_sp->GetRunLock());
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     thread_sp =
         process_sp->GetThreadList().FindThreadByIndexID(index_id, can_update);
     sb_thread.SetThread(thread_sp);
@@ -844,8 +844,8 @@ lldb::SBAddressRangeList SBProcess::FindRangesInMemory(
     error = Status::FromErrorString("process is running");
     return matches;
   }
-  std::lock_guard<std::recursive_mutex> guard(
-      process_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   matches.m_opaque_up->ref() = process_sp->FindRangesInMemory(
       reinterpret_cast<const uint8_t *>(buf), size, ranges.ref().ref(),
       alignment, max_matches, error.ref());
@@ -870,8 +870,8 @@ lldb::addr_t SBProcess::FindInMemory(const void *buf, uint64_t size,
     return LLDB_INVALID_ADDRESS;
   }
 
-  std::lock_guard<std::recursive_mutex> guard(
-      process_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   return process_sp->FindInMemory(reinterpret_cast<const uint8_t *>(buf), size,
                                   range.ref(), alignment, error.ref());
 }
@@ -889,12 +889,11 @@ size_t SBProcess::ReadMemory(addr_t addr, void *dst, size_t dst_len,
   size_t bytes_read = 0;
   ProcessSP process_sp(GetSP());
 
-
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       bytes_read = process_sp->ReadMemory(addr, dst, dst_len, sb_error.ref());
     } else {
       sb_error = Status::FromErrorString("process is running");
@@ -915,8 +914,8 @@ size_t SBProcess::ReadCStringFromMemory(addr_t addr, void *buf, size_t size,
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       bytes_read = process_sp->ReadCStringFromMemory(addr, (char *)buf, size,
                                                      sb_error.ref());
     } else {
@@ -937,8 +936,8 @@ uint64_t SBProcess::ReadUnsignedFromMemory(addr_t addr, uint32_t byte_size,
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       value = process_sp->ReadUnsignedIntegerFromMemory(addr, byte_size, 0,
                                                         sb_error.ref());
     } else {
@@ -959,8 +958,8 @@ lldb::addr_t SBProcess::ReadPointerFromMemory(addr_t addr,
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       ptr = process_sp->ReadPointerFromMemory(addr, sb_error.ref());
     } else {
       sb_error = Status::FromErrorString("process is running");
@@ -982,8 +981,8 @@ size_t SBProcess::WriteMemory(addr_t addr, const void *src, size_t src_len,
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       bytes_written =
           process_sp->WriteMemory(addr, src, src_len, sb_error.ref());
     } else {
@@ -1060,8 +1059,8 @@ SBProcess::GetNumSupportedHardwareWatchpoints(lldb::SBError &sb_error) const {
   uint32_t num = 0;
   ProcessSP process_sp(GetSP());
   if (process_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     std::optional<uint32_t> actual_num = process_sp->GetWatchpointSlotCount();
     if (actual_num) {
       num = *actual_num;
@@ -1091,8 +1090,8 @@ uint32_t SBProcess::LoadImage(const lldb::SBFileSpec &sb_local_image_spec,
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       PlatformSP platform_sp = process_sp->GetTarget().GetPlatform();
       return platform_sp->LoadImage(process_sp.get(), *sb_local_image_spec,
                                     *sb_remote_image_spec, sb_error.ref());
@@ -1115,8 +1114,8 @@ uint32_t SBProcess::LoadImageUsingPaths(const lldb::SBFileSpec &image_spec,
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       PlatformSP platform_sp = process_sp->GetTarget().GetPlatform();
       size_t num_paths = paths.GetSize();
       std::vector<std::string> paths_vec;
@@ -1148,8 +1147,8 @@ lldb::SBError SBProcess::UnloadImage(uint32_t image_token) {
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       PlatformSP platform_sp = process_sp->GetTarget().GetPlatform();
       sb_error.SetError(
           platform_sp->UnloadImage(process_sp.get(), image_token));
@@ -1169,8 +1168,8 @@ lldb::SBError SBProcess::SendEventData(const char *event_data) {
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       sb_error.SetError(process_sp->SendEventData(event_data));
     } else {
       sb_error = Status::FromErrorString("process is running");
@@ -1225,8 +1224,8 @@ bool SBProcess::IsInstrumentationRuntimePresent(
   if (!process_sp)
     return false;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      process_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   InstrumentationRuntimeSP runtime_sp =
       process_sp->GetInstrumentationRuntime(type);
@@ -1278,8 +1277,8 @@ lldb::SBError SBProcess::SaveCore(SBSaveCoreOptions &options) {
     return error;
   }
 
-  std::lock_guard<std::recursive_mutex> guard(
-      process_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   if (process_sp->GetState() != eStateStopped) {
     error = Status::FromErrorString("the process is not stopped");
@@ -1301,8 +1300,8 @@ SBProcess::GetMemoryRegionInfo(lldb::addr_t load_addr,
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
 
       sb_error.ref() =
           process_sp->GetMemoryRegionInfo(load_addr, sb_region_info.ref());
@@ -1323,8 +1322,8 @@ lldb::SBMemoryRegionInfoList SBProcess::GetMemoryRegions() {
   ProcessSP process_sp(GetSP());
   Process::StopLocker stop_locker;
   if (process_sp && stop_locker.TryLock(&process_sp->GetRunLock())) {
-    std::lock_guard<std::recursive_mutex> guard(
-        process_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
 
     process_sp->GetMemoryRegions(sb_region_list.ref());
   }
@@ -1465,8 +1464,8 @@ lldb::addr_t SBProcess::AllocateMemory(size_t size, uint32_t permissions,
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       addr = process_sp->AllocateMemory(size, permissions, sb_error.ref());
     } else {
       sb_error = Status::FromErrorString("process is running");
@@ -1485,8 +1484,8 @@ lldb::SBError SBProcess::DeallocateMemory(lldb::addr_t ptr) {
   if (process_sp) {
     Process::StopLocker stop_locker;
     if (stop_locker.TryLock(&process_sp->GetRunLock())) {
-      std::lock_guard<std::recursive_mutex> guard(
-          process_sp->GetTarget().GetAPIMutex());
+      TargetAPIMutex api_lock = process_sp->GetTarget().GetAPIMutex();
+      std::lock_guard<TargetAPIMutex> guard(api_lock);
       Status error = process_sp->DeallocateMemory(ptr);
       sb_error.SetError(std::move(error));
     } else {
diff --git a/lldb/source/API/SBSymbol.cpp b/lldb/source/API/SBSymbol.cpp
index 19f2f5e62fd48..25468f88c2de7 100644
--- a/lldb/source/API/SBSymbol.cpp
+++ b/lldb/source/API/SBSymbol.cpp
@@ -127,9 +127,11 @@ SBInstructionList SBSymbol::GetInstructions(SBTarget target,
   SBInstructionList sb_instructions;
   if (m_opaque_ptr) {
     TargetSP target_sp(target.GetSP());
-    std::unique_lock<std::recursive_mutex> lock;
+    TargetAPIMutex api_lock;
+    std::unique_lock<TargetAPIMutex> guard;
     if (target_sp && m_opaque_ptr->ValueIsAddress()) {
-      lock = std::unique_lock<std::recursive_mutex>(target_sp->GetAPIMutex());
+      api_lock = TargetAPIMutex(target_sp->GetAPIMutex());
+      guard = std::unique_lock<TargetAPIMutex>(api_lock);
       const Address &symbol_addr = m_opaque_ptr->GetAddressRef();
       ModuleSP module_sp = symbol_addr.GetModule();
       if (module_sp) {
diff --git a/lldb/source/API/SBTarget.cpp b/lldb/source/API/SBTarget.cpp
index 9eca813d8584b..b6ee17091ef6b 100644
--- a/lldb/source/API/SBTarget.cpp
+++ b/lldb/source/API/SBTarget.cpp
@@ -82,7 +82,8 @@ using namespace lldb_private;
 #define DEFAULT_DISASM_BYTE_SIZE 32
 
 static Status AttachToProcess(ProcessAttachInfo &attach_info, Target &target) {
-  std::lock_guard<std::recursive_mutex> guard(target.GetAPIMutex());
+  TargetAPIMutex api_lock = target.GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
 
   auto process_sp = target.GetProcessSP();
   if (process_sp) {
@@ -310,7 +311,8 @@ SBError SBTarget::Install() {
 
   SBError sb_error;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     sb_error.ref() = target_sp->Install(nullptr);
   }
   return sb_error;
@@ -329,7 +331,8 @@ SBProcess SBTarget::Launch(SBListener &listener, char const **argv,
   SBProcess sb_process;
   ProcessSP process_sp;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
 
     if (stop_at_entry)
       launch_flags |= eLaunchFlagStopAtEntry;
@@ -407,7 +410,8 @@ SBProcess SBTarget::Launch(SBLaunchInfo &sb_launch_info, SBError &error) {
 
   SBProcess sb_process;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     StateType state = eStateInvalid;
     {
       ProcessSP process_sp = target_sp->GetProcessSP();
@@ -545,7 +549,8 @@ lldb::SBProcess SBTarget::ConnectRemote(SBListener &listener, const char *url,
   SBProcess sb_process;
   ProcessSP process_sp;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     if (listener.IsValid())
       process_sp =
           target_sp->CreateProcess(listener.m_opaque_sp, plugin_name, nullptr,
@@ -604,7 +609,8 @@ lldb::SBAddress SBTarget::ResolveLoadAddress(lldb::addr_t vm_addr) {
   lldb::SBAddress sb_addr;
   Address &addr = sb_addr.ref();
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     if (target_sp->ResolveLoadAddress(vm_addr, addr))
       return sb_addr;
   }
@@ -621,7 +627,8 @@ lldb::SBAddress SBTarget::ResolveFileAddress(lldb::addr_t file_addr) {
   lldb::SBAddress sb_addr;
   Address &addr = sb_addr.ref();
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     if (target_sp->ResolveFileAddress(file_addr, addr))
       return sb_addr;
   }
@@ -637,7 +644,8 @@ lldb::SBAddress SBTarget::ResolvePastLoadAddress(uint32_t stop_id,
   lldb::SBAddress sb_addr;
   Address &addr = sb_addr.ref();
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     if (target_sp->ResolveLoadAddress(vm_addr, addr))
       return sb_addr;
   }
@@ -672,7 +680,8 @@ size_t SBTarget::ReadMemory(const SBAddress addr, void *buf, size_t size,
 
   size_t bytes_read = 0;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     bytes_read =
         target_sp->ReadMemory(addr.ref(), buf, size, error.ref(), true);
   } else {
@@ -767,7 +776,8 @@ SBBreakpoint SBTarget::BreakpointCreateByLocation(
 
   SBBreakpoint sb_bp;
   if (TargetSP target_sp = GetSP(); target_sp && line != 0) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
 
     const LazyBool check_inlines = eLazyBoolCalculate;
     const LazyBool skip_prologue = eLazyBoolCalculate;
@@ -795,7 +805,8 @@ SBBreakpoint SBTarget::BreakpointCreateByLocation(
 
   SBBreakpoint sb_bp;
   if (TargetSP target_sp = GetSP(); target_sp && line != 0) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
 
     const LazyBool check_inlines = eLazyBoolCalculate;
     const LazyBool skip_prologue = eLazyBoolCalculate;
@@ -820,7 +831,8 @@ SBBreakpoint SBTarget::BreakpointCreateByName(const char *symbol_name,
 
   SBBreakpoint sb_bp;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
 
     const bool internal = false;
     const bool hardware = false;
@@ -892,7 +904,8 @@ lldb::SBBreakpoint SBTarget::BreakpointCreateByName(
     const bool internal = false;
     const bool hardware = false;
     const LazyBool skip_prologue = eLazyBoolCalculate;
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     FunctionNameType mask = static_cast<FunctionNameType>(name_type_mask);
     sb_bp = target_sp->CreateBreakpoint(module_list.get(), comp_unit_list.get(),
                                         symbol_name, mask, symbol_language,
@@ -935,7 +948,8 @@ lldb::SBBreakpoint SBTarget::BreakpointCreateByNames(
 
   SBBreakpoint sb_bp;
   if (TargetSP target_sp = GetSP(); target_sp && num_names > 0) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     const bool internal = false;
     const bool hardware = false;
     FunctionNameType mask = static_cast<FunctionNameType>(name_type_mask);
@@ -980,7 +994,8 @@ lldb::SBBreakpoint SBTarget::BreakpointCreateByRegex(
   SBBreakpoint sb_bp;
   if (TargetSP target_sp = GetSP();
       target_sp && symbol_name_regex && symbol_name_regex[0]) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     RegularExpression regexp((llvm::StringRef(symbol_name_regex)));
     const bool internal = false;
     const bool hardware = false;
@@ -999,7 +1014,8 @@ SBBreakpoint SBTarget::BreakpointCreateByAddress(addr_t address) {
 
   SBBreakpoint sb_bp;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     const bool hardware = false;
     sb_bp = target_sp->CreateBreakpoint(address, false, hardware);
   }
@@ -1016,7 +1032,8 @@ SBBreakpoint SBTarget::BreakpointCreateBySBAddress(SBAddress &sb_address) {
   }
 
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     const bool hardware = false;
     sb_bp = target_sp->CreateBreakpoint(sb_address.ref(), false, hardware);
   }
@@ -1064,7 +1081,8 @@ lldb::SBBreakpoint SBTarget::BreakpointCreateBySourceRegex(
   SBBreakpoint sb_bp;
   if (TargetSP target_sp = GetSP();
       target_sp && source_regex && source_regex[0]) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     const bool hardware = false;
     const LazyBool move_to_nearest_code = eLazyBoolCalculate;
     RegularExpression regexp((llvm::StringRef(source_regex)));
@@ -1088,7 +1106,8 @@ SBTarget::BreakpointCreateForException(lldb::LanguageType language,
 
   SBBreakpoint sb_bp;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     const bool hardware = false;
     sb_bp = target_sp->CreateExceptionBreakpoint(language, catch_bp, throw_bp,
                                                   hardware);
@@ -1106,7 +1125,8 @@ lldb::SBBreakpoint SBTarget::BreakpointCreateFromScript(
 
   SBBreakpoint sb_bp;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     Status error;
 
     StructuredData::ObjectSP obj_sp = extra_args.m_impl_up->GetObjectSP();
@@ -1149,7 +1169,8 @@ bool SBTarget::BreakpointDelete(break_id_t bp_id) {
 
   bool result = false;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     result = target_sp->RemoveBreakpointByID(bp_id);
   }
 
@@ -1162,7 +1183,8 @@ SBBreakpoint SBTarget::FindBreakpointByID(break_id_t bp_id) {
   SBBreakpoint sb_breakpoint;
   if (TargetSP target_sp = GetSP();
       target_sp && bp_id != LLDB_INVALID_BREAK_ID) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     sb_breakpoint = target_sp->GetBreakpointByID(bp_id);
   }
 
@@ -1174,7 +1196,8 @@ bool SBTarget::FindBreakpointsByName(const char *name,
   LLDB_INSTRUMENT_VA(this, name, bkpts);
 
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     llvm::Expected<std::vector<BreakpointSP>> expected_vector =
         target_sp->GetBreakpointList().FindBreakpointsByName(name);
     if (!expected_vector) {
@@ -1195,7 +1218,8 @@ void SBTarget::GetBreakpointNames(SBStringList &names) {
   names.Clear();
 
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
 
     std::vector<std::string> name_vec;
     target_sp->GetBreakpointNames(name_vec);
@@ -1208,7 +1232,8 @@ void SBTarget::DeleteBreakpointName(const char *name) {
   LLDB_INSTRUMENT_VA(this, name);
 
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     target_sp->DeleteBreakpointName(llvm::StringRef(name));
   }
 }
@@ -1217,7 +1242,8 @@ bool SBTarget::EnableAllBreakpoints() {
   LLDB_INSTRUMENT_VA(this);
 
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     target_sp->EnableAllowedBreakpoints();
     return true;
   }
@@ -1228,7 +1254,8 @@ bool SBTarget::DisableAllBreakpoints() {
   LLDB_INSTRUMENT_VA(this);
 
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     target_sp->DisableAllowedBreakpoints();
     return true;
   }
@@ -1239,7 +1266,8 @@ bool SBTarget::DeleteAllBreakpoints() {
   LLDB_INSTRUMENT_VA(this);
 
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     target_sp->RemoveAllowedBreakpoints();
     return true;
   }
@@ -1261,7 +1289,8 @@ lldb::SBError SBTarget::BreakpointsCreateFromFile(SBFileSpec &source_file,
 
   SBError sberr;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
 
     BreakpointIDList bp_ids;
 
@@ -1306,7 +1335,8 @@ lldb::SBError SBTarget::BreakpointsWriteToFile(SBFileSpec &dest_file,
 
   SBError sberr;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     BreakpointIDList bp_id_list;
     bkpt_list.CopyToBreakpointIDList(bp_id_list);
     sberr.ref() = target_sp->SerializeBreakpointsToFile(dest_file.ref(),
@@ -1343,7 +1373,8 @@ bool SBTarget::DeleteWatchpoint(watch_id_t wp_id) {
 
   bool result = false;
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     std::unique_lock<std::recursive_mutex> lock;
     target_sp->GetWatchpointList().GetListMutex(lock);
     result = target_sp->RemoveWatchpointByID(wp_id);
@@ -1359,7 +1390,8 @@ SBWatchpoint SBTarget::FindWatchpointByID(lldb::watch_id_t wp_id) {
   lldb::WatchpointSP watchpoint_sp;
   if (TargetSP target_sp = GetSP();
       target_sp && wp_id != LLDB_INVALID_WATCH_ID) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     std::unique_lock<std::recursive_mutex> lock;
     target_sp->GetWatchpointList().GetListMutex(lock);
     watchpoint_sp = target_sp->GetWatchpointList().FindByID(wp_id);
@@ -1404,7 +1436,8 @@ SBTarget::WatchpointCreateByAddress(lldb::addr_t addr, size_t size,
 
   if (TargetSP target_sp = GetSP();
       target_sp && addr != LLDB_INVALID_ADDRESS && size > 0) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     // Target::CreateWatchpoint() is thread safe.
     Status cw_error;
     // This API doesn't take in a type, so we can't figure out what it is.
@@ -1422,7 +1455,8 @@ bool SBTarget::EnableAllWatchpoints() {
   LLDB_INSTRUMENT_VA(this);
 
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     std::unique_lock<std::recursive_mutex> lock;
     target_sp->GetWatchpointList().GetListMutex(lock);
     target_sp->EnableAllWatchpoints();
@@ -1435,7 +1469,8 @@ bool SBTarget::DisableAllWatchpoints() {
   LLDB_INSTRUMENT_VA(this);
 
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     std::unique_lock<std::recursive_mutex> lock;
     target_sp->GetWatchpointList().GetListMutex(lock);
     target_sp->DisableAllWatchpoints();
@@ -1500,7 +1535,8 @@ bool SBTarget::DeleteAllWatchpoints() {
   LLDB_INSTRUMENT_VA(this);
 
   if (TargetSP target_sp = GetSP()) {
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     std::unique_lock<std::recursive_mutex> lock;
     target_sp->GetWatchpointList().GetListMutex(lock);
     target_sp->RemoveAllWatchpoints();
@@ -2457,7 +2493,8 @@ lldb::SBValue SBTarget::EvaluateExpression(const char *expr,
     if (expr == nullptr || expr[0] == '\0')
       return expr_result;
 
-    std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+    TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     ExecutionContext exe_ctx(m_opaque_sp.get());
 
     frame = exe_ctx.GetFramePtr();
diff --git a/lldb/source/API/SBThread.cpp b/lldb/source/API/SBThread.cpp
index fafb533771e56..90cd0fe447540 100644
--- a/lldb/source/API/SBThread.cpp
+++ b/lldb/source/API/SBThread.cpp
@@ -464,7 +464,8 @@ static Status ResumeNewPlan(StoppedExecutionContext exe_ctx,
   process->GetThreadList().SetSelectedThreadByID(thread->GetID());
 
   // Release the run lock but keep the API lock.
-  std::unique_lock<std::recursive_mutex> api_lock = exe_ctx.AllowResume();
+  TargetAPIMutex api_mutex = exe_ctx.AllowResume();
+  std::lock_guard<TargetAPIMutex> guard(api_mutex, std::adopt_lock);
   if (process->GetTarget().GetDebugger().GetAsyncExecution())
     return process->Resume();
   return process->ResumeSynchronous(nullptr);
diff --git a/lldb/source/API/SBValue.cpp b/lldb/source/API/SBValue.cpp
index de660577b3a9b..77f0331aeed8c 100644
--- a/lldb/source/API/SBValue.cpp
+++ b/lldb/source/API/SBValue.cpp
@@ -972,9 +972,9 @@ lldb::ValueObjectSP SBValue::GetSP(ValueLocker &locker) const {
   // IsValid means that the SBValue has a value in it.  But that's not the
   // only time that ValueObjects are useful.  We also want to return the value
   // if there's an error state in it.
-  if (!m_opaque_sp || (!m_opaque_sp->IsValid()
-      && (m_opaque_sp->GetRootSP()
-          && !m_opaque_sp->GetRootSP()->GetError().Fail()))) {
+  if (!m_opaque_sp || (!m_opaque_sp->IsValid() &&
+                       (m_opaque_sp->GetRootSP() &&
+                        !m_opaque_sp->GetRootSP()->GetError().Fail()))) {
     locker.GetError() = Status::FromErrorString("No value");
     return ValueObjectSP();
   }
@@ -1104,7 +1104,6 @@ lldb::SBValue SBValue::EvaluateExpression(const char *expr,
     return SBValue();
   }
 
-
   ValueLocker locker;
   lldb::ValueObjectSP value_sp(GetSP(locker));
   if (!value_sp) {
@@ -1116,7 +1115,8 @@ lldb::SBValue SBValue::EvaluateExpression(const char *expr,
     return SBValue();
   }
 
-  std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
+  TargetAPIMutex api_lock = target_sp->GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   ExecutionContext exe_ctx(target_sp.get());
 
   StackFrame *frame = exe_ctx.GetFramePtr();
diff --git a/lldb/source/API/SBWatchpoint.cpp b/lldb/source/API/SBWatchpoint.cpp
index 30528b8d34652..065c90e997088 100644
--- a/lldb/source/API/SBWatchpoint.cpp
+++ b/lldb/source/API/SBWatchpoint.cpp
@@ -108,8 +108,8 @@ addr_t SBWatchpoint::GetWatchAddress() {
 
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        watchpoint_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     ret_addr = watchpoint_sp->GetLoadAddress();
   }
 
@@ -123,8 +123,8 @@ size_t SBWatchpoint::GetWatchSize() {
 
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        watchpoint_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     watch_size = watchpoint_sp->GetByteSize();
   }
 
@@ -137,7 +137,8 @@ void SBWatchpoint::SetEnabled(bool enabled) {
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
     Target &target = watchpoint_sp->GetTarget();
-    std::lock_guard<std::recursive_mutex> guard(target.GetAPIMutex());
+    TargetAPIMutex api_lock = target.GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     ProcessSP process_sp = target.GetProcessSP();
     const bool notify = true;
     if (process_sp) {
@@ -156,8 +157,8 @@ bool SBWatchpoint::IsEnabled() {
 
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        watchpoint_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return watchpoint_sp->IsEnabled();
   } else
     return false;
@@ -169,8 +170,8 @@ uint32_t SBWatchpoint::GetHitCount() {
   uint32_t count = 0;
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        watchpoint_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     count = watchpoint_sp->GetHitCount();
   }
 
@@ -182,8 +183,8 @@ uint32_t SBWatchpoint::GetIgnoreCount() {
 
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        watchpoint_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     return watchpoint_sp->GetIgnoreCount();
   } else
     return 0;
@@ -194,8 +195,8 @@ void SBWatchpoint::SetIgnoreCount(uint32_t n) {
 
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        watchpoint_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     watchpoint_sp->SetIgnoreCount(n);
   }
 }
@@ -207,8 +208,8 @@ const char *SBWatchpoint::GetCondition() {
   if (!watchpoint_sp)
     return nullptr;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      watchpoint_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   return ConstString(watchpoint_sp->GetConditionText()).GetCString();
 }
 
@@ -217,8 +218,8 @@ void SBWatchpoint::SetCondition(const char *condition) {
 
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        watchpoint_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     watchpoint_sp->SetCondition(condition);
   }
 }
@@ -231,8 +232,8 @@ bool SBWatchpoint::GetDescription(SBStream &description,
 
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        watchpoint_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     watchpoint_sp->GetDescription(&strm, level);
     strm.EOL();
   } else
@@ -291,8 +292,8 @@ lldb::SBType SBWatchpoint::GetType() {
 
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        watchpoint_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     const CompilerType &type = watchpoint_sp->GetCompilerType();
     return lldb::SBType(type);
   }
@@ -304,8 +305,8 @@ WatchpointValueKind SBWatchpoint::GetWatchValueKind() {
 
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        watchpoint_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
     if (watchpoint_sp->IsWatchVariable())
       return WatchpointValueKind::eWatchPointValueKindVariable;
     return WatchpointValueKind::eWatchPointValueKindExpression;
@@ -320,8 +321,8 @@ const char *SBWatchpoint::GetWatchSpec() {
   if (!watchpoint_sp)
     return nullptr;
 
-  std::lock_guard<std::recursive_mutex> guard(
-      watchpoint_sp->GetTarget().GetAPIMutex());
+  TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+  std::lock_guard<TargetAPIMutex> guard(api_lock);
   // Store the result of `GetWatchSpec()` as a ConstString
   // so that the C string we return has a sufficiently long
   // lifetime. Note this a memory leak but should be fairly
@@ -333,8 +334,8 @@ bool SBWatchpoint::IsWatchingReads() {
   LLDB_INSTRUMENT_VA(this);
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        watchpoint_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
 
     return watchpoint_sp->WatchpointRead();
   }
@@ -346,8 +347,8 @@ bool SBWatchpoint::IsWatchingWrites() {
   LLDB_INSTRUMENT_VA(this);
   lldb::WatchpointSP watchpoint_sp(GetSP());
   if (watchpoint_sp) {
-    std::lock_guard<std::recursive_mutex> guard(
-        watchpoint_sp->GetTarget().GetAPIMutex());
+    TargetAPIMutex api_lock = watchpoint_sp->GetTarget().GetAPIMutex();
+    std::lock_guard<TargetAPIMutex> guard(api_lock);
 
     return watchpoint_sp->WatchpointWrite() ||
            watchpoint_sp->WatchpointModify();
diff --git a/lldb/source/Interpreter/CommandObject.cpp b/lldb/source/Interpreter/CommandObject.cpp
index 75abf49e77207..25c2af204ddb3 100644
--- a/lldb/source/Interpreter/CommandObject.cpp
+++ b/lldb/source/Interpreter/CommandObject.cpp
@@ -234,9 +234,10 @@ bool CommandObject::CheckRequirements(CommandReturnObject &result) {
     }
 
     if (flags & eCommandTryTargetAPILock) {
-      if (target && !target->IsDummyTarget())
-        m_api_locker =
-            std::unique_lock<std::recursive_mutex>(target->GetAPIMutex());
+      if (target && !target->IsDummyTarget()) {
+        m_api_mutex = target->GetAPIMutex();
+        m_api_locker = std::unique_lock<TargetAPIMutex>(m_api_mutex);
+      }
     }
   }
 
diff --git a/lldb/source/Target/CMakeLists.txt b/lldb/source/Target/CMakeLists.txt
index 90fe6fc3ad973..2874394c400aa 100644
--- a/lldb/source/Target/CMakeLists.txt
+++ b/lldb/source/Target/CMakeLists.txt
@@ -56,6 +56,7 @@ add_lldb_library(lldbTarget
   StructuredDataPlugin.cpp
   SystemRuntime.cpp
   Target.cpp
+  TargetAPIMutex.cpp
   TargetList.cpp
   Thread.cpp
   ThreadCollection.cpp
diff --git a/lldb/source/Target/ExecutionContext.cpp b/lldb/source/Target/ExecutionContext.cpp
index e4b2f07d8d8d1..31c5838844da1 100644
--- a/lldb/source/Target/ExecutionContext.cpp
+++ b/lldb/source/Target/ExecutionContext.cpp
@@ -145,8 +145,8 @@ lldb_private::GetStoppedExecutionContext(
     return llvm::createStringError(
         "StoppedExecutionContext created with a null target");
 
-  auto api_lock =
-      std::unique_lock<std::recursive_mutex>(target_sp->GetAPIMutex());
+  auto api_mutex = target_sp->GetAPIMutex();
+  std::unique_lock<TargetAPIMutex> api_locker(api_mutex);
 
   auto process_sp = exe_ctx_ref_ptr->GetProcessSP();
   if (!process_sp)
@@ -170,13 +170,15 @@ lldb_private::GetStoppedExecutionContext(
   }
 
   return StoppedExecutionContext(target_sp, process_sp, thread_sp, frame_sp,
-                                 std::move(api_lock), std::move(stop_locker));
+                                 std::move(api_mutex), std::move(api_locker),
+                                 std::move(stop_locker));
 }
 
-std::unique_lock<std::recursive_mutex> StoppedExecutionContext::AllowResume() {
+TargetAPIMutex StoppedExecutionContext::AllowResume() {
   Clear();
   m_stop_locker = ProcessRunLock::ProcessRunLocker();
-  return std::move(m_api_lock);
+  m_api_locker.release();
+  return std::move(m_api_mutex);
 }
 
 ExecutionContext::ExecutionContext(ExecutionContextScope *exe_scope_ptr)
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index 0638f29969d31..74bdee8a54a72 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -66,7 +66,6 @@
 #include "lldb/Utility/LLDBAssert.h"
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/Log.h"
-#include "lldb/Utility/Policy.h"
 #include "lldb/Utility/RealpathPrefixes.h"
 #include "lldb/Utility/State.h"
 #include "lldb/Utility/StreamString.h"
@@ -5993,12 +5992,8 @@ Target::TargetEventData::GetModuleListFromEvent(const Event *event_ptr) {
   return module_list;
 }
 
-std::recursive_mutex &Target::GetAPIMutex() {
-  Policy policy = PolicyStack::Get().Current();
-  if (policy.view == Policy::View::Private)
-    return m_private_mutex;
-
-  return m_mutex;
+TargetAPIMutex Target::GetAPIMutex() {
+  return TargetAPIMutex(shared_from_this());
 }
 
 /// Get metrics associated with this target in JSON format.
diff --git a/lldb/source/Target/TargetAPIMutex.cpp b/lldb/source/Target/TargetAPIMutex.cpp
new file mode 100644
index 0000000000000..26079c540ee2a
--- /dev/null
+++ b/lldb/source/Target/TargetAPIMutex.cpp
@@ -0,0 +1,36 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/Target/TargetAPIMutex.h"
+#include "lldb/Target/Target.h"
+#include "lldb/Utility/Policy.h"
+
+using namespace lldb_private;
+
+void TargetAPIMutex::lock() {
+  if (m_target_sp) {
+    Policy policy = PolicyStack::Get().Current();
+    std::recursive_mutex &real_mutex = policy.view == Policy::View::Private
+                                           ? m_target_sp->m_private_mutex
+                                           : m_target_sp->m_mutex;
+    m_mutex = std::shared_ptr<std::recursive_mutex>(m_target_sp, &real_mutex);
+  }
+  if (m_mutex)
+    m_mutex->lock();
+}
+
+bool TargetAPIMutex::try_lock() {
+  if (m_target_sp) {
+    Policy policy = PolicyStack::Get().Current();
+    std::recursive_mutex &real_mutex = policy.view == Policy::View::Private
+                                           ? m_target_sp->m_private_mutex
+                                           : m_target_sp->m_mutex;
+    m_mutex = std::shared_ptr<std::recursive_mutex>(m_target_sp, &real_mutex);
+  }
+  return m_mutex ? m_mutex->try_lock() : true;
+}
diff --git a/lldb/source/ValueObject/ValueObject.cpp b/lldb/source/ValueObject/ValueObject.cpp
index 49fd513c7f578..eff2fe50572b4 100644
--- a/lldb/source/ValueObject/ValueObject.cpp
+++ b/lldb/source/ValueObject/ValueObject.cpp
@@ -3840,9 +3840,10 @@ bool ValueImpl::IsValid() {
   return target_sp && target_sp->IsValid();
 }
 
-lldb::ValueObjectSP
-ValueImpl::GetSP(Process::StopLocker &stop_locker,
-                 std::unique_lock<std::recursive_mutex> &lock, Status &error) {
+lldb::ValueObjectSP ValueImpl::GetSP(Process::StopLocker &stop_locker,
+                                     TargetAPIMutex &api_mutex,
+                                     std::unique_lock<TargetAPIMutex> &lock,
+                                     Status &error) {
   if (!m_valobj_sp) {
     error = Status::FromErrorString("invalid value object");
     return m_valobj_sp;
@@ -3858,7 +3859,8 @@ ValueImpl::GetSP(Process::StopLocker &stop_locker,
   if (!target)
     return ValueObjectSP();
 
-  lock = std::unique_lock<std::recursive_mutex>(target->GetAPIMutex());
+  api_mutex = target->GetAPIMutex();
+  lock = std::unique_lock<TargetAPIMutex>(api_mutex);
 
   ProcessSP process_sp(value_sp->GetProcessSP());
   if (process_sp && !stop_locker.TryLock(&process_sp->GetRunLock())) {
diff --git a/lldb/tools/lldb-dap/ProtocolUtils.cpp b/lldb/tools/lldb-dap/ProtocolUtils.cpp
index fb27859c5726f..d7d583a318d3a 100644
--- a/lldb/tools/lldb-dap/ProtocolUtils.cpp
+++ b/lldb/tools/lldb-dap/ProtocolUtils.cpp
@@ -21,6 +21,7 @@
 #include "lldb/Host/PosixApi.h" // Adds PATH_MAX for windows
 
 #include <iomanip>
+#include <mutex>
 #include <optional>
 #include <sstream>
 
diff --git a/lldb/unittests/Target/CMakeLists.txt b/lldb/unittests/Target/CMakeLists.txt
index bf08a8f015ba0..c3ac21eb217b8 100644
--- a/lldb/unittests/Target/CMakeLists.txt
+++ b/lldb/unittests/Target/CMakeLists.txt
@@ -14,6 +14,7 @@ add_lldb_unittest(TargetTests
   ScratchTypeSystemTest.cpp
   StackFrameRecognizerTest.cpp
   SummaryStatisticsTest.cpp
+  TargetAPIMutexTest.cpp
   FindFileTest.cpp
 
   LINK_COMPONENTS
diff --git a/lldb/unittests/Target/TargetAPIMutexTest.cpp b/lldb/unittests/Target/TargetAPIMutexTest.cpp
new file mode 100644
index 0000000000000..2650723ec6f4e
--- /dev/null
+++ b/lldb/unittests/Target/TargetAPIMutexTest.cpp
@@ -0,0 +1,195 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/Target/TargetAPIMutex.h"
+#include "Plugins/Platform/Linux/PlatformLinux.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Target/Platform.h"
+#include "lldb/Target/Target.h"
+#include "lldb/Utility/ArchSpec.h"
+#include "gtest/gtest.h"
+
+#include <thread>
+
+using namespace lldb_private;
+using namespace lldb;
+
+TEST(TargetAPIMutexTest, DefaultConstructedIsANoOp) {
+  // No synchronization primitive is touched at all in this state, so
+  // there is no pairing requirement: try_lock() always succeeds, and
+  // lock()/unlock() are callable with no invariant to violate.
+  TargetAPIMutex lock;
+  EXPECT_TRUE(lock.try_lock());
+  lock.lock();
+  lock.unlock();
+  lock.lock();
+  lock.unlock();
+}
+
+namespace {
+class TargetAPIMutexTargetTest : public ::testing::Test {
+public:
+  void SetUp() override {
+    FileSystem::Initialize();
+    HostInfo::Initialize();
+    platform_linux::PlatformLinux::Initialize();
+  }
+  void TearDown() override {
+    platform_linux::PlatformLinux::Terminate();
+    HostInfo::Terminate();
+    FileSystem::Terminate();
+  }
+};
+
+TargetSP CreateTarget() {
+  ArchSpec arch("x86_64-pc-linux");
+  Platform::SetHostPlatform(
+      platform_linux::PlatformLinux::CreateInstance(true, &arch));
+
+  DebuggerSP debugger_sp = Debugger::CreateInstance();
+  TargetSP target_sp;
+  PlatformSP platform_sp;
+  Status error = debugger_sp->GetTargetList().CreateTarget(
+      *debugger_sp, "", arch, eLoadDependentsNo, platform_sp, target_sp);
+  return target_sp;
+}
+} // namespace
+
+TEST_F(TargetAPIMutexTargetTest, WrapsTheTargetMutex) {
+  TargetSP target_sp = CreateTarget();
+  ASSERT_TRUE(target_sp);
+
+  TargetAPIMutex lock(target_sp);
+  lock.lock();
+
+  // Recursive reentrancy is delegated straight to the underlying
+  // std::recursive_mutex: a second handle over the same target, locked
+  // from the same thread, must not block.
+  TargetAPIMutex second_lock(target_sp);
+  EXPECT_TRUE(second_lock.try_lock());
+  second_lock.unlock();
+
+  lock.unlock();
+
+  std::thread t([target_sp]() {
+    TargetAPIMutex background_lock(target_sp);
+    EXPECT_TRUE(background_lock.try_lock());
+    background_lock.unlock();
+  });
+  t.join();
+}
+
+TEST_F(TargetAPIMutexTargetTest, RealMutexBlocksOtherThreads) {
+  TargetSP target_sp = CreateTarget();
+  ASSERT_TRUE(target_sp);
+
+  TargetAPIMutex lock(target_sp);
+  lock.lock();
+
+  std::thread t([target_sp]() {
+    TargetAPIMutex background_lock(target_sp);
+    EXPECT_FALSE(background_lock.try_lock());
+  });
+  t.join();
+
+  lock.unlock();
+}
+
+TEST_F(TargetAPIMutexTargetTest, BareHandleDoesNotAutoUnlockOnDestruction) {
+  // TargetAPIMutex carries no RAII of its own -- exactly like
+  // std::recursive_mutex, a bare handle going out of scope without an
+  // explicit unlock() leaves the real mutex held. Callers that want
+  // scope-based release must wrap it in std::lock_guard/std::unique_lock.
+  TargetSP target_sp = CreateTarget();
+  ASSERT_TRUE(target_sp);
+
+  {
+    TargetAPIMutex lock(target_sp);
+    lock.lock();
+  }
+
+  std::thread t([target_sp]() {
+    TargetAPIMutex background_lock(target_sp);
+    EXPECT_FALSE(background_lock.try_lock());
+  });
+  t.join();
+}
+
+TEST_F(TargetAPIMutexTargetTest, LockGuardReleasesOnScopeExit) {
+  TargetSP target_sp = CreateTarget();
+  ASSERT_TRUE(target_sp);
+
+  {
+    TargetAPIMutex lock(target_sp);
+    std::lock_guard<TargetAPIMutex> guard(lock);
+  }
+
+  std::thread t([target_sp]() {
+    TargetAPIMutex background_lock(target_sp);
+    EXPECT_TRUE(background_lock.try_lock());
+    background_lock.unlock();
+  });
+  t.join();
+}
+
+TEST_F(TargetAPIMutexTargetTest, MoveTransfersOwnership) {
+  TargetSP target_sp = CreateTarget();
+  ASSERT_TRUE(target_sp);
+
+  TargetAPIMutex lock(target_sp);
+  lock.lock();
+
+  TargetAPIMutex moved(std::move(lock));
+
+  // The moved-from handle no longer references the real mutex: unlocking
+  // it is a no-op, so the mutex stays held until `moved` releases it.
+  lock.unlock();
+  std::thread contended([target_sp]() {
+    TargetAPIMutex background_lock(target_sp);
+    EXPECT_FALSE(background_lock.try_lock());
+  });
+  contended.join();
+
+  moved.unlock();
+  std::thread released([target_sp]() {
+    TargetAPIMutex background_lock(target_sp);
+    EXPECT_TRUE(background_lock.try_lock());
+    background_lock.unlock();
+  });
+  released.join();
+}
+
+TEST_F(TargetAPIMutexTargetTest, ResolvesFreshOnEachLockCall) {
+  // lock()/try_lock() re-resolve the real mutex on every call rather
+  // than caching a single resolution for the handle's lifetime: a
+  // handle can be locked, unlocked, and locked again, each time
+  // correctly contending with other threads for the same target mutex.
+  TargetSP target_sp = CreateTarget();
+  ASSERT_TRUE(target_sp);
+
+  TargetAPIMutex lock(target_sp);
+  lock.lock();
+  lock.unlock();
+
+  std::thread t([target_sp]() {
+    TargetAPIMutex background_lock(target_sp);
+    background_lock.lock();
+    background_lock.unlock();
+  });
+  t.join();
+
+  lock.lock();
+  std::thread contended([target_sp]() {
+    TargetAPIMutex background_lock(target_sp);
+    EXPECT_FALSE(background_lock.try_lock());
+  });
+  contended.join();
+  lock.unlock();
+}

>From 127a851733bd1dd8e9161a8ec21547d06779aa4f Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Tue, 18 Aug 2026 22:29:07 +0100
Subject: [PATCH 13/16] [lldb] Fix use-after-free of a scripted frame's
 register info (#216840)

`ScriptedFrame::CreateRegisterContext` built the frame's
`DynamicRegisterInfo` in a local `shared_ptr` and passed it to
`RegisterContextMemory`, which kept a `DynamicRegisterInfo` reference
and did not own it. That function was the sole owner, so the register
info died as soon as it returned, leaving the register context it had
just produced with a dangling reference.

The frame survives creation because `SetAllRegisterData` runs while the
register info is still alive. It crashes later, on the first register
lookup driven by a DWARF location expression, for instance when a
backtrace formats the frame's arguments.

This patch makes `RegisterContextMemory` own its register info: it takes
a `shared_ptr` and keeps it for its own lifetime, so a register context
is self-contained and nothing else has to stay alive on its behalf.
`ScriptedThread` and `OperatingSystemPython` both kept the register info
in a member to back the reference they handed out, so both members are
gone.

Previously, `CreateRegisterContext` was static and ran before the frame
was created. Now the constructor calls it and stores the result, and
`GetRegisterContext` just hands that back.

This patch also improves error reporting: the register context creation
propagates llvm::Error instead of logging at each step and returning
null. `GetDynamicRegisterInfo` and `CreateRegisterContext` both return
`Expected`, and the constructor reports what actually went wrong with
`Debugger::ReportError` rather than leaving it buried in the thread log.

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
(cherry picked from commit b0e9c530b5f0206de4a4f59f6728dbd5f10a5ca2)
---
 .../Python/OperatingSystemPython.cpp          |  50 +++----
 .../Python/OperatingSystemPython.h            |   3 +-
 .../Process/Utility/RegisterContextMemory.cpp |  23 ++--
 .../Process/Utility/RegisterContextMemory.h   |   4 +-
 .../Process/scripted/ScriptedFrame.cpp        | 126 +++++++-----------
 .../Plugins/Process/scripted/ScriptedFrame.h  |   8 +-
 .../Process/scripted/ScriptedThread.cpp       |  32 ++---
 .../Plugins/Process/scripted/ScriptedThread.h |   3 +-
 .../wrapped_frame_register_context/Makefile   |   4 +
 ...rameProviderWrappedFrameRegisterContext.py |  35 +++++
 .../frame_provider.py                         |  83 ++++++++++++
 .../wrapped_frame_register_context/main.c     |   6 +
 12 files changed, 237 insertions(+), 140 deletions(-)
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/Makefile
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/TestFrameProviderWrappedFrameRegisterContext.py
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/frame_provider.py
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/main.c

diff --git a/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp b/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
index 354e7ea30059e..09403e387aabd 100644
--- a/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
+++ b/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
@@ -74,7 +74,7 @@ llvm::StringRef OperatingSystemPython::GetPluginDescriptionStatic() {
 
 OperatingSystemPython::OperatingSystemPython(lldb_private::Process *process,
                                              const FileSpec &python_module_path)
-    : OperatingSystem(process), m_thread_list_valobj_sp(), m_register_info_up(),
+    : OperatingSystem(process), m_thread_list_valobj_sp(),
       m_interpreter(nullptr), m_script_object_sp() {
   if (!process)
     return;
@@ -142,29 +142,29 @@ OperatingSystemPython::OperatingSystemPython(lldb_private::Process *process,
 
 OperatingSystemPython::~OperatingSystemPython() = default;
 
-DynamicRegisterInfo *OperatingSystemPython::GetDynamicRegisterInfo() {
-  if (m_register_info_up == nullptr) {
-    if (!m_interpreter || !m_operating_system_interface_sp)
-      return nullptr;
-    Log *log = GetLog(LLDBLog::OS);
+DynamicRegisterInfoSP OperatingSystemPython::GetDynamicRegisterInfo() {
+  if (!m_interpreter || !m_operating_system_interface_sp)
+    return nullptr;
 
-    LLDB_LOGF(log,
-              "OperatingSystemPython::GetDynamicRegisterInfo() fetching "
-              "thread register definitions from python for pid %" PRIu64,
-              m_process->GetID());
-
-    StructuredData::DictionarySP dictionary =
-        m_operating_system_interface_sp->GetRegisterInfo();
-    if (!dictionary)
-      return nullptr;
-
-    m_register_info_up = DynamicRegisterInfo::Create(
-        *dictionary, m_process->GetTarget().GetArchitecture());
-    assert(m_register_info_up);
-    assert(m_register_info_up->GetNumRegisters() > 0);
-    assert(m_register_info_up->GetNumRegisterSets() > 0);
-  }
-  return m_register_info_up.get();
+  Log *log = GetLog(LLDBLog::OS);
+
+  LLDB_LOGF(log,
+            "OperatingSystemPython::GetDynamicRegisterInfo() fetching "
+            "thread register definitions from python for pid %" PRIu64,
+            m_process->GetID());
+
+  StructuredData::DictionarySP dictionary =
+      m_operating_system_interface_sp->GetRegisterInfo();
+  if (!dictionary)
+    return nullptr;
+
+  DynamicRegisterInfoSP register_info_sp = DynamicRegisterInfo::Create(
+      *dictionary, m_process->GetTarget().GetArchitecture());
+  assert(register_info_sp);
+  assert(register_info_sp->GetNumRegisters() > 0);
+  assert(register_info_sp->GetNumRegisterSets() > 0);
+
+  return register_info_sp;
 }
 
 bool OperatingSystemPython::UpdateThreadList(ThreadList &old_thread_list,
@@ -312,7 +312,7 @@ OperatingSystemPython::CreateRegisterContextForThread(Thread *thread,
               ") creating memory register context",
               thread->GetID(), thread->GetProtocolID(), reg_data_addr);
     reg_ctx_sp = std::make_shared<RegisterContextMemory>(
-        *thread, 0, *GetDynamicRegisterInfo(), reg_data_addr);
+        *thread, 0, GetDynamicRegisterInfo(), reg_data_addr);
   } else {
     // No register data address is provided, query the python plug-in to let it
     // make up the data as it sees fit
@@ -330,7 +330,7 @@ OperatingSystemPython::CreateRegisterContextForThread(Thread *thread,
       DataBufferSP data_sp(new DataBufferHeap(value.c_str(), value.length()));
       if (data_sp->GetByteSize()) {
         RegisterContextMemory *reg_ctx_memory = new RegisterContextMemory(
-            *thread, 0, *GetDynamicRegisterInfo(), LLDB_INVALID_ADDRESS);
+            *thread, 0, GetDynamicRegisterInfo(), LLDB_INVALID_ADDRESS);
         if (reg_ctx_memory) {
           reg_ctx_sp.reset(reg_ctx_memory);
           reg_ctx_memory->SetAllRegisterData(data_sp);
diff --git a/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.h b/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.h
index 1e4517dbf7800..b7524a6f9e548 100644
--- a/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.h
+++ b/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.h
@@ -73,10 +73,9 @@ class OperatingSystemPython : public lldb_private::OperatingSystem {
       lldb_private::ThreadList &old_thread_list,
       std::vector<bool> &core_used_map, bool *did_create_ptr);
 
-  lldb_private::DynamicRegisterInfo *GetDynamicRegisterInfo();
+  lldb::DynamicRegisterInfoSP GetDynamicRegisterInfo();
 
   lldb::ValueObjectSP m_thread_list_valobj_sp;
-  std::unique_ptr<lldb_private::DynamicRegisterInfo> m_register_info_up;
   lldb_private::ScriptInterpreter *m_interpreter = nullptr;
   lldb::OperatingSystemInterfaceSP m_operating_system_interface_sp = nullptr;
   lldb_private::StructuredData::GenericSP m_script_object_sp = nullptr;
diff --git a/lldb/source/Plugins/Process/Utility/RegisterContextMemory.cpp b/lldb/source/Plugins/Process/Utility/RegisterContextMemory.cpp
index 84a19d5b13035..0017cd7319ad7 100644
--- a/lldb/source/Plugins/Process/Utility/RegisterContextMemory.cpp
+++ b/lldb/source/Plugins/Process/Utility/RegisterContextMemory.cpp
@@ -20,20 +20,21 @@ using namespace lldb_private;
 // RegisterContextMemory constructor
 RegisterContextMemory::RegisterContextMemory(Thread &thread,
                                              uint32_t concrete_frame_idx,
-                                             DynamicRegisterInfo &reg_infos,
+                                             DynamicRegisterInfoSP reg_infos_sp,
                                              addr_t reg_data_addr)
-    : RegisterContext(thread, concrete_frame_idx), m_reg_infos(reg_infos),
-      m_reg_valid(), m_reg_data(), m_reg_data_addr(reg_data_addr) {
+    : RegisterContext(thread, concrete_frame_idx),
+      m_reg_infos_sp(std::move(reg_infos_sp)), m_reg_valid(), m_reg_data(),
+      m_reg_data_addr(reg_data_addr) {
   // Resize our vector of bools to contain one bool for every register. We will
   // use these boolean values to know when a register value is valid in
   // m_reg_data.
-  const size_t num_regs = reg_infos.GetNumRegisters();
+  const size_t num_regs = m_reg_infos_sp->GetNumRegisters();
   assert(num_regs > 0);
   m_reg_valid.resize(num_regs);
 
   // Make a heap based buffer that is big enough to store all registers
-  m_data =
-      std::make_shared<DataBufferHeap>(reg_infos.GetRegisterDataByteSize(), 0);
+  m_data = std::make_shared<DataBufferHeap>(
+      m_reg_infos_sp->GetRegisterDataByteSize(), 0);
   m_reg_data.SetData(m_data);
 }
 
@@ -52,24 +53,24 @@ void RegisterContextMemory::SetAllRegisterValid(bool b) {
 }
 
 size_t RegisterContextMemory::GetRegisterCount() {
-  return m_reg_infos.GetNumRegisters();
+  return m_reg_infos_sp->GetNumRegisters();
 }
 
 const RegisterInfo *RegisterContextMemory::GetRegisterInfoAtIndex(size_t reg) {
-  return m_reg_infos.GetRegisterInfoAtIndex(reg);
+  return m_reg_infos_sp->GetRegisterInfoAtIndex(reg);
 }
 
 size_t RegisterContextMemory::GetRegisterSetCount() {
-  return m_reg_infos.GetNumRegisterSets();
+  return m_reg_infos_sp->GetNumRegisterSets();
 }
 
 const RegisterSet *RegisterContextMemory::GetRegisterSet(size_t reg_set) {
-  return m_reg_infos.GetRegisterSet(reg_set);
+  return m_reg_infos_sp->GetRegisterSet(reg_set);
 }
 
 uint32_t RegisterContextMemory::ConvertRegisterKindToRegisterNumber(
     lldb::RegisterKind kind, uint32_t num) {
-  return m_reg_infos.ConvertRegisterKindToRegisterNumber(kind, num);
+  return m_reg_infos_sp->ConvertRegisterKindToRegisterNumber(kind, num);
 }
 
 bool RegisterContextMemory::ReadRegister(const RegisterInfo *reg_info,
diff --git a/lldb/source/Plugins/Process/Utility/RegisterContextMemory.h b/lldb/source/Plugins/Process/Utility/RegisterContextMemory.h
index 2aad99ec9b210..5ed3e03f273d4 100644
--- a/lldb/source/Plugins/Process/Utility/RegisterContextMemory.h
+++ b/lldb/source/Plugins/Process/Utility/RegisterContextMemory.h
@@ -20,7 +20,7 @@ class RegisterContextMemory : public lldb_private::RegisterContext {
 public:
   RegisterContextMemory(lldb_private::Thread &thread,
                         uint32_t concrete_frame_idx,
-                        lldb_private::DynamicRegisterInfo &reg_info,
+                        lldb::DynamicRegisterInfoSP reg_info_sp,
                         lldb::addr_t reg_data_addr);
 
   ~RegisterContextMemory() override;
@@ -59,7 +59,7 @@ class RegisterContextMemory : public lldb_private::RegisterContext {
 protected:
   void SetAllRegisterValid(bool b);
 
-  lldb_private::DynamicRegisterInfo &m_reg_infos;
+  lldb::DynamicRegisterInfoSP m_reg_infos_sp;
   std::vector<bool> m_reg_valid;
   lldb::WritableDataBufferSP m_data;
   lldb_private::DataExtractor m_reg_data;
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
index d6288b6eadb1c..cac9f0e44bb73 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
@@ -116,16 +116,8 @@ ScriptedFrame::Create(ThreadSP thread_sp,
   if (maybe_sym_ctx)
     sc = *maybe_sym_ctx;
 
-  lldb::RegisterContextSP reg_ctx_sp;
-  auto regs_or_err =
-      CreateRegisterContext(*scripted_frame_interface, *thread_sp, frame_id);
-  if (!regs_or_err)
-    LLDB_LOG_ERROR(GetLog(LLDBLog::Thread), regs_or_err.takeError(), "{0}");
-  else
-    reg_ctx_sp = *regs_or_err;
-
   return std::make_shared<ScriptedFrame>(thread_sp, scripted_frame_interface,
-                                         frame_id, pc, sc, reg_ctx_sp,
+                                         frame_id, pc, sc,
                                          owned_script_object_sp);
 }
 
@@ -133,16 +125,29 @@ ScriptedFrame::ScriptedFrame(ThreadSP thread_sp,
                              ScriptedFrameInterfaceSP interface_sp,
                              lldb::user_id_t id, lldb::addr_t pc,
                              SymbolContext &sym_ctx,
-                             lldb::RegisterContextSP reg_ctx_sp,
                              StructuredData::GenericSP script_object_sp)
     : StackFrame(thread_sp, /*frame_idx=*/id,
-                 /*concrete_frame_idx=*/id, /*reg_context_sp=*/reg_ctx_sp,
+                 /*concrete_frame_idx=*/id, /*reg_context_sp=*/nullptr,
                  /*cfa=*/0, /*pc=*/pc,
                  /*behaves_like_zeroth_frame=*/!id, /*symbol_ctx=*/&sym_ctx),
       m_scripted_frame_interface_sp(interface_sp),
       m_script_object_sp(script_object_sp) {
   // FIXME: This should be part of the base class constructor.
   m_stack_frame_kind = StackFrame::Kind::Synthetic;
+
+  llvm::Expected<lldb::RegisterContextSP> reg_ctx_or_err =
+      CreateRegisterContext();
+  if (!reg_ctx_or_err) {
+    std::optional<lldb::user_id_t> debugger_id;
+    if (ProcessSP process_sp = thread_sp->GetProcess())
+      debugger_id = process_sp->GetTarget().GetDebugger().GetID();
+    Debugger::ReportError("failed to create scripted frame register context: " +
+                              llvm::toString(reg_ctx_or_err.takeError()),
+                          debugger_id);
+    return;
+  }
+
+  m_reg_context_sp = *reg_ctx_or_err;
 }
 
 ScriptedFrame::~ScriptedFrame() {}
@@ -176,57 +181,45 @@ lldb::ScriptedFrameInterfaceSP ScriptedFrame::GetInterface() const {
   return m_scripted_frame_interface_sp;
 }
 
-std::shared_ptr<DynamicRegisterInfo> ScriptedFrame::GetDynamicRegisterInfo() {
+llvm::Expected<DynamicRegisterInfoSP> ScriptedFrame::GetDynamicRegisterInfo() {
   CheckInterpreterAndScriptObject();
 
   StructuredData::DictionarySP reg_info = GetInterface()->GetRegisterInfo();
-
-  Status error;
   if (!reg_info)
-    return ScriptedInterface::ErrorWithMessage<
-        std::shared_ptr<DynamicRegisterInfo>>(
-        LLVM_PRETTY_FUNCTION, "failed to get scripted frame registers info",
-        error, LLDBLog::Thread);
+    return llvm::createStringError(
+        "failed to get scripted frame registers info");
 
   ThreadSP thread_sp = m_thread_wp.lock();
   if (!thread_sp || !thread_sp->IsValid())
-    return ScriptedInterface::ErrorWithMessage<
-        std::shared_ptr<DynamicRegisterInfo>>(
-        LLVM_PRETTY_FUNCTION,
-        "failed to get scripted frame registers info: invalid thread", error,
-        LLDBLog::Thread);
+    return llvm::createStringError("invalid thread");
 
   ProcessSP process_sp = thread_sp->GetProcess();
   if (!process_sp || !process_sp->IsValid())
-    return ScriptedInterface::ErrorWithMessage<
-        std::shared_ptr<DynamicRegisterInfo>>(
-        LLVM_PRETTY_FUNCTION,
-        "failed to get scripted frame registers info: invalid process", error,
-        LLDBLog::Thread);
-
-  return DynamicRegisterInfo::Create(*reg_info,
-                                     process_sp->GetTarget().GetArchitecture());
-}
-
-llvm::Expected<lldb::RegisterContextSP>
-ScriptedFrame::CreateRegisterContext(ScriptedFrameInterface &interface,
-                                     Thread &thread, lldb::user_id_t frame_id) {
-  StructuredData::DictionarySP reg_info = interface.GetRegisterInfo();
+    return llvm::createStringError("invalid process");
 
-  if (!reg_info)
+  DynamicRegisterInfoSP register_info_sp = DynamicRegisterInfo::Create(
+      *reg_info, process_sp->GetTarget().GetArchitecture());
+  if (!register_info_sp)
     return llvm::createStringError(
-        "failed to get scripted frame registers info");
+        "failed to create scripted frame registers info");
+
+  return register_info_sp;
+}
 
-  std::shared_ptr<DynamicRegisterInfo> register_info_sp =
-      DynamicRegisterInfo::Create(
-          *reg_info, thread.GetProcess()->GetTarget().GetArchitecture());
+llvm::Expected<lldb::RegisterContextSP> ScriptedFrame::CreateRegisterContext() {
+  if (!m_scripted_frame_interface_sp)
+    return llvm::createStringError("invalid scripted frame interface");
 
-  lldb::RegisterContextSP reg_ctx_sp;
+  ThreadSP thread_sp = GetThread();
+  if (!thread_sp)
+    return llvm::createStringError("invalid thread");
 
-  std::optional<std::string> reg_data = interface.GetRegisterContext();
+  // A frame that reports no register data has no register context. That is a
+  // valid state, not a failure: only frames that expose registers implement it.
+  std::optional<std::string> reg_data =
+      m_scripted_frame_interface_sp->GetRegisterContext();
   if (!reg_data)
-    return llvm::createStringError(
-        "failed to get scripted frame registers data");
+    return lldb::RegisterContextSP();
 
   DataBufferSP data_sp(
       std::make_shared<DataBufferHeap>(reg_data->c_str(), reg_data->size()));
@@ -234,45 +227,22 @@ ScriptedFrame::CreateRegisterContext(ScriptedFrameInterface &interface,
   if (!data_sp->GetByteSize())
     return llvm::createStringError("failed to copy raw registers data");
 
+  llvm::Expected<DynamicRegisterInfoSP> register_info_or_err =
+      GetDynamicRegisterInfo();
+  if (!register_info_or_err)
+    return register_info_or_err.takeError();
+
   std::shared_ptr<RegisterContextMemory> reg_ctx_memory =
-      std::make_shared<RegisterContextMemory>(
-          thread, frame_id, *register_info_sp, LLDB_INVALID_ADDRESS);
+      std::make_shared<RegisterContextMemory>(*thread_sp, GetFrameIndex(),
+                                              std::move(*register_info_or_err),
+                                              LLDB_INVALID_ADDRESS);
 
   reg_ctx_memory->SetAllRegisterData(data_sp);
-  reg_ctx_sp = reg_ctx_memory;
 
-  return reg_ctx_sp;
+  return reg_ctx_memory;
 }
 
 lldb::RegisterContextSP ScriptedFrame::GetRegisterContext() {
-  if (!m_reg_context_sp) {
-    Status error;
-    if (!m_scripted_frame_interface_sp)
-      return ScriptedInterface::ErrorWithMessage<RegisterContextSP>(
-          LLVM_PRETTY_FUNCTION,
-          "failed to get scripted frame registers context: invalid interface",
-          error, LLDBLog::Thread);
-
-    ThreadSP thread_sp = GetThread();
-    if (!thread_sp)
-      return ScriptedInterface::ErrorWithMessage<RegisterContextSP>(
-          LLVM_PRETTY_FUNCTION,
-          "failed to get scripted frame registers context: invalid thread",
-          error, LLDBLog::Thread);
-
-    auto regs_or_err = CreateRegisterContext(*m_scripted_frame_interface_sp,
-                                             *thread_sp, GetFrameIndex());
-    if (!regs_or_err) {
-      error = Status::FromError(regs_or_err.takeError());
-      return ScriptedInterface::ErrorWithMessage<RegisterContextSP>(
-          LLVM_PRETTY_FUNCTION,
-          "failed to get scripted frame registers context", error,
-          LLDBLog::Thread);
-    }
-
-    m_reg_context_sp = *regs_or_err;
-  }
-
   return m_reg_context_sp;
 }
 
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedFrame.h b/lldb/source/Plugins/Process/scripted/ScriptedFrame.h
index 9afe88abd14f2..679d81bc46bdc 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedFrame.h
+++ b/lldb/source/Plugins/Process/scripted/ScriptedFrame.h
@@ -25,7 +25,7 @@ class ScriptedFrame : public lldb_private::StackFrame {
   ScriptedFrame(lldb::ThreadSP thread_sp,
                 lldb::ScriptedFrameInterfaceSP interface_sp,
                 lldb::user_id_t frame_idx, lldb::addr_t pc,
-                SymbolContext &sym_ctx, lldb::RegisterContextSP reg_ctx_sp,
+                SymbolContext &sym_ctx,
                 StructuredData::GenericSP script_object_sp = nullptr);
 
   ~ScriptedFrame() override;
@@ -90,9 +90,7 @@ class ScriptedFrame : public lldb_private::StackFrame {
 private:
   void CheckInterpreterAndScriptObject() const;
   lldb::ScriptedFrameInterfaceSP GetInterface() const;
-  static llvm::Expected<lldb::RegisterContextSP>
-  CreateRegisterContext(ScriptedFrameInterface &interface, Thread &thread,
-                        lldb::user_id_t frame_id);
+  llvm::Expected<lldb::RegisterContextSP> CreateRegisterContext();
 
   // Populate m_variable_list_sp from the scripted frame interface. The boolean
   // controls if we should try to fabricate Variable objects for each of the
@@ -104,7 +102,7 @@ class ScriptedFrame : public lldb_private::StackFrame {
   ScriptedFrame(const ScriptedFrame &) = delete;
   const ScriptedFrame &operator=(const ScriptedFrame &) = delete;
 
-  std::shared_ptr<DynamicRegisterInfo> GetDynamicRegisterInfo();
+  llvm::Expected<lldb::DynamicRegisterInfoSP> GetDynamicRegisterInfo();
 
   lldb::ScriptedFrameInterfaceSP m_scripted_frame_interface_sp;
   lldb_private::StructuredData::GenericSP m_script_object_sp;
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
index 9ba9beaff66d1..fcadde0b1ff64 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
@@ -146,9 +146,16 @@ ScriptedThread::CreateRegisterContextForFrame(StackFrame *frame) {
         LLVM_PRETTY_FUNCTION, "Failed to copy raw registers data.", error,
         LLDBLog::Thread);
 
+  DynamicRegisterInfoSP register_info_sp = GetDynamicRegisterInfo();
+  if (!register_info_sp)
+    return ScriptedInterface::ErrorWithMessage<lldb::RegisterContextSP>(
+        LLVM_PRETTY_FUNCTION,
+        "Failed to create scripted thread registers info.", error,
+        LLDBLog::Thread);
+
   std::shared_ptr<RegisterContextMemory> reg_ctx_memory =
       std::make_shared<RegisterContextMemory>(
-          *this, 0, *GetDynamicRegisterInfo(), LLDB_INVALID_ADDRESS);
+          *this, 0, std::move(register_info_sp), LLDB_INVALID_ADDRESS);
   if (!reg_ctx_memory)
     return ScriptedInterface::ErrorWithMessage<lldb::RegisterContextSP>(
         LLVM_PRETTY_FUNCTION, "Failed to create a register context.", error,
@@ -435,24 +442,19 @@ lldb::ScriptedThreadInterfaceSP ScriptedThread::GetInterface() const {
   return m_scripted_thread_interface_sp;
 }
 
-std::shared_ptr<DynamicRegisterInfo> ScriptedThread::GetDynamicRegisterInfo() {
+DynamicRegisterInfoSP ScriptedThread::GetDynamicRegisterInfo() {
   CheckInterpreterAndScriptObject();
 
-  if (!m_register_info_sp) {
-    StructuredData::DictionarySP reg_info = GetInterface()->GetRegisterInfo();
-
-    Status error;
-    if (!reg_info)
-      return ScriptedInterface::ErrorWithMessage<
-          std::shared_ptr<DynamicRegisterInfo>>(
-          LLVM_PRETTY_FUNCTION, "Failed to get scripted thread registers info.",
-          error, LLDBLog::Thread);
+  StructuredData::DictionarySP reg_info = GetInterface()->GetRegisterInfo();
 
-    m_register_info_sp = DynamicRegisterInfo::Create(
-        *reg_info, m_scripted_process.GetTarget().GetArchitecture());
-  }
+  Status error;
+  if (!reg_info)
+    return ScriptedInterface::ErrorWithMessage<DynamicRegisterInfoSP>(
+        LLVM_PRETTY_FUNCTION, "Failed to get scripted thread registers info.",
+        error, LLDBLog::Thread);
 
-  return m_register_info_sp;
+  return DynamicRegisterInfo::Create(
+      *reg_info, m_scripted_process.GetTarget().GetArchitecture());
 }
 
 StructuredData::ObjectSP ScriptedThread::FetchThreadExtendedInfo() {
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedThread.h b/lldb/source/Plugins/Process/scripted/ScriptedThread.h
index d5a97fd38146f..c5e977a46bdc5 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedThread.h
+++ b/lldb/source/Plugins/Process/scripted/ScriptedThread.h
@@ -70,12 +70,11 @@ class ScriptedThread : public lldb_private::Thread {
   ScriptedThread(const ScriptedThread &) = delete;
   const ScriptedThread &operator=(const ScriptedThread &) = delete;
 
-  std::shared_ptr<DynamicRegisterInfo> GetDynamicRegisterInfo();
+  lldb::DynamicRegisterInfoSP GetDynamicRegisterInfo();
 
   const ScriptedProcess &m_scripted_process;
   lldb::ScriptedThreadInterfaceSP m_scripted_thread_interface_sp = nullptr;
   lldb_private::StructuredData::GenericSP m_script_object_sp = nullptr;
-  std::shared_ptr<DynamicRegisterInfo> m_register_info_sp = nullptr;
 };
 
 } // namespace lldb_private
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/Makefile b/lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/Makefile
new file mode 100644
index 0000000000000..695335e068c0c
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/Makefile
@@ -0,0 +1,4 @@
+C_SOURCES := main.c
+CFLAGS_EXTRAS := -std=c99
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/TestFrameProviderWrappedFrameRegisterContext.py b/lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/TestFrameProviderWrappedFrameRegisterContext.py
new file mode 100644
index 0000000000000..f2f31e8e1b0fd
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/TestFrameProviderWrappedFrameRegisterContext.py
@@ -0,0 +1,35 @@
+"""
+Test that a scripted frame reporting a register context can format its frame
+without crashing.
+"""
+
+import os
+
+import lldb
+import lldbsuite.test.lldbutil as lldbutil
+from lldbsuite.test.lldbtest import *
+
+
+class TestFrameProviderWrappedFrameRegisterContext(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def test_backtrace_with_forwarded_register_context(self):
+        self.build()
+        target, process, _, _ = lldbutil.run_to_source_breakpoint(
+            self, "Break here", lldb.SBFileSpec("main.c")
+        )
+
+        self.runCmd(
+            "command script import "
+            + os.path.join(self.getSourceDir(), "frame_provider.py")
+        )
+        error = lldb.SBError()
+        target.RegisterScriptedFrameProvider(
+            "frame_provider.WrapVariablesProvider", lldb.SBStructuredData(), error
+        )
+        self.assertSuccess(error, "Failed to register the frame provider")
+
+        # Formatting the arguments evaluates their DWARF location expressions,
+        # which reads registers through the scripted frame's register context.
+        # Checking the values, not just for a crash, proves the lookup worked.
+        self.expect("bt", substrs=["compute(a=3, b=4)", "main"])
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/frame_provider.py b/lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/frame_provider.py
new file mode 100644
index 0000000000000..94be1b23d2552
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/frame_provider.py
@@ -0,0 +1,83 @@
+"""
+Frame provider whose scripted frame replaces a native frame and forwards that
+frame's registers through get_register_context.
+
+Reporting a register context is what makes LLDB evaluate the frame's DWARF
+location expressions when it formats variables.
+"""
+
+import struct
+
+import lldb
+from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
+from lldb.plugins.scripted_process import ScriptedFrame
+
+WRAPPED_FUNCTION = "compute"
+
+
+class WrappedFrame(ScriptedFrame):
+    def __init__(self, thread, frame, idx):
+        super().__init__(thread, lldb.SBStructuredData())
+        self._frame = frame
+        self._idx = idx
+
+    def get_id(self):
+        return self._idx
+
+    def get_pc(self):
+        return self._frame.GetPC()
+
+    def get_symbol_context(self):
+        return self._frame.GetSymbolContext(lldb.eSymbolContextEverything)
+
+    def get_function_name(self):
+        return self._frame.GetFunctionName() or "<wrapped>"
+
+    def is_artificial(self):
+        return False
+
+    def is_hidden(self):
+        return False
+
+    def get_register_context(self):
+        """Forward the wrapped frame's GPRs, packed in register_info order."""
+        regs = {}
+        for reg_set in self._frame.registers:
+            if "general purpose" in reg_set.name.lower():
+                for reg in reg_set:
+                    regs[reg.name] = int(reg.value, 16) if reg.value else 0
+                break
+        if not regs:
+            return None
+
+        info = self.get_register_info()["registers"]
+
+        def read(entry):
+            # A register set reports a register under the name LLDB displays,
+            # which can be an alias of the architectural name the register info
+            # uses. The register info carries that alias in "alt-name".
+            if entry["name"] in regs:
+                return regs[entry["name"]]
+            return regs.get(entry.get("alt-name", ""), 0)
+
+        return struct.pack(f"{len(info)}Q", *(read(r) for r in info))
+
+
+class WrapVariablesProvider(ScriptedFrameProvider):
+    @staticmethod
+    def get_description():
+        return f"Wrap the {WRAPPED_FUNCTION!r} frame and forward its registers"
+
+    def get_frame_at_index(self, index):
+        if index >= self.input_frames.GetSize():
+            return None
+
+        frame = self.input_frames.GetFrameAtIndex(index)
+        if not frame.IsValid():
+            return None
+
+        if frame.GetFunctionName() == WRAPPED_FUNCTION:
+            return WrappedFrame(self.thread, frame, index)
+
+        # Returning an int reuses the input frame at that index unchanged.
+        return index
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/main.c b/lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/main.c
new file mode 100644
index 0000000000000..3ac59261bd7f2
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/wrapped_frame_register_context/main.c
@@ -0,0 +1,6 @@
+int compute(int a, int b) {
+  int sum = a + b;
+  return sum; // Break here.
+}
+
+int main(void) { return compute(3, 4) == 7 ? 0 : 1; }

>From d6343a3854b79cf7d071413a822c2c52949d4e93 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Tue, 18 Aug 2026 22:30:12 +0100
Subject: [PATCH 14/16] [lldb] Don't merge a frame provider's frames into the
 unwinder frame list (#216847)

`Thread::ClearStackFrames` keeps the previous public frame list as the
predecessor of the next unwinder list. When a scripted frame provider is
registered, that public list is a `SyntheticStackFrameList` holding
`BorrowedStackFrames`, and the unwinder list's merge step reuses a
predecessor frame whenever the stack IDs match, so it adopted frames
belonging to the provider's list.

`UpdatePreviousFrameFromCurrentFrame` refreshes
`StackFrame::m_frame_index`, but `BorrowedStackFrame::GetFrameIndex()`
returns its own `m_new_frame_index`, which supersedes it. The adopted
frame keeps reporting its index from the previous stop, so once the
stack grows the backtrace stops counting up:

```
  frame #1: middle
  frame #1: main
  frame #2: start
```

Such a frame also delegates its stack ID, register context and symbol
context to a frame from the previous stop that the merge never
refreshes, so it could report stale state as well. In the backtrace
above, main also lost its source location.

When the merge finds a matching predecessor frame that is a
`BorrowedStackFrame`, keep the freshly unwound frame instead of adopting
it. Only providers create BorrowedStackFrames, so this never drops a
frame the unwinder built itself.

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
(cherry picked from commit 1aed87d2069e38d1017965371c83f22e3ec4a369)
---
 lldb/source/Target/StackFrameList.cpp         |  9 ++++
 .../frame_index_after_depth_change/Makefile   |  4 ++
 ...FrameProviderFrameIndexAfterDepthChange.py | 49 +++++++++++++++++++
 .../frame_provider.py                         | 21 ++++++++
 .../frame_index_after_depth_change/main.c     | 20 ++++++++
 5 files changed, 103 insertions(+)
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/Makefile
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/TestFrameProviderFrameIndexAfterDepthChange.py
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/frame_provider.py
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/main.c

diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp
index 465d1af11add0..95fae215daa83 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -15,6 +15,7 @@
 #include "lldb/Symbol/Block.h"
 #include "lldb/Symbol/Function.h"
 #include "lldb/Symbol/Symbol.h"
+#include "lldb/Target/BorrowedStackFrame.h"
 #include "lldb/Target/Process.h"
 #include "lldb/Target/RegisterContext.h"
 #include "lldb/Target/StackFrame.h"
@@ -637,6 +638,14 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx,
       if (curr_frame->GetStackID() != prev_frame->GetStackID())
         break;
 
+      // Never adopt a frame borrowed from another StackFrameList, which only a
+      // provider's SyntheticStackFrameList hands out: it keeps reporting the
+      // index of the frame it borrows, and the update below cannot change
+      // that. Skipping it is safe because the merge only carries cached state
+      // onto a frame this list has already unwound correctly.
+      if (llvm::isa<BorrowedStackFrame>(prev_frame))
+        continue;
+
       prev_frame->UpdatePreviousFrameFromCurrentFrame(*curr_frame);
       // Now copy the fixed up previous frame into the current frames so the
       // pointer doesn't change.
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/Makefile b/lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/Makefile
new file mode 100644
index 0000000000000..695335e068c0c
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/Makefile
@@ -0,0 +1,4 @@
+C_SOURCES := main.c
+CFLAGS_EXTRAS := -std=c99
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/TestFrameProviderFrameIndexAfterDepthChange.py b/lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/TestFrameProviderFrameIndexAfterDepthChange.py
new file mode 100644
index 0000000000000..54fdee70c51b2
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/TestFrameProviderFrameIndexAfterDepthChange.py
@@ -0,0 +1,49 @@
+"""
+Test that the unwinder renumbers its own frames when the stack gets deeper
+between two stops while a scripted frame provider is registered.
+"""
+
+import os
+import re
+
+import lldb
+import lldbsuite.test.lldbutil as lldbutil
+from lldbsuite.test.lldbtest import *
+
+
+class TestFrameProviderFrameIndexAfterDepthChange(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def test_unwinder_indices_after_stack_grows(self):
+        self.build()
+        target, process, _, _ = lldbutil.run_to_name_breakpoint(self, "shallow")
+
+        self.runCmd(
+            "command script import "
+            + os.path.join(self.getSourceDir(), "frame_provider.py")
+        )
+        error = lldb.SBError()
+        target.RegisterScriptedFrameProvider(
+            "frame_provider.IdentityProvider", lldb.SBStructuredData(), error
+        )
+        self.assertSuccess(error, "Failed to register the frame provider")
+
+        # Only a fully fetched list is kept as the next stop's predecessor, and
+        # that predecessor is what the next unwinder list merges against.
+        self.runCmd("bt")
+
+        # Stop deeper down, so the outermost frames no longer belong at the
+        # indices they had at the shallow stop.
+        lldbutil.run_break_set_by_symbol(self, "deep")
+        process.Continue()
+        self.assertState(process.GetState(), lldb.eStateStopped)
+
+        # 'bt --provider *' prints one section per provider, the base unwinder
+        # first. Its frames must still be numbered sequentially.
+        self.runCmd("bt --provider '*'")
+        output = self.res.GetOutput()
+        unwinder = output.split("=== Provider 1")[0]
+        indices = [int(idx) for idx in re.findall(r"frame #(\d+)", unwinder)]
+
+        self.assertTrue(indices, "Found no frames for the base unwinder")
+        self.assertEqual(indices, list(range(len(indices))), output)
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/frame_provider.py b/lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/frame_provider.py
new file mode 100644
index 0000000000000..72f5cfd3ea4c8
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/frame_provider.py
@@ -0,0 +1,21 @@
+"""
+Frame provider that forwards every input frame under its own index.
+
+Identity forwarding still wraps each frame in a BorrowedStackFrame, which is
+what this test needs: those wrappers end up cached in the thread's public frame
+list, and that list becomes the predecessor the next unwinder list merges
+against.
+"""
+
+from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
+
+
+class IdentityProvider(ScriptedFrameProvider):
+    @staticmethod
+    def get_description():
+        return "Provider that forwards each frame under its own index"
+
+    def get_frame_at_index(self, index):
+        if index < len(self.input_frames):
+            return index
+        return None
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/main.c b/lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/main.c
new file mode 100644
index 0000000000000..d7daab2f50bad
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/frame_index_after_depth_change/main.c
@@ -0,0 +1,20 @@
+// The stack is shallower at the first stop than at the second, so the outermost
+// frames have to be renumbered in between.
+
+void shallow(void) {
+  int shallow_local = 1;
+  (void)shallow_local; // Shallow breakpoint.
+}
+
+void deep(void) {
+  int deep_local = 2;
+  (void)deep_local; // Deep breakpoint.
+}
+
+void middle(void) { deep(); }
+
+int main(void) {
+  shallow();
+  middle();
+  return 0;
+}

>From c2ebfd636b8803a8dede5cffaf897cdf8dc43351 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Wed, 19 Aug 2026 07:13:04 +0100
Subject: [PATCH 15/16] [lldb] Fix scripted frame provider cross-thread
 re-entrant deadlock (#208242)

`GetStoppedExecutionContext` unconditionally blocked acquiring the
target's API mutex. A thread already holding that mutex (for example a
`bt` command thread, through `CommandObjectParsed`'s
`eCommandTryTargetAPILock`) can end up waiting on a `StackFrameList`
lock held by another thread (for example the debugger's event-handler
thread) that is itself blocked re-acquiring the API mutex from inside a
scripted frame provider's Python code that touches the SB API. This is a
classic AB-BA deadlock.

This patch introduces
`Policy::Capabilities::can_bypass_target_api_mutex`, pushed around every
scripted-extension callback in `ScriptedPythonInterface`:
`CreatePluginObject`, `Dispatch` and `CallStaticMethod`. A thread
running one of these callbacks isn't servicing a client-facing SB API
entry point; it doesn't need the same locking guarantees a top-level SB
API call does for any of the calls it makes during that window, not just
the one that happens to deadlock.

`TargetAPIMutex::lock()`/`try_lock()` check this capability when
resolving which mutex to use: when the current thread's policy says it
can bypass, they leave the handle pointing at nothing, touching no
synchronization primitive at all, instead of the real mutex. Every
existing caller keeps its own `lock_guard`/`unique_lock` code unchanged
and becomes deadlock-safe automatically, since a no-op handle can be
`locked`/`unlocked` from any thread with no cross-thread hazard.

Extensions that run directly on the user's behalf opt out. A scripted
command that the user invoked directly, rather than a callback running
internally, needs to keep serializing on the API mutex. This is achieved
by checking `ScriptedInterface::UserCanRunDirectly` so the exemption
covers every scripting language rather than only Python.

This patch adds regression tests for both the original deadlock and for
a blocking `SBMutex.lock()` call made from inside a callback, including
`TestSBMutexReflectsTargetMutex`, which confirms `SBMutex` aliases the
real, shared target mutex rather than the bypass no-op.

Depends on #212872, which introduces TargetAPIMutex's per-call
resolve/replay behavior that makes this bypass safe to observe through a
deferred-lock SBMutex.

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
(cherry picked from commit b81f88c2ee2ba1c161aa4d68f58c00e75c5189ad)
---
 .../Interfaces/ScriptedCommandInterface.h     |   2 +
 .../Interfaces/ScriptedInterface.h            |   6 +
 lldb/include/lldb/Target/Target.h             |   4 +
 lldb/include/lldb/Target/TargetAPIMutex.h     |   2 +
 lldb/include/lldb/Utility/Policy.h            |   7 ++
 .../Interfaces/ScriptedPythonInterface.h      |  13 ++
 .../ScriptedFrameProvider.cpp                 |  11 +-
 lldb/source/Target/Target.cpp                 |   8 ++
 lldb/source/Target/TargetAPIMutex.cpp         |  27 ++---
 lldb/source/Utility/Policy.cpp                |   7 ++
 .../Makefile                                  |   2 +
 ...ProviderRegisterCommandAPIMutexDeadlock.py |  87 ++++++++++++++
 .../frame_provider.py                         |  23 ++++
 .../main.c                                    |   7 ++
 .../sbmutex_reflects_target_mutex/Makefile    |   2 +
 .../TestHoldMutexNoDeadlock.py                |  89 ++++++++++++++
 .../TestSBMutexReflectsTargetMutex.py         | 111 ++++++++++++++++++
 .../hold_mutex_frame_provider.py              |  35 ++++++
 .../sbmutex_reflects_target_mutex/main.c      |  12 ++
 .../sbmutex_frame_provider.py                 |  84 +++++++++++++
 lldb/unittests/Interpreter/CMakeLists.txt     |   1 +
 .../Interpreter/TestScriptedInterface.cpp     |  55 +++++++++
 lldb/unittests/Target/TargetAPIMutexTest.cpp  |  61 ++++++++++
 lldb/unittests/Utility/PolicyTest.cpp         |  16 ++-
 24 files changed, 651 insertions(+), 21 deletions(-)
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/Makefile
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/TestFrameProviderRegisterCommandAPIMutexDeadlock.py
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/frame_provider.py
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/main.c
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/Makefile
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/TestHoldMutexNoDeadlock.py
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/TestSBMutexReflectsTargetMutex.py
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/hold_mutex_frame_provider.py
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/main.c
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/sbmutex_frame_provider.py
 create mode 100644 lldb/unittests/Interpreter/TestScriptedInterface.cpp

diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedCommandInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedCommandInterface.h
index 29f4d273e49f0..879aaa936f759 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedCommandInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedCommandInterface.h
@@ -15,6 +15,8 @@
 namespace lldb_private {
 class ScriptedCommandInterface : virtual public ScriptedInterface {
 public:
+  bool UserCanRunDirectly() const override { return true; }
+
   virtual llvm::Expected<StructuredData::GenericSP>
   CreatePluginObject(llvm::StringRef class_name,
                      lldb::DebuggerSP debugger_sp) = 0;
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
index 21bb91960f777..417a143cb3e3d 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
@@ -37,6 +37,12 @@ class ScriptedInterface {
     return m_scripted_metadata;
   }
 
+  /// 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
+  /// already-bypassed callback every extension inherits the ambient policy.
+  virtual bool UserCanRunDirectly() const { return false; }
+
   struct AbstractMethodRequirement {
     llvm::StringLiteral name;
     size_t min_arg_count = 0;
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index 853a1770345f4..652c8020217ec 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -2048,6 +2048,10 @@ class Target : public std::enable_shared_from_this<Target>,
   void PrintDummySignals(Stream &strm, Args &signals);
 
 protected:
+  /// The mutex the calling thread must serialize on for its current policy, or
+  /// nullptr when that policy bypasses the API mutex entirely.
+  std::recursive_mutex *GetAPIMutexForCurrentPolicy();
+
   /// Implementing of ModuleList::Notifier.
 
   void NotifyModuleAdded(const ModuleList &module_list,
diff --git a/lldb/include/lldb/Target/TargetAPIMutex.h b/lldb/include/lldb/Target/TargetAPIMutex.h
index d822329842264..01af2ac7d31f1 100644
--- a/lldb/include/lldb/Target/TargetAPIMutex.h
+++ b/lldb/include/lldb/Target/TargetAPIMutex.h
@@ -56,6 +56,8 @@ class TargetAPIMutex {
   }
 
 private:
+  void Resolve();
+
   /// An aliasing shared_ptr into m_target_sp's own mutex, resolved fresh
   /// on every lock()/try_lock() call. Shares m_target_sp's control block
   /// (keeping the Target alive) while pointing at the mutex living inside
diff --git a/lldb/include/lldb/Utility/Policy.h b/lldb/include/lldb/Utility/Policy.h
index afeeab19c2ed0..b3f2e1ba04e9c 100644
--- a/lldb/include/lldb/Utility/Policy.h
+++ b/lldb/include/lldb/Utility/Policy.h
@@ -50,6 +50,7 @@ struct Policy {
     bool can_run_breakpoint_actions = true;
     bool can_load_frame_providers = true;
     bool can_run_frame_recognizers = true;
+    bool can_bypass_target_api_mutex = false;
   };
 
   /// Why a private-state policy is being pushed. Distinguishes a PST's
@@ -75,6 +76,7 @@ struct Policy {
   static Policy CreatePrivateState(
       PrivateStatePurpose purpose = PrivateStatePurpose::Default);
   static Policy CreatePublicStateRunningExpression();
+  static Policy CreateScriptedExtensionCall();
   /// @}
 
   void Dump(Stream &s) const;
@@ -140,6 +142,11 @@ class PolicyStack {
     return Guard();
   }
 
+  [[nodiscard]] Guard PushScriptedExtensionCall() {
+    Push(Policy::CreateScriptedExtensionCall());
+    return Guard();
+  }
+
 private:
   void Push(Policy policy) { m_stack.push_back(std::move(policy)); }
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index ce48f2468d380..91663d1293108 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -18,6 +18,7 @@
 #include "lldb/API/SBCommandReturnObject.h"
 #include "lldb/Interpreter/Interfaces/ScriptedInterface.h"
 #include "lldb/Utility/DataBufferHeap.h"
+#include "lldb/Utility/Policy.h"
 
 #include "../PythonDataObjects.h"
 #include "../SWIGPythonBridge.h"
@@ -196,6 +197,10 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
         return create_error("Missing scripting object.");
     }
 
+    std::optional<PolicyStack::Guard> policy_guard;
+    if (!UserCanRunDirectly())
+      policy_guard = PolicyStack::Get().PushScriptedExtensionCall();
+
     Locker py_lock(&m_interpreter, Locker::AcquireLock | Locker::NoSTDIN,
                    Locker::FreeLock);
 
@@ -413,6 +418,10 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
       return ErrorWithMessage<T>(caller_signature, "missing script class name",
                                  error);
 
+    std::optional<PolicyStack::Guard> policy_guard;
+    if (!UserCanRunDirectly())
+      policy_guard = PolicyStack::Get().PushScriptedExtensionCall();
+
     Locker py_lock(&m_interpreter, Locker::AcquireLock | Locker::NoSTDIN,
                    Locker::FreeLock);
 
@@ -536,6 +545,10 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
       return ErrorWithMessage<T>(caller_signature, "python object ill-formed",
                                  error);
 
+    std::optional<PolicyStack::Guard> policy_guard;
+    if (!UserCanRunDirectly())
+      policy_guard = PolicyStack::Get().PushScriptedExtensionCall();
+
     Locker py_lock(&m_interpreter, Locker::AcquireLock | Locker::NoSTDIN,
                    Locker::FreeLock);
 
diff --git a/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/ScriptedFrameProvider.cpp b/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/ScriptedFrameProvider.cpp
index ab09d86b24d95..977ea5e0548e4 100644
--- a/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/ScriptedFrameProvider.cpp
+++ b/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/ScriptedFrameProvider.cpp
@@ -179,10 +179,13 @@ ScriptedFrameProvider::GetFrameAtIndex(uint32_t idx) {
     if (real_frame_index < m_input_frames->GetNumFrames()) {
       StackFrameSP real_frame_sp =
           m_input_frames->GetFrameAtIndex(real_frame_index);
-      synth_frame_sp =
-          (real_frame_index == idx)
-              ? real_frame_sp
-              : std::make_shared<BorrowedStackFrame>(real_frame_sp, idx);
+      // Always wrap in a BorrowedStackFrame, even when the index is
+      // unchanged. FetchFramesUpTo below unconditionally overwrites
+      // frame_sp->m_frame_list_id to tag the frame as belonging to this
+      // synthetic list; reusing real_frame_sp directly would corrupt the
+      // parent list's cached frame (still m_input_frames' object) to claim
+      // it belongs to this list instead.
+      synth_frame_sp = std::make_shared<BorrowedStackFrame>(real_frame_sp, idx);
     }
   } else if (StructuredData::Dictionary *dict = obj_sp->GetAsDictionary()) {
     // Check if it's a dictionary describing a frame.
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index 74bdee8a54a72..80f5ea544a430 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -66,6 +66,7 @@
 #include "lldb/Utility/LLDBAssert.h"
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/Log.h"
+#include "lldb/Utility/Policy.h"
 #include "lldb/Utility/RealpathPrefixes.h"
 #include "lldb/Utility/State.h"
 #include "lldb/Utility/StreamString.h"
@@ -5996,6 +5997,13 @@ TargetAPIMutex Target::GetAPIMutex() {
   return TargetAPIMutex(shared_from_this());
 }
 
+std::recursive_mutex *Target::GetAPIMutexForCurrentPolicy() {
+  Policy policy = PolicyStack::Get().Current();
+  if (policy.capabilities.can_bypass_target_api_mutex)
+    return nullptr;
+  return policy.view == Policy::View::Private ? &m_private_mutex : &m_mutex;
+}
+
 /// Get metrics associated with this target in JSON format.
 llvm::json::Value
 Target::ReportStatistics(const lldb_private::StatisticsOptions &options) {
diff --git a/lldb/source/Target/TargetAPIMutex.cpp b/lldb/source/Target/TargetAPIMutex.cpp
index 26079c540ee2a..97d5fa98ac1a3 100644
--- a/lldb/source/Target/TargetAPIMutex.cpp
+++ b/lldb/source/Target/TargetAPIMutex.cpp
@@ -8,29 +8,26 @@
 
 #include "lldb/Target/TargetAPIMutex.h"
 #include "lldb/Target/Target.h"
-#include "lldb/Utility/Policy.h"
 
 using namespace lldb_private;
 
+void TargetAPIMutex::Resolve() {
+  if (!m_target_sp)
+    return;
+
+  std::recursive_mutex *real_mutex = m_target_sp->GetAPIMutexForCurrentPolicy();
+  m_mutex = real_mutex
+                ? std::shared_ptr<std::recursive_mutex>(m_target_sp, real_mutex)
+                : nullptr;
+}
+
 void TargetAPIMutex::lock() {
-  if (m_target_sp) {
-    Policy policy = PolicyStack::Get().Current();
-    std::recursive_mutex &real_mutex = policy.view == Policy::View::Private
-                                           ? m_target_sp->m_private_mutex
-                                           : m_target_sp->m_mutex;
-    m_mutex = std::shared_ptr<std::recursive_mutex>(m_target_sp, &real_mutex);
-  }
+  Resolve();
   if (m_mutex)
     m_mutex->lock();
 }
 
 bool TargetAPIMutex::try_lock() {
-  if (m_target_sp) {
-    Policy policy = PolicyStack::Get().Current();
-    std::recursive_mutex &real_mutex = policy.view == Policy::View::Private
-                                           ? m_target_sp->m_private_mutex
-                                           : m_target_sp->m_mutex;
-    m_mutex = std::shared_ptr<std::recursive_mutex>(m_target_sp, &real_mutex);
-  }
+  Resolve();
   return m_mutex ? m_mutex->try_lock() : true;
 }
diff --git a/lldb/source/Utility/Policy.cpp b/lldb/source/Utility/Policy.cpp
index 4d1999aaf7b92..04293d7a03f85 100644
--- a/lldb/source/Utility/Policy.cpp
+++ b/lldb/source/Utility/Policy.cpp
@@ -64,6 +64,12 @@ Policy Policy::CreatePublicStateRunningExpression() {
   return p;
 }
 
+Policy Policy::CreateScriptedExtensionCall() {
+  Policy p = PolicyStack::Get().Current();
+  p.capabilities.can_bypass_target_api_mutex = true;
+  return p;
+}
+
 PolicyStack::Guard::~Guard() {
   if (!m_active)
     return;
@@ -108,6 +114,7 @@ void Policy::Dump(Stream &s) const {
   s << " bp_actions=" << capabilities.can_run_breakpoint_actions;
   s << " frame_providers=" << capabilities.can_load_frame_providers;
   s << " frame_recognizers=" << capabilities.can_run_frame_recognizers;
+  s << " bypass_api_mutex=" << capabilities.can_bypass_target_api_mutex;
   s << '}';
 }
 
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/Makefile b/lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/Makefile
new file mode 100644
index 0000000000000..c9319d6e6888a
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/Makefile
@@ -0,0 +1,2 @@
+C_SOURCES := main.c
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/TestFrameProviderRegisterCommandAPIMutexDeadlock.py b/lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/TestFrameProviderRegisterCommandAPIMutexDeadlock.py
new file mode 100644
index 0000000000000..e93ed6890fc04
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/TestFrameProviderRegisterCommandAPIMutexDeadlock.py
@@ -0,0 +1,87 @@
+"""
+Test that a scripted frame provider whose get_frame_at_index touches SB
+API (self.input_frames) does not deadlock when running `bt` from the
+command interpreter.
+
+GetStoppedExecutionContext (used by SBFrame::IsValid, among others)
+unconditionally blocked acquiring the target's API mutex. The command
+thread running `bt` already holds that mutex (CommandObjectParsed's
+eCommandTryTargetAPILock) and can end up waiting on a StackFrameList
+lock held by the debugger's event-handler thread, which is itself
+blocked re-acquiring the API mutex from inside this provider's Python
+code: an AB-BA deadlock between the command thread and the
+event-handler thread.
+
+The event-handler thread only runs when commands are driven through
+SBDebugger.RunCommandInterpreter (what the lldb driver itself uses),
+not through plain HandleCommand, so this test drives commands that way.
+
+Note: this is a genuine cross-thread race (the command thread vs. the
+debugger's event-handler thread), not a deterministic sequential
+deadlock, so this test is best-effort: it raises the odds of hitting
+the race within a single invocation but cannot guarantee it.
+"""
+
+import os
+import lldb
+import lldbsuite.test.lldbutil as lldbutil
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+
+
+class TestFrameProviderRegisterCommandAPIMutexDeadlock(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def test_register_command_then_bt_no_deadlock(self):
+        """
+        Register a scripted frame provider whose get_frame_at_index
+        touches SB API, then repeatedly run `bt` through
+        RunCommandInterpreter. Should complete without deadlocking.
+        """
+        self.build()
+
+        lldbutil.run_to_name_breakpoint(self, "frame3")
+
+        provider_path = os.path.join(self.getSourceDir(), "frame_provider.py")
+
+        commands = ["command script import " + provider_path]
+        commands.append(
+            "target frame-provider register -C frame_provider.DictFrameProvider"
+        )
+        # Run `bt` several times to raise the odds of hitting the race
+        # (see module docstring).
+        commands.extend(["bt"] * 20)
+        commands.append("quit")
+
+        stdin_path = self.getBuildArtifact("stdin.txt")
+        stdout_path = self.getBuildArtifact("stdout.txt")
+        with open(stdin_path, "w") as f:
+            f.write("\n".join(commands) + "\n")
+
+        with open(stdin_path, "r") as in_fileH, open(stdout_path, "w") as out_fileH:
+            in_sbf = lldb.SBFile(in_fileH.fileno(), "r", False)
+            out_sbf = lldb.SBFile(out_fileH.fileno(), "w", False)
+            self.assertSuccess(self.dbg.SetInputFile(in_sbf))
+            self.assertSuccess(self.dbg.SetOutputFile(out_sbf))
+            self.assertSuccess(self.dbg.SetErrorFile(out_sbf))
+
+            options = lldb.SBCommandInterpreterRunOptions()
+            options.SetEchoCommands(False)
+            options.SetPrintResults(True)
+            options.SetStopOnError(False)
+            options.SetStopOnCrash(False)
+
+            # If the API-mutex deadlock regresses, this call hangs forever
+            # (timing out the test run).
+            n_errors, quit_requested, has_crashed = self.dbg.RunCommandInterpreter(
+                True, False, options, 0, False, False
+            )
+
+        with open(stdout_path, "r") as out_fileH:
+            output = out_fileH.read()
+
+        self.assertFalse(has_crashed, "lldb should not have crashed")
+        self.assertTrue(quit_requested, "quit command should have been processed")
+        self.assertEqual(n_errors, 0, f"unexpected errors in output:\n{output}")
+
+        self.assertIn("successfully registered scripted frame provider", output)
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/frame_provider.py b/lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/frame_provider.py
new file mode 100644
index 0000000000000..9b4b948eb372d
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/frame_provider.py
@@ -0,0 +1,23 @@
+"""
+Frame provider that returns dict-based synthetic frames while touching
+self.input_frames from get_frame_at_index, to exercise the API-mutex
+deadlock.
+"""
+
+from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
+
+
+class DictFrameProvider(ScriptedFrameProvider):
+    @staticmethod
+    def get_description():
+        return "Provider that returns dict-based synthetic frames"
+
+    def get_frame_at_index(self, index):
+        if index >= len(self.input_frames):
+            return None
+        # __getitem__ calls SBFrame.IsValid() internally, which is what
+        # exercises GetStoppedExecutionContext.
+        frame = self.input_frames[index]
+        if frame is None:
+            return None
+        return {"idx": index, "pc": frame.GetPC()}
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/main.c b/lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/main.c
new file mode 100644
index 0000000000000..1aa56e3eddf7a
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/register_command_api_mutex_deadlock/main.c
@@ -0,0 +1,7 @@
+int frame3() { return 3; }
+
+int frame2() { return frame3(); }
+
+int frame1() { return frame2(); }
+
+int main() { return frame1(); }
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/Makefile b/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/Makefile
new file mode 100644
index 0000000000000..c9319d6e6888a
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/Makefile
@@ -0,0 +1,2 @@
+C_SOURCES := main.c
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/TestHoldMutexNoDeadlock.py b/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/TestHoldMutexNoDeadlock.py
new file mode 100644
index 0000000000000..539c8949c4671
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/TestHoldMutexNoDeadlock.py
@@ -0,0 +1,89 @@
+"""
+Test that a scripted frame provider can safely call a blocking
+SBMutex.lock() from inside get_frame_at_index without deadlocking.
+
+The private state thread can reach this callback without already
+holding the target's real API mutex. Without the bypass described
+below, a blocking lock() call here could genuinely wait for that mutex,
+and deadlock if some other thread (e.g. a `bt` command thread) holds it
+at that moment. ScriptedPythonInterface::Dispatch prevents this by
+pushing the can_bypass_target_api_mutex policy around the whole
+callback. TargetAPIMutex re-checks that policy on every lock() call
+rather than caching whatever was current when the SBMutex was
+constructed, so lock() here resolves to a genuine no-op instead: no
+synchronization primitive is touched at all, and it never contends with
+anyone.
+
+This drives a genuine cross-thread race and is best-effort: it raises
+the odds of exercising the path within a single invocation but the
+important guarantee is that it cannot hang, not that it hits any
+particular thread ordering.
+"""
+
+import os
+import lldb
+import lldbsuite.test.lldbutil as lldbutil
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+
+
+class TestHoldMutexNoDeadlock(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def test_hold_mutex_no_deadlock(self):
+        """
+        Register a scripted frame provider that locks and holds
+        target.GetAPIMutex() from get_frame_at_index, then run `bt` and
+        `continue` through RunCommandInterpreter. Should complete
+        without deadlocking.
+        """
+        self.build()
+
+        lldbutil.run_to_name_breakpoint(self, "frame3")
+
+        provider_path = os.path.join(
+            self.getSourceDir(), "hold_mutex_frame_provider.py"
+        )
+        commands = ["command script import " + provider_path]
+        commands.append(
+            "target frame-provider register "
+            "-C hold_mutex_frame_provider.HoldMutexFrameProvider"
+        )
+        # Interleave `bt` with `continue` (hitting the same breakpoint
+        # again, via a loop in main.c) so get_frame_at_index runs
+        # repeatedly instead of once, raising the odds of hitting the
+        # race within a single test invocation.
+        commands.extend(["bt", "continue"] * 20)
+        commands.append("quit")
+
+        stdin_path = self.getBuildArtifact("stdin.txt")
+        stdout_path = self.getBuildArtifact("stdout.txt")
+        with open(stdin_path, "w") as f:
+            f.write("\n".join(commands) + "\n")
+
+        with open(stdin_path, "r") as in_fileH, open(stdout_path, "w") as out_fileH:
+            in_sbf = lldb.SBFile(in_fileH.fileno(), "r", False)
+            out_sbf = lldb.SBFile(out_fileH.fileno(), "w", False)
+            self.assertSuccess(self.dbg.SetInputFile(in_sbf))
+            self.assertSuccess(self.dbg.SetOutputFile(out_sbf))
+            self.assertSuccess(self.dbg.SetErrorFile(out_sbf))
+
+            options = lldb.SBCommandInterpreterRunOptions()
+            options.SetEchoCommands(False)
+            options.SetPrintResults(True)
+            options.SetStopOnError(False)
+            options.SetStopOnCrash(False)
+
+            # If the bypass regresses, this call hangs forever (timing
+            # out the test run).
+            n_errors, quit_requested, has_crashed = self.dbg.RunCommandInterpreter(
+                True, False, options, 0, False, False
+            )
+
+        with open(stdout_path, "r") as out_fileH:
+            output = out_fileH.read()
+
+        self.assertFalse(has_crashed, "lldb should not have crashed")
+        self.assertTrue(quit_requested, "quit command should have been processed")
+        self.assertEqual(n_errors, 0, f"unexpected errors in output:\n{output}")
+        self.assertIn("successfully registered scripted frame provider", output)
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/TestSBMutexReflectsTargetMutex.py b/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/TestSBMutexReflectsTargetMutex.py
new file mode 100644
index 0000000000000..fed5000daecbb
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/TestSBMutexReflectsTargetMutex.py
@@ -0,0 +1,111 @@
+"""
+Test that a scripted frame provider calling SBTarget.GetAPIMutex() from
+get_frame_at_index gets a handle that reflects the state of the target's
+real, shared API mutex, even though the callback's own thread is exempt
+from having to serialize on it. SBMutex is meant to be obtainable inside
+a bypassed scripted callback and locked later, once that bypass no
+longer applies (e.g. on a different thread with no scripted-extension
+call on its stack, as this test's own provider does; see
+sbmutex_frame_provider.py). So it must always alias the genuine target
+mutex rather than resolving to the no-op the bypass policy makes it for
+internal callers.
+
+The provider obtains the mutex from inside get_frame_at_index, which is
+safe since obtaining a handle doesn't resolve or lock anything, and does
+every acquisition with try_lock() on threads it spawns and joins (see
+sbmutex_frame_provider.py). Nothing may block on lock() there, on any
+thread: the thread that reaches the callback may already hold the real
+mutex while waiting on the provider, so a blocking acquisition deadlocks
+the session. `bt` is interleaved with `continue` so the callback runs many
+times rather than once.
+"""
+
+import os
+import lldb
+import lldbsuite.test.lldbutil as lldbutil
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+
+
+class TestSBMutexReflectsTargetMutex(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def test_sbmutex_reflects_target_mutex(self):
+        """
+        Register a scripted frame provider that checks
+        target.GetAPIMutex().try_lock() from get_frame_at_index, then
+        repeatedly run `bt` and `continue` through RunCommandInterpreter.
+        Should complete without deadlocking, regardless of whether
+        contention is observed.
+        """
+        self.build()
+
+        lldbutil.run_to_name_breakpoint(self, "frame3")
+
+        provider_path = os.path.join(self.getSourceDir(), "sbmutex_frame_provider.py")
+        artifact_path = self.getBuildArtifact("contention.txt")
+        if os.path.exists(artifact_path):
+            os.remove(artifact_path)
+
+        commands = ["command script import " + provider_path]
+        commands.append(
+            "target frame-provider register "
+            "-C sbmutex_frame_provider.ContentionCheckFrameProvider "
+            "-k artifact_path -v " + artifact_path
+        )
+        # `bt` only re-invokes get_frame_at_index when the thread's stack
+        # frame list was invalidated by a new stop, so interleave `bt` with
+        # `continue` (hitting the same breakpoint again, in a loop in
+        # main.c) to get repeated fresh invocations of the check.
+        commands.extend(["bt", "continue"] * 20)
+        commands.append("quit")
+
+        stdin_path = self.getBuildArtifact("stdin.txt")
+        stdout_path = self.getBuildArtifact("stdout.txt")
+        with open(stdin_path, "w") as f:
+            f.write("\n".join(commands) + "\n")
+
+        with open(stdin_path, "r") as in_fileH, open(stdout_path, "w") as out_fileH:
+            in_sbf = lldb.SBFile(in_fileH.fileno(), "r", False)
+            out_sbf = lldb.SBFile(out_fileH.fileno(), "w", False)
+            self.assertSuccess(self.dbg.SetInputFile(in_sbf))
+            self.assertSuccess(self.dbg.SetOutputFile(out_sbf))
+            self.assertSuccess(self.dbg.SetErrorFile(out_sbf))
+
+            options = lldb.SBCommandInterpreterRunOptions()
+            options.SetEchoCommands(False)
+            options.SetPrintResults(True)
+            options.SetStopOnError(False)
+            options.SetStopOnCrash(False)
+
+            n_errors, quit_requested, has_crashed = self.dbg.RunCommandInterpreter(
+                True, False, options, 0, False, False
+            )
+
+        with open(stdout_path, "r") as out_fileH:
+            output = out_fileH.read()
+
+        self.assertFalse(has_crashed, "lldb should not have crashed")
+        self.assertTrue(quit_requested, "quit command should have been processed")
+        self.assertEqual(n_errors, 0, f"unexpected errors in output:\n{output}")
+        self.assertIn("successfully registered scripted frame provider", output)
+
+        self.assertTrue(
+            os.path.exists(artifact_path),
+            "get_frame_at_index should have run and recorded at least one outcome",
+        )
+        with open(artifact_path, "r") as f:
+            outcomes = [line.strip() for line in f if line.strip()]
+
+        self.assertTrue(outcomes, "expected at least one recorded outcome")
+        # Either recorded outcome means a try_lock() failed, which a no-op
+        # handle can never do. Only the third outcome, two handles holding the
+        # mutex at once, indicates SBMutex resolved to the bypass no-op.
+        self.assertTrue(
+            set(outcomes)
+            <= {
+                "second handle contended with the first",
+                "another thread already held the real mutex",
+            },
+            f"SBMutex did not alias the real target mutex: {outcomes}",
+        )
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/hold_mutex_frame_provider.py b/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/hold_mutex_frame_provider.py
new file mode 100644
index 0000000000000..a7d2db5d567d0
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/hold_mutex_frame_provider.py
@@ -0,0 +1,35 @@
+"""
+Frame provider whose get_frame_at_index locks the target's real API
+mutex via SBMutex and holds it briefly, from inside the bypassed
+scripted-extension callback. See TestHoldMutexNoDeadlock.py for why this
+must not deadlock.
+"""
+
+import time
+
+from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
+
+HOLD_DURATION_SECONDS = 0.2
+
+
+class HoldMutexFrameProvider(ScriptedFrameProvider):
+    @staticmethod
+    def get_description():
+        return (
+            "Provider that holds the real API mutex via SBMutex from get_frame_at_index"
+        )
+
+    def get_frame_at_index(self, index):
+        if index >= len(self.input_frames):
+            return None
+
+        if index == 0:
+            mutex = self.target.GetAPIMutex()
+            mutex.lock()
+            time.sleep(HOLD_DURATION_SECONDS)
+            mutex.unlock()
+
+        frame = self.input_frames[index]
+        if frame is None:
+            return None
+        return {"idx": index, "pc": frame.GetPC()}
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/main.c b/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/main.c
new file mode 100644
index 0000000000000..ed95560986ac0
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/main.c
@@ -0,0 +1,12 @@
+int frame3() { return 3; }
+
+int frame2() { return frame3(); }
+
+int frame1() { return frame2(); }
+
+int main() {
+  int result = 0;
+  for (int i = 0; i < 25; ++i)
+    result += frame1();
+  return result;
+}
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/sbmutex_frame_provider.py b/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/sbmutex_frame_provider.py
new file mode 100644
index 0000000000000..655fb62f130ae
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/sbmutex_reflects_target_mutex/sbmutex_frame_provider.py
@@ -0,0 +1,84 @@
+"""
+Frame provider whose get_frame_at_index confirms SBMutex aliases the target's
+genuine API mutex rather than the no-op TargetAPIMutex resolves to under the
+can_bypass_target_api_mutex policy that ScriptedPythonInterface pushes for a
+callback's entire duration.
+
+Every acquisition here uses try_lock() and runs on a freshly spawned thread, so
+no scripted-extension call is on its stack and none of it is exempt from the
+real mutex. Blocking on lock() is never an option: the thread that reaches this
+callback may already hold the real mutex and is waiting on this code, so a
+blocking acquisition on any thread deadlocks the session.
+
+TargetAPIMutex::try_lock() returns true unconditionally when it resolves to the
+no-op, so any *failed* try_lock() proves the handle reached the real mutex.
+"""
+
+import threading
+
+from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
+
+# A second handle could not take the mutex the first one holds, so the two alias
+# the same real mutex.
+CONTENDED = "second handle contended with the first"
+# Some other thread already held the real mutex, which a no-op cannot do.
+OTHER_HOLDER = "another thread already held the real mutex"
+# Failure: two handles held the mutex at once, so at least one is a no-op.
+UNCONTENDED = "two handles held the real mutex at once"
+
+
+class ContentionCheckFrameProvider(ScriptedFrameProvider):
+    @staticmethod
+    def get_description():
+        return "Provider that checks SBMutex contention from background threads"
+
+    def __init__(self, input_frames, args):
+        super().__init__(input_frames, args)
+        self.artifact_path = None
+        if self.args is not None:
+            value = self.args.GetValueForKey("artifact_path")
+            if value.IsValid():
+                self.artifact_path = value.GetStringValue(4096)
+
+    def _check_contention(self):
+        first = self.target.GetAPIMutex()
+        if not first.try_lock():
+            self._record(OTHER_HOLDER)
+            return
+
+        outcome = [UNCONTENDED]
+
+        def check_from_another_thread():
+            other = self.target.GetAPIMutex()
+            if other.try_lock():
+                other.unlock()
+            else:
+                outcome[0] = CONTENDED
+
+        other_thread = threading.Thread(target=check_from_another_thread)
+        other_thread.start()
+        other_thread.join()
+        first.unlock()
+        self._record(outcome[0])
+
+    def _record(self, outcome):
+        with open(self.artifact_path, "a") as f:
+            f.write(outcome + "\n")
+
+    def get_frame_at_index(self, index):
+        if index >= len(self.input_frames):
+            return None
+
+        if index == 0 and self.artifact_path:
+            # Obtaining a handle locks nothing, so it is safe on this thread;
+            # only the try_lock() calls have to run elsewhere. Every spawned
+            # thread is joined before returning, so nothing holds the mutex
+            # once the bypass ends.
+            checker = threading.Thread(target=self._check_contention)
+            checker.start()
+            checker.join()
+
+        frame = self.input_frames[index]
+        if frame is None:
+            return None
+        return {"idx": index, "pc": frame.GetPC()}
diff --git a/lldb/unittests/Interpreter/CMakeLists.txt b/lldb/unittests/Interpreter/CMakeLists.txt
index 7eec76105aad2..80308b48b806f 100644
--- a/lldb/unittests/Interpreter/CMakeLists.txt
+++ b/lldb/unittests/Interpreter/CMakeLists.txt
@@ -7,6 +7,7 @@ add_lldb_unittest(InterpreterTests
   TestOptionValue.cpp
   TestOptionValueFileColonLine.cpp
   TestRegexCommand.cpp
+  TestScriptedInterface.cpp
 
   LINK_LIBS
       lldbCommands
diff --git a/lldb/unittests/Interpreter/TestScriptedInterface.cpp b/lldb/unittests/Interpreter/TestScriptedInterface.cpp
new file mode 100644
index 0000000000000..48fd98e601e0e
--- /dev/null
+++ b/lldb/unittests/Interpreter/TestScriptedInterface.cpp
@@ -0,0 +1,55 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/Interfaces/ScriptedCommandInterface.h"
+#include "lldb/Interpreter/Interfaces/ScriptedInterface.h"
+#include "gtest/gtest.h"
+
+using namespace lldb_private;
+
+namespace {
+
+class DummyScriptedInterface : public ScriptedInterface {
+public:
+  llvm::SmallVector<AbstractMethodRequirement>
+  GetAbstractMethodRequirements() const override {
+    return {};
+  }
+};
+
+class DummyScriptedCommandInterface : public ScriptedCommandInterface {
+public:
+  llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name,
+                     lldb::DebuggerSP debugger_sp) override {
+    return nullptr;
+  }
+
+  llvm::SmallVector<AbstractMethodRequirement>
+  GetAbstractMethodRequirements() const override {
+    return {};
+  }
+};
+
+} // namespace
+
+TEST(ScriptedInterfaceTest, ExtensionsCannotBeRunDirectly) {
+  DummyScriptedInterface interface;
+  EXPECT_FALSE(interface.UserCanRunDirectly());
+}
+
+TEST(ScriptedInterfaceTest, CommandsCanBeRunDirectly) {
+  DummyScriptedCommandInterface command_interface;
+  EXPECT_TRUE(command_interface.UserCanRunDirectly());
+
+  // The scripted-extension policy is pushed through a ScriptedInterface, so the
+  // override has to be reachable from the base: a command that looks like any
+  // other extension there would silently lose its API mutex.
+  ScriptedInterface &as_base = command_interface;
+  EXPECT_TRUE(as_base.UserCanRunDirectly());
+}
diff --git a/lldb/unittests/Target/TargetAPIMutexTest.cpp b/lldb/unittests/Target/TargetAPIMutexTest.cpp
index 2650723ec6f4e..7311ed0f125e6 100644
--- a/lldb/unittests/Target/TargetAPIMutexTest.cpp
+++ b/lldb/unittests/Target/TargetAPIMutexTest.cpp
@@ -14,6 +14,7 @@
 #include "lldb/Target/Platform.h"
 #include "lldb/Target/Target.h"
 #include "lldb/Utility/ArchSpec.h"
+#include "lldb/Utility/Policy.h"
 #include "gtest/gtest.h"
 
 #include <thread>
@@ -193,3 +194,63 @@ TEST_F(TargetAPIMutexTargetTest, ResolvesFreshOnEachLockCall) {
   contended.join();
   lock.unlock();
 }
+
+TEST_F(TargetAPIMutexTargetTest,
+       UnlockReplaysLockResolutionAcrossPolicyChange) {
+  // lock() and unlock() must agree on which mutex they touch even if the
+  // calling thread's policy changes in between: unlock() replays lock()'s
+  // resolution rather than re-resolving from the current policy.
+  TargetSP target_sp = CreateTarget();
+  ASSERT_TRUE(target_sp);
+
+  TargetAPIMutex lock(target_sp);
+  lock.lock();
+
+  // If unlock() re-resolved here it would see the bypass and skip releasing
+  // the mutex it actually locked.
+  {
+    PolicyStack::Guard guard = PolicyStack::Get().PushScriptedExtensionCall();
+    lock.unlock();
+  }
+
+  // The real mutex must have actually been released: a fresh acquisition
+  // from a different thread (outside the bypass policy) must succeed
+  // immediately. A same-thread try_lock() would pass even if unlock() had
+  // incorrectly no-op'd, since std::recursive_mutex lets the same thread
+  // reenter a lock it still holds.
+  std::thread t([target_sp]() {
+    TargetAPIMutex background_lock(target_sp);
+    EXPECT_TRUE(background_lock.try_lock());
+    background_lock.unlock();
+  });
+  t.join();
+}
+
+TEST_F(TargetAPIMutexTargetTest, BypassPolicyMakesTryLockANoOp) {
+  TargetSP target_sp = CreateTarget();
+  ASSERT_TRUE(target_sp);
+
+  TargetAPIMutex holder(target_sp);
+  holder.lock();
+
+  // The contention has to come from another thread: std::recursive_mutex lets
+  // the owning thread reenter a lock it already holds, so a same-thread
+  // try_lock() would succeed whether or not the bypass is in effect.
+  std::thread contended([target_sp]() {
+    TargetAPIMutex background_lock(target_sp);
+    EXPECT_FALSE(background_lock.try_lock());
+  });
+  contended.join();
+
+  // The bypass touches no primitive, so the same acquisition succeeds while
+  // the real mutex is held elsewhere.
+  std::thread bypassed([target_sp]() {
+    PolicyStack::Guard guard = PolicyStack::Get().PushScriptedExtensionCall();
+    TargetAPIMutex background_lock(target_sp);
+    EXPECT_TRUE(background_lock.try_lock());
+    background_lock.unlock();
+  });
+  bypassed.join();
+
+  holder.unlock();
+}
diff --git a/lldb/unittests/Utility/PolicyTest.cpp b/lldb/unittests/Utility/PolicyTest.cpp
index 5ad045a03d30b..57919b57bdf0a 100644
--- a/lldb/unittests/Utility/PolicyTest.cpp
+++ b/lldb/unittests/Utility/PolicyTest.cpp
@@ -70,6 +70,16 @@ TEST(PolicyTest, PublicStateRunningExpression) {
   EXPECT_TRUE(p.capabilities.can_run_frame_recognizers);
 }
 
+TEST(PolicyTest, ScriptedExtensionCall) {
+  Policy p = Policy::CreateScriptedExtensionCall();
+  EXPECT_TRUE(p.capabilities.can_bypass_target_api_mutex);
+
+  PolicyStack::Guard guard = PolicyStack::Get().PushPrivateState();
+  Policy nested = Policy::CreateScriptedExtensionCall();
+  EXPECT_EQ(nested.view, Policy::View::Private);
+  EXPECT_TRUE(nested.capabilities.can_bypass_target_api_mutex);
+}
+
 TEST(PolicyTest, StackDefaultIsPublicState) {
   Policy current = PolicyStack::Get().Current();
   EXPECT_EQ(current.view, Policy::View::Public);
@@ -145,7 +155,8 @@ TEST(PolicyTest, DumpPublicState) {
   EXPECT_EQ(s.GetString(),
             "policy: view=public, capabilities={"
             "eval_expr=true run_all=true try_all=true "
-            "bp_actions=true frame_providers=true frame_recognizers=true}");
+            "bp_actions=true frame_providers=true frame_recognizers=true "
+            "bypass_api_mutex=false}");
 }
 
 TEST(PolicyTest, DumpPrivateState) {
@@ -154,7 +165,8 @@ TEST(PolicyTest, DumpPrivateState) {
   EXPECT_EQ(s.GetString(),
             "policy: view=private, capabilities={"
             "eval_expr=true run_all=true try_all=true "
-            "bp_actions=true frame_providers=true frame_recognizers=true}");
+            "bp_actions=true frame_providers=true frame_recognizers=true "
+            "bypass_api_mutex=false}");
 }
 
 TEST(PolicyTest, DumpStack) {

>From d101b43997ed5cb5b73da589c0bcafe5832d8cdc Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Tue, 1 Sep 2026 22:46:19 +0100
Subject: [PATCH 16/16] [lldb/test] Suppress crash reports from LLDB's gtest
 death tests (#218981)

PolicyStackDeathTest.GuardDestroyedOnDifferentThread and
ProcessRunLockDeathTest.MoveLockedAcrossThreads deliberately trip
thread-affinity checks that call report_fatal_error, so the forked
death-test child dies via SIGABRT. On platforms with a system crash
reporter that leaves a crash log behind for each run, which CI scrapes
and reports as a test failure even though both tests pass.

Call llvm::sys::Process::PreventCoreFiles() as the first statement
inside the EXPECT_DEATH block. EXPECT_DEATH forks by default, so this
only affects the child process and leaves the runner's own signal
handling alone. The assertions still hold: report_fatal_error writes to
stderr before aborting, so the regex still matches, and the nonzero exit
status satisfies ExitedUnsuccessfully.

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
(cherry picked from commit bea207b72938758a59b499e2aba345d9c882cb3c)
---
 lldb/unittests/Host/ProcessRunLockTest.cpp | 7 +++++++
 lldb/unittests/Utility/PolicyTest.cpp      | 6 ++++++
 2 files changed, 13 insertions(+)

diff --git a/lldb/unittests/Host/ProcessRunLockTest.cpp b/lldb/unittests/Host/ProcessRunLockTest.cpp
index 5a48ad250170c..e797cc13fcfba 100644
--- a/lldb/unittests/Host/ProcessRunLockTest.cpp
+++ b/lldb/unittests/Host/ProcessRunLockTest.cpp
@@ -8,6 +8,8 @@
 
 #include "lldb/Host/ProcessRunLock.h"
 
+#include "llvm/Support/Process.h"
+
 #include "gtest/gtest.h"
 
 #include <condition_variable>
@@ -177,6 +179,11 @@ TEST(ProcessRunLockDeathTest, MoveLockedAcrossThreads) {
   // "ProcessRunLocker" common prefix only.
   EXPECT_DEATH(
       {
+        // The abort below is expected, so keep it away from the system crash
+        // reporter, which would otherwise record it as a real crash. This must
+        // stay inside the death-test statement so only the forked child is
+        // affected.
+        llvm::sys::Process::PreventCoreFiles();
         std::thread t([locker = std::move(a)]() mutable { (void)locker; });
         t.join();
       },
diff --git a/lldb/unittests/Utility/PolicyTest.cpp b/lldb/unittests/Utility/PolicyTest.cpp
index 57919b57bdf0a..66c08f24eb5a5 100644
--- a/lldb/unittests/Utility/PolicyTest.cpp
+++ b/lldb/unittests/Utility/PolicyTest.cpp
@@ -8,6 +8,7 @@
 
 #include "lldb/Utility/Policy.h"
 #include "lldb/Utility/StreamString.h"
+#include "llvm/Support/Process.h"
 #include "gtest/gtest.h"
 
 #include <thread>
@@ -202,6 +203,11 @@ TEST(PolicyStackDeathTest, GuardDestroyedOnDifferentThread) {
   // where the violation is detected.
   EXPECT_DEATH(
       {
+        // The abort below is expected, so keep it away from the system crash
+        // reporter, which would otherwise record it as a real crash. This must
+        // stay inside the death-test statement so only the forked child is
+        // affected.
+        llvm::sys::Process::PreventCoreFiles();
         std::thread t([guard = std::move(outer)]() mutable { (void)guard; });
         t.join();
       },



More information about the llvm-branch-commits mailing list