[Lldb-commits] [lldb] [lldb] Introduce ScriptedFrameProvider for real threads (PR #161870)

Med Ismail Bennani via lldb-commits lldb-commits at lists.llvm.org
Tue Nov 11 03:02:51 PST 2025


https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/161870

>From 39963caab748ae52f0a3847d2f5519dae24fa4fc Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Tue, 11 Nov 2025 03:02:04 -0800
Subject: [PATCH] [lldb] Add support for ScriptedFrame with real threads

This patch extends ScriptedFrame to work with real (non-scripted) threads,
enabling frame providers to synthesize frames for native processes.

Previously, ScriptedFrame only worked within ScriptedProcess/ScriptedThread
contexts. This patch decouples ScriptedFrame from ScriptedThread, allowing
users to augment or replace stack frames in real debugging sessions for use
cases like custom calling conventions, reconstructing corrupted frames from
core files, or adding diagnostic frames.

Key changes:

- ScriptedFrame::Create() now accepts ThreadSP instead of requiring
ScriptedThread, extracting architecture from the target triple rather
than ScriptedProcess.arch

- Added SBTarget::RegisterScriptedFrameProvider() and
ClearScriptedFrameProvider() APIs, with Target storing a
SyntheticFrameProviderDescriptor template for new threads

- Added "target frame-provider register/clear" commands for CLI access

- Thread class gains LoadScriptedFrameProvider(), ClearScriptedFrameProvider(),
and GetFrameProvider() methods for per-thread frame provider management

- New SyntheticStackFrameList overrides FetchFramesUpTo() to lazily provide
frames from either the frame provider or the real stack

This enables practical use of the SyntheticFrameProvider infrastructure in
real debugging workflows.

rdar://161834688

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
---
 lldb/bindings/python/python-wrapper.swig      |  12 +
 .../templates/scripted_frame_provider.py      |  47 +++
 .../python/templates/scripted_process.py      |  47 ++-
 lldb/include/lldb/API/SBTarget.h              |  30 ++
 lldb/include/lldb/API/SBThread.h              |   1 +
 lldb/include/lldb/API/SBThreadCollection.h    |   1 +
 .../ScriptedFrameProviderInterface.h          |   7 +
 .../lldb/Interpreter/ScriptInterpreter.h      |   3 +
 lldb/include/lldb/Target/StackFrame.h         |   7 +-
 lldb/include/lldb/Target/StackFrameList.h     |  36 +-
 .../lldb/Target/SyntheticFrameProvider.h      |  26 +-
 lldb/include/lldb/Target/Target.h             |  35 ++
 lldb/include/lldb/Target/Thread.h             |  12 +
 lldb/include/lldb/Target/ThreadSpec.h         |   2 +
 lldb/include/lldb/Utility/ScriptedMetadata.h  |  27 ++
 lldb/include/lldb/lldb-private-interfaces.h   |   4 +-
 lldb/source/API/SBTarget.cpp                  |  82 +++++
 lldb/source/Commands/CommandObjectTarget.cpp  | 197 ++++++++++
 lldb/source/Interpreter/ScriptInterpreter.cpp |   7 +
 lldb/source/Plugins/CMakeLists.txt            |   1 +
 .../Process/scripted/ScriptedFrame.cpp        |  85 +++--
 .../Plugins/Process/scripted/ScriptedFrame.h  |  33 +-
 .../Process/scripted/ScriptedThread.cpp       |   6 +-
 .../ScriptInterpreterPythonInterfaces.cpp     |   2 +
 .../ScriptedFrameProviderPythonInterface.cpp  |  56 +++
 .../ScriptedFrameProviderPythonInterface.h    |  23 +-
 .../Interfaces/ScriptedPythonInterface.cpp    |  13 +
 .../Interfaces/ScriptedPythonInterface.h      | 111 ++++++
 .../Python/SWIGPythonBridge.h                 |   1 +
 .../SyntheticFrameProvider/CMakeLists.txt     |   1 +
 .../ScriptedFrameProvider/CMakeLists.txt      |  12 +
 .../ScriptedFrameProvider.cpp                 | 215 +++++++++++
 .../ScriptedFrameProvider.h                   |  53 +++
 lldb/source/Target/StackFrameList.cpp         |  32 ++
 lldb/source/Target/SyntheticFrameProvider.cpp |  25 +-
 lldb/source/Target/Target.cpp                 |  55 +++
 lldb/source/Target/Thread.cpp                 |  72 +++-
 lldb/source/Target/ThreadSpec.cpp             |   4 +
 .../scripted_frame_provider/Makefile          |   3 +
 .../TestScriptedFrameProvider.py              | 339 ++++++++++++++++++
 .../scripted_frame_provider/main.cpp          |  58 +++
 .../test_frame_providers.py                   | 176 +++++++++
 .../Python/PythonTestSuite.cpp                |   5 +
 43 files changed, 1891 insertions(+), 73 deletions(-)
 create mode 100644 lldb/source/Plugins/SyntheticFrameProvider/CMakeLists.txt
 create mode 100644 lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/CMakeLists.txt
 create mode 100644 lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/ScriptedFrameProvider.cpp
 create mode 100644 lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/ScriptedFrameProvider.h
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/Makefile
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/main.cpp
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py

diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig
index 3a0995e84f643..84fb3a95c0942 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -422,6 +422,18 @@ void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBBreakpoint(PyObject *
   return sb_ptr;
 }
 
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBThread(PyObject * data) {
+  lldb::SBThread *sb_ptr = nullptr;
+
+  int valid_cast =
+      SWIG_ConvertPtr(data, (void **)&sb_ptr, SWIGTYPE_p_lldb__SBThread, 0);
+
+  if (valid_cast == -1)
+    return NULL;
+
+  return sb_ptr;
+}
+
 void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFrame(PyObject * data) {
   lldb::SBFrame *sb_ptr = nullptr;
 
diff --git a/lldb/examples/python/templates/scripted_frame_provider.py b/lldb/examples/python/templates/scripted_frame_provider.py
index 20f4d76d188c2..1954d990eae1d 100644
--- a/lldb/examples/python/templates/scripted_frame_provider.py
+++ b/lldb/examples/python/templates/scripted_frame_provider.py
@@ -31,6 +31,53 @@ class ScriptedFrameProvider(metaclass=ABCMeta):
         )
     """
 
+    @staticmethod
+    def applies_to_thread(thread):
+        """Determine if this frame provider should be used for a given thread.
+
+        This static method is called before creating an instance of the frame
+        provider to determine if it should be applied to a specific thread.
+        Override this method to provide custom filtering logic.
+
+        Args:
+            thread (lldb.SBThread): The thread to check.
+
+        Returns:
+            bool: True if this frame provider should be used for the thread,
+                False otherwise. The default implementation returns True for
+                all threads.
+
+        Example:
+
+        .. code-block:: python
+
+            @staticmethod
+            def applies_to_thread(thread):
+                # Only apply to thread 1
+                return thread.GetIndexID() == 1
+        """
+        return True
+
+    @abstractmethod
+    def get_description(self):
+        """Get a description of this frame provider.
+
+        This method should return a human-readable string describing what
+        this frame provider does. The description is used for debugging
+        and display purposes.
+
+        Returns:
+            str: A description of the frame provider.
+
+        Example:
+
+        .. code-block:: python
+
+            def get_description(self):
+                return "Crash log frame provider for thread 1"
+        """
+        pass
+
     @abstractmethod
     def __init__(self, input_frames, args):
         """Construct a scripted frame provider.
diff --git a/lldb/examples/python/templates/scripted_process.py b/lldb/examples/python/templates/scripted_process.py
index 49059d533f38a..136edce165140 100644
--- a/lldb/examples/python/templates/scripted_process.py
+++ b/lldb/examples/python/templates/scripted_process.py
@@ -245,6 +245,7 @@ def __init__(self, process, args):
                 key/value pairs used by the scripted thread.
         """
         self.target = None
+        self.arch = None
         self.originating_process = None
         self.process = None
         self.args = None
@@ -266,6 +267,9 @@ def __init__(self, process, args):
             and process.IsValid()
         ):
             self.target = process.target
+            triple = self.target.triple
+            if triple:
+                self.arch = triple.split("-")[0]
             self.originating_process = process
             self.process = self.target.GetProcess()
             self.get_register_info()
@@ -352,17 +356,14 @@ def get_stackframes(self):
     def get_register_info(self):
         if self.register_info is None:
             self.register_info = dict()
-            if "x86_64" in self.originating_process.arch:
+            if "x86_64" in self.arch:
                 self.register_info["sets"] = ["General Purpose Registers"]
                 self.register_info["registers"] = INTEL64_GPR
-            elif (
-                "arm64" in self.originating_process.arch
-                or self.originating_process.arch == "aarch64"
-            ):
+            elif "arm64" in self.arch or self.arch == "aarch64":
                 self.register_info["sets"] = ["General Purpose Registers"]
                 self.register_info["registers"] = ARM64_GPR
             else:
-                raise ValueError("Unknown architecture", self.originating_process.arch)
+                raise ValueError("Unknown architecture", self.arch)
         return self.register_info
 
     @abstractmethod
@@ -405,11 +406,12 @@ def __init__(self, thread, args):
         """Construct a scripted frame.
 
         Args:
-            thread (ScriptedThread): The thread owning this frame.
+            thread (ScriptedThread/lldb.SBThread): The thread owning this frame.
             args (lldb.SBStructuredData): A Dictionary holding arbitrary
                 key/value pairs used by the scripted frame.
         """
         self.target = None
+        self.arch = None
         self.originating_thread = None
         self.thread = None
         self.args = None
@@ -419,15 +421,17 @@ def __init__(self, thread, args):
         self.register_ctx = {}
         self.variables = []
 
-        if (
-            isinstance(thread, ScriptedThread)
-            or isinstance(thread, lldb.SBThread)
-            and thread.IsValid()
+        if isinstance(thread, ScriptedThread) or (
+            isinstance(thread, lldb.SBThread) and thread.IsValid()
         ):
-            self.target = thread.target
             self.process = thread.process
+            self.target = self.process.target
+            triple = self.target.triple
+            if triple:
+                self.arch = triple.split("-")[0]
+            tid = thread.tid if isinstance(thread, ScriptedThread) else thread.id
             self.originating_thread = thread
-            self.thread = self.process.GetThreadByIndexID(thread.tid)
+            self.thread = self.process.GetThreadByIndexID(tid)
             self.get_register_info()
 
     @abstractmethod
@@ -508,7 +512,18 @@ def get_variables(self, filters):
 
     def get_register_info(self):
         if self.register_info is None:
-            self.register_info = self.originating_thread.get_register_info()
+            if isinstance(self.originating_thread, ScriptedThread):
+                self.register_info = self.originating_thread.get_register_info()
+            elif isinstance(self.originating_thread, lldb.SBThread):
+                self.register_info = dict()
+                if "x86_64" in self.arch:
+                    self.register_info["sets"] = ["General Purpose Registers"]
+                    self.register_info["registers"] = INTEL64_GPR
+                elif "arm64" in self.arch or self.arch == "aarch64":
+                    self.register_info["sets"] = ["General Purpose Registers"]
+                    self.register_info["registers"] = ARM64_GPR
+                else:
+                    raise ValueError("Unknown architecture", self.arch)
         return self.register_info
 
     @abstractmethod
@@ -642,12 +657,12 @@ def get_stop_reason(self):
 
             # TODO: Passthrough stop reason from driving process
             if self.driving_thread.GetStopReason() != lldb.eStopReasonNone:
-                if "arm64" in self.originating_process.arch:
+                if "arm64" in self.arch:
                     stop_reason["type"] = lldb.eStopReasonException
                     stop_reason["data"]["desc"] = (
                         self.driving_thread.GetStopDescription(100)
                     )
-                elif self.originating_process.arch == "x86_64":
+                elif self.arch == "x86_64":
                     stop_reason["type"] = lldb.eStopReasonSignal
                     stop_reason["data"]["signal"] = signal.SIGTRAP
                 else:
diff --git a/lldb/include/lldb/API/SBTarget.h b/lldb/include/lldb/API/SBTarget.h
index 173fd05b54a13..fe74adaeec362 100644
--- a/lldb/include/lldb/API/SBTarget.h
+++ b/lldb/include/lldb/API/SBTarget.h
@@ -19,6 +19,7 @@
 #include "lldb/API/SBLaunchInfo.h"
 #include "lldb/API/SBStatisticsOptions.h"
 #include "lldb/API/SBSymbolContextList.h"
+#include "lldb/API/SBThreadCollection.h"
 #include "lldb/API/SBType.h"
 #include "lldb/API/SBValue.h"
 #include "lldb/API/SBWatchpoint.h"
@@ -986,6 +987,35 @@ class LLDB_API SBTarget {
 
   lldb::SBMutex GetAPIMutex() const;
 
+  /// Register a scripted frame provider for this target.
+  /// If a scripted frame provider with the same name and same argument
+  /// dictionary is already registered on this target, it will be overwritten.
+  ///
+  /// \param[in] class_name
+  ///     The name of the Python class that implements the frame provider.
+  ///
+  /// \param[in] args_dict
+  ///     A dictionary of arguments to pass to the frame provider class.
+  ///
+  /// \param[out] error
+  ///     An error object indicating success or failure.
+  ///
+  /// \return
+  ///     A unique identifier for the frame provider descriptor that was
+  ///     registered. 0 if the registration failed.
+  uint32_t RegisterScriptedFrameProvider(const char *class_name,
+                                         lldb::SBStructuredData args_dict,
+                                         lldb::SBError &error);
+
+  /// Remove a scripted frame provider from this target by name.
+  ///
+  /// \param[in] provider_id
+  ///     The id of the frame provider class to remove.
+  ///
+  /// \return
+  ///     An error object indicating success or failure.
+  lldb::SBError RemoveScriptedFrameProvider(uint32_t provider_id);
+
 protected:
   friend class SBAddress;
   friend class SBAddressRange;
diff --git a/lldb/include/lldb/API/SBThread.h b/lldb/include/lldb/API/SBThread.h
index f6a6d19935b83..639e7a0a1a5c0 100644
--- a/lldb/include/lldb/API/SBThread.h
+++ b/lldb/include/lldb/API/SBThread.h
@@ -256,6 +256,7 @@ class LLDB_API SBThread {
   friend class SBThreadPlan;
   friend class SBTrace;
 
+  friend class lldb_private::ScriptInterpreter;
   friend class lldb_private::python::SWIGBridge;
 
   SBThread(const lldb::ThreadSP &lldb_object_sp);
diff --git a/lldb/include/lldb/API/SBThreadCollection.h b/lldb/include/lldb/API/SBThreadCollection.h
index 5a052e6246026..d13dea0f11cd2 100644
--- a/lldb/include/lldb/API/SBThreadCollection.h
+++ b/lldb/include/lldb/API/SBThreadCollection.h
@@ -46,6 +46,7 @@ class LLDB_API SBThreadCollection {
   void SetOpaque(const lldb::ThreadCollectionSP &threads);
 
 private:
+  friend class SBTarget;
   friend class SBProcess;
   friend class SBThread;
   friend class SBSaveCoreOptions;
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
index 2d9f713676f90..3fa1a8d25b5db 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
@@ -16,11 +16,18 @@
 namespace lldb_private {
 class ScriptedFrameProviderInterface : public ScriptedInterface {
 public:
+  virtual bool AppliesToThread(llvm::StringRef class_name,
+                               lldb::ThreadSP thread_sp) {
+    return true;
+  }
+
   virtual llvm::Expected<StructuredData::GenericSP>
   CreatePluginObject(llvm::StringRef class_name,
                      lldb::StackFrameListSP input_frames,
                      StructuredData::DictionarySP args_sp) = 0;
 
+  virtual std::string GetDescription(llvm::StringRef class_name) { return {}; }
+
   virtual StructuredData::ObjectSP GetFrameAtIndex(uint32_t index) {
     return {};
   }
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 7fed4940b85bf..0b91d6756552d 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -21,6 +21,7 @@
 #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"
@@ -580,6 +581,8 @@ class ScriptInterpreter : public PluginInterface {
 
   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
diff --git a/lldb/include/lldb/Target/StackFrame.h b/lldb/include/lldb/Target/StackFrame.h
index cdbe8ae3c6779..af2e49b4a67da 100644
--- a/lldb/include/lldb/Target/StackFrame.h
+++ b/lldb/include/lldb/Target/StackFrame.h
@@ -441,8 +441,11 @@ class StackFrame : public ExecutionContextScope,
   ///   frames are included in this frame index count.
   uint32_t GetFrameIndex() const;
 
-  /// Set this frame's synthetic frame index.
-  void SetFrameIndex(uint32_t index) { m_frame_index = index; }
+  /// Set this frame's frame index.
+  void SetFrameIndex(uint32_t index) {
+    m_frame_index = index;
+    m_concrete_frame_index = index;
+  }
 
   /// Query this frame to find what frame it is in this Thread's
   /// StackFrameList, not counting inlined frames.
diff --git a/lldb/include/lldb/Target/StackFrameList.h b/lldb/include/lldb/Target/StackFrameList.h
index 5b0df0ddb3e29..539c070ff0f4b 100644
--- a/lldb/include/lldb/Target/StackFrameList.h
+++ b/lldb/include/lldb/Target/StackFrameList.h
@@ -20,13 +20,13 @@ namespace lldb_private {
 
 class ScriptedThread;
 
-class StackFrameList {
+class StackFrameList : public std::enable_shared_from_this<StackFrameList> {
 public:
   // Constructors and Destructors
   StackFrameList(Thread &thread, const lldb::StackFrameListSP &prev_frames_sp,
                  bool show_inline_frames);
 
-  ~StackFrameList();
+  virtual ~StackFrameList();
 
   /// Get the number of visible frames. Frames may be created if \p can_create
   /// is true. Synthetic (inline) frames expanded from the concrete frame #0
@@ -106,6 +106,7 @@ class StackFrameList {
 
 protected:
   friend class Thread;
+  friend class ScriptedFrameProvider;
   friend class ScriptedThread;
 
   /// Use this API to build a stack frame list (used for scripted threads, for
@@ -211,19 +212,23 @@ class StackFrameList {
   /// Whether or not to show synthetic (inline) frames. Immutable.
   const bool m_show_inlined_frames;
 
+  /// Returns true if fetching frames was interrupted, false otherwise.
+  virtual bool FetchFramesUpTo(uint32_t end_idx,
+                               InterruptionControl allow_interrupt);
+
 private:
   uint32_t SetSelectedFrameNoLock(lldb_private::StackFrame *frame);
   lldb::StackFrameSP
   GetFrameAtIndexNoLock(uint32_t idx,
                         std::shared_lock<std::shared_mutex> &guard);
 
+  /// @{
   /// These two Fetch frames APIs and SynthesizeTailCallFrames are called in
   /// GetFramesUpTo, they are the ones that actually add frames.  They must be
   /// called with the writer end of the list mutex held.
-
-  /// Returns true if fetching frames was interrupted, false otherwise.
-  bool FetchFramesUpTo(uint32_t end_idx, InterruptionControl allow_interrupt);
+  ///
   /// Not currently interruptible so returns void.
+  /// }@
   void FetchOnlyConcreteFramesUpTo(uint32_t end_idx);
   void SynthesizeTailCallFrames(StackFrame &next_frame);
 
@@ -231,6 +236,27 @@ class StackFrameList {
   const StackFrameList &operator=(const StackFrameList &) = delete;
 };
 
+/// A StackFrameList that wraps another StackFrameList and uses a
+/// SyntheticFrameProvider to lazily provide frames from either the provider
+/// or the underlying real stack frame list.
+class SyntheticStackFrameList : public StackFrameList {
+public:
+  SyntheticStackFrameList(Thread &thread, lldb::StackFrameListSP input_frames,
+                          const lldb::StackFrameListSP &prev_frames_sp,
+                          bool show_inline_frames);
+
+protected:
+  /// Override FetchFramesUpTo to lazily return frames from the provider
+  /// or from the actual stack frame list.
+  bool FetchFramesUpTo(uint32_t end_idx,
+                       InterruptionControl allow_interrupt) override;
+
+private:
+  /// The input stack frame list that the provider transforms.
+  /// This could be a real StackFrameList or another SyntheticStackFrameList.
+  lldb::StackFrameListSP m_input_frames;
+};
+
 } // namespace lldb_private
 
 #endif // LLDB_TARGET_STACKFRAMELIST_H
diff --git a/lldb/include/lldb/Target/SyntheticFrameProvider.h b/lldb/include/lldb/Target/SyntheticFrameProvider.h
index 61a492f356ece..d41c3f73515f6 100644
--- a/lldb/include/lldb/Target/SyntheticFrameProvider.h
+++ b/lldb/include/lldb/Target/SyntheticFrameProvider.h
@@ -24,22 +24,25 @@ namespace lldb_private {
 
 /// This struct contains the metadata needed to instantiate a frame provider
 /// and optional filters to control which threads it applies to.
-struct SyntheticFrameProviderDescriptor {
+struct ScriptedFrameProviderDescriptor {
   /// Metadata for instantiating the provider (e.g. script class name and args).
   lldb::ScriptedMetadataSP scripted_metadata_sp;
 
+  /// Interface for calling static methods on the provider class.
+  lldb::ScriptedFrameProviderInterfaceSP interface_sp;
+
   /// Optional list of thread specifications to which this provider applies.
   /// If empty, the provider applies to all threads. A thread matches if it
   /// satisfies ANY of the specs in this vector (OR logic).
   std::vector<ThreadSpec> thread_specs;
 
-  SyntheticFrameProviderDescriptor() = default;
+  ScriptedFrameProviderDescriptor() = default;
 
-  SyntheticFrameProviderDescriptor(lldb::ScriptedMetadataSP metadata_sp)
+  ScriptedFrameProviderDescriptor(lldb::ScriptedMetadataSP metadata_sp)
       : scripted_metadata_sp(metadata_sp) {}
 
-  SyntheticFrameProviderDescriptor(lldb::ScriptedMetadataSP metadata_sp,
-                                   const std::vector<ThreadSpec> &specs)
+  ScriptedFrameProviderDescriptor(lldb::ScriptedMetadataSP metadata_sp,
+                                  const std::vector<ThreadSpec> &specs)
       : scripted_metadata_sp(metadata_sp), thread_specs(specs) {}
 
   /// Get the name of this descriptor (the scripted class name).
@@ -47,6 +50,8 @@ struct SyntheticFrameProviderDescriptor {
     return scripted_metadata_sp ? scripted_metadata_sp->GetClassName() : "";
   }
 
+  std::string GetDescription() const;
+
   /// Check if this descriptor applies to the given thread.
   bool AppliesToThread(Thread &thread) const {
     // If no thread specs specified, applies to all threads.
@@ -64,6 +69,13 @@ struct SyntheticFrameProviderDescriptor {
   /// Check if this descriptor has valid metadata for script-based providers.
   bool IsValid() const { return scripted_metadata_sp != nullptr; }
 
+  /// Get a unique identifier for this descriptor based on its contents.
+  /// The ID is computed from the class name and arguments dictionary,
+  /// not from the pointer address, so two descriptors with the same
+  /// contents will have the same ID.
+  uint32_t GetID() const;
+
+  /// Dump a description of this descriptor to the given stream.
   void Dump(Stream *s) const;
 };
 
@@ -95,7 +107,7 @@ class SyntheticFrameProvider : public PluginInterface {
   ///     otherwise an \a llvm::Error.
   static llvm::Expected<lldb::SyntheticFrameProviderSP>
   CreateInstance(lldb::StackFrameListSP input_frames,
-                 const SyntheticFrameProviderDescriptor &descriptor);
+                 const ScriptedFrameProviderDescriptor &descriptor);
 
   /// Try to create a SyntheticFrameProvider instance for the given input
   /// frames using a specific C++ plugin.
@@ -125,6 +137,8 @@ class SyntheticFrameProvider : public PluginInterface {
 
   ~SyntheticFrameProvider() override;
 
+  virtual std::string GetDescription() const = 0;
+
   /// Get a single stack frame at the specified index.
   ///
   /// This method is called lazily - frames are only created when requested.
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index 40f9c9bea1c12..c4dd2f08a4a44 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -32,6 +32,7 @@
 #include "lldb/Target/PathMappingList.h"
 #include "lldb/Target/SectionLoadHistory.h"
 #include "lldb/Target/Statistics.h"
+#include "lldb/Target/SyntheticFrameProvider.h"
 #include "lldb/Target/ThreadSpec.h"
 #include "lldb/Utility/ArchSpec.h"
 #include "lldb/Utility/Broadcaster.h"
@@ -697,6 +698,33 @@ class Target : public std::enable_shared_from_this<Target>,
   Status Attach(ProcessAttachInfo &attach_info,
                 Stream *stream); // Optional stream to receive first stop info
 
+  /// Add or update a scripted frame provider descriptor for this target.
+  /// The descriptor's name is extracted from its scripted metadata class name.
+  /// All new threads in this target will check if they match any descriptors
+  /// to create their frame providers.
+  ///
+  /// \param[in] descriptor
+  ///     The descriptor to add or update.
+  llvm::Expected<uint32_t> AddScriptedFrameProviderDescriptor(
+      const ScriptedFrameProviderDescriptor &descriptor);
+
+  /// Remove a scripted frame provider descriptor by name.
+  ///
+  /// \param[in] id
+  ///     The id of the descriptor to remove.
+  ///
+  /// \return
+  ///     True if a descriptor was removed, false if no descriptor with that
+  ///     name existed.
+  bool RemoveScriptedFrameProviderDescriptor(uint32_t id);
+
+  /// Clear all scripted frame provider descriptors for this target.
+  void ClearScriptedFrameProviderDescriptors();
+
+  /// Get all scripted frame provider descriptors for this target.
+  const llvm::DenseMap<uint32_t, ScriptedFrameProviderDescriptor> &
+  GetScriptedFrameProviderDescriptors() const;
+
   // This part handles the breakpoints.
 
   BreakpointList &GetBreakpointList(bool internal = false);
@@ -1689,6 +1717,13 @@ class Target : public std::enable_shared_from_this<Target>,
   PathMappingList m_image_search_paths;
   TypeSystemMap m_scratch_type_system_map;
 
+  /// Map of scripted frame provider descriptors for this target.
+  /// Keys are the provider descriptors ids, values are the descriptors.
+  /// Used to initialize frame providers for new threads.
+  llvm::DenseMap<uint32_t, ScriptedFrameProviderDescriptor>
+      m_frame_provider_descriptors;
+  mutable std::recursive_mutex m_frame_provider_descriptors_mutex;
+
   typedef std::map<lldb::LanguageType, lldb::REPLSP> REPLMap;
   REPLMap m_repl_map;
 
diff --git a/lldb/include/lldb/Target/Thread.h b/lldb/include/lldb/Target/Thread.h
index 841f80cd1b1eb..46ce192556756 100644
--- a/lldb/include/lldb/Target/Thread.h
+++ b/lldb/include/lldb/Target/Thread.h
@@ -1297,6 +1297,15 @@ class Thread : public std::enable_shared_from_this<Thread>,
 
   lldb::StackFrameListSP GetStackFrameList();
 
+  llvm::Error
+  LoadScriptedFrameProvider(const ScriptedFrameProviderDescriptor &descriptor);
+
+  void ClearScriptedFrameProvider();
+
+  lldb::SyntheticFrameProviderSP GetFrameProvider() const {
+    return m_frame_provider_sp;
+  }
+
 protected:
   friend class ThreadPlan;
   friend class ThreadList;
@@ -1400,6 +1409,9 @@ class Thread : public std::enable_shared_from_this<Thread>,
   /// The Thread backed by this thread, if any.
   lldb::ThreadWP m_backed_thread;
 
+  /// The Scripted Frame Provider, if any.
+  lldb::SyntheticFrameProviderSP m_frame_provider_sp;
+
 private:
   bool m_extended_info_fetched; // Have we tried to retrieve the m_extended_info
                                 // for this thread?
diff --git a/lldb/include/lldb/Target/ThreadSpec.h b/lldb/include/lldb/Target/ThreadSpec.h
index 7c7c832741196..63f8f8b5ec181 100644
--- a/lldb/include/lldb/Target/ThreadSpec.h
+++ b/lldb/include/lldb/Target/ThreadSpec.h
@@ -34,6 +34,8 @@ class ThreadSpec {
 public:
   ThreadSpec();
 
+  ThreadSpec(Thread &thread);
+
   static std::unique_ptr<ThreadSpec>
   CreateFromStructuredData(const StructuredData::Dictionary &data_dict,
                            Status &error);
diff --git a/lldb/include/lldb/Utility/ScriptedMetadata.h b/lldb/include/lldb/Utility/ScriptedMetadata.h
index 69c83edce909a..8523c95429718 100644
--- a/lldb/include/lldb/Utility/ScriptedMetadata.h
+++ b/lldb/include/lldb/Utility/ScriptedMetadata.h
@@ -10,7 +10,9 @@
 #define LLDB_INTERPRETER_SCRIPTEDMETADATA_H
 
 #include "lldb/Utility/ProcessInfo.h"
+#include "lldb/Utility/StreamString.h"
 #include "lldb/Utility/StructuredData.h"
+#include "llvm/ADT/Hashing.h"
 
 namespace lldb_private {
 class ScriptedMetadata {
@@ -27,11 +29,36 @@ class ScriptedMetadata {
     }
   }
 
+  ScriptedMetadata(const ScriptedMetadata &other)
+      : m_class_name(other.m_class_name), m_args_sp(other.m_args_sp) {}
+
   explicit operator bool() const { return !m_class_name.empty(); }
 
   llvm::StringRef GetClassName() const { return m_class_name; }
   StructuredData::DictionarySP GetArgsSP() const { return m_args_sp; }
 
+  /// Get a unique identifier for this metadata based on its contents.
+  /// The ID is computed from the class name and arguments dictionary,
+  /// not from the pointer address, so two metadata objects with the same
+  /// contents will have the same ID.
+  uint32_t GetID() const {
+    if (m_class_name.empty())
+      return 0;
+
+    // Hash the class name.
+    llvm::hash_code hash = llvm::hash_value(m_class_name);
+
+    // Hash the arguments dictionary if present.
+    if (m_args_sp) {
+      StreamString ss;
+      m_args_sp->GetDescription(ss);
+      hash = llvm::hash_combine(hash, llvm::hash_value(ss.GetData()));
+    }
+
+    // Return the lower 32 bits of the hash.
+    return static_cast<uint32_t>(hash);
+  }
+
 private:
   std::string m_class_name;
   StructuredData::DictionarySP m_args_sp;
diff --git a/lldb/include/lldb/lldb-private-interfaces.h b/lldb/include/lldb/lldb-private-interfaces.h
index 2fe3af7c62e00..34a1f2c2732f9 100644
--- a/lldb/include/lldb/lldb-private-interfaces.h
+++ b/lldb/include/lldb/lldb-private-interfaces.h
@@ -25,7 +25,7 @@ class Value;
 
 namespace lldb_private {
 class ScriptedInterfaceUsages;
-struct SyntheticFrameProviderDescriptor;
+struct ScriptedFrameProviderDescriptor;
 typedef lldb::ABISP (*ABICreateInstance)(lldb::ProcessSP process_sp,
                                          const ArchSpec &arch);
 typedef std::unique_ptr<Architecture> (*ArchitectureCreateInstance)(
@@ -90,7 +90,7 @@ typedef lldb::ScriptInterpreterSP (*ScriptInterpreterCreateInstance)(
 typedef llvm::Expected<lldb::SyntheticFrameProviderSP> (
     *ScriptedFrameProviderCreateInstance)(
     lldb::StackFrameListSP input_frames,
-    const lldb_private::SyntheticFrameProviderDescriptor &descriptor);
+    const lldb_private::ScriptedFrameProviderDescriptor &descriptor);
 typedef llvm::Expected<lldb::SyntheticFrameProviderSP> (
     *SyntheticFrameProviderCreateInstance)(
     lldb::StackFrameListSP input_frames,
diff --git a/lldb/source/API/SBTarget.cpp b/lldb/source/API/SBTarget.cpp
index 98d10aa07c53f..bb1d98b6e15c1 100644
--- a/lldb/source/API/SBTarget.cpp
+++ b/lldb/source/API/SBTarget.cpp
@@ -23,6 +23,7 @@
 #include "lldb/API/SBStringList.h"
 #include "lldb/API/SBStructuredData.h"
 #include "lldb/API/SBSymbolContextList.h"
+#include "lldb/API/SBThreadCollection.h"
 #include "lldb/API/SBTrace.h"
 #include "lldb/Breakpoint/BreakpointID.h"
 #include "lldb/Breakpoint/BreakpointIDList.h"
@@ -39,6 +40,7 @@
 #include "lldb/Core/Section.h"
 #include "lldb/Core/StructuredDataImpl.h"
 #include "lldb/Host/Host.h"
+#include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
 #include "lldb/Symbol/DeclVendor.h"
 #include "lldb/Symbol/ObjectFile.h"
 #include "lldb/Symbol/SymbolFile.h"
@@ -50,6 +52,7 @@
 #include "lldb/Target/LanguageRuntime.h"
 #include "lldb/Target/Process.h"
 #include "lldb/Target/StackFrame.h"
+#include "lldb/Target/SyntheticFrameProvider.h"
 #include "lldb/Target/Target.h"
 #include "lldb/Target/TargetList.h"
 #include "lldb/Utility/ArchSpec.h"
@@ -59,6 +62,7 @@
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/ProcessInfo.h"
 #include "lldb/Utility/RegularExpression.h"
+#include "lldb/Utility/ScriptedMetadata.h"
 #include "lldb/ValueObject/ValueObjectConstResult.h"
 #include "lldb/ValueObject/ValueObjectList.h"
 #include "lldb/ValueObject/ValueObjectVariable.h"
@@ -2408,3 +2412,81 @@ lldb::SBMutex SBTarget::GetAPIMutex() const {
     return lldb::SBMutex(target_sp);
   return lldb::SBMutex();
 }
+
+uint32_t
+SBTarget::RegisterScriptedFrameProvider(const char *class_name,
+                                        lldb::SBStructuredData args_dict,
+                                        lldb::SBError &error) {
+  LLDB_INSTRUMENT_VA(this, class_name, args_dict, error);
+
+  TargetSP target_sp = GetSP();
+  if (!target_sp) {
+    error.SetErrorString("invalid target");
+    return 0;
+  }
+
+  if (!class_name || !class_name[0]) {
+    error.SetErrorString("invalid class name");
+    return 0;
+  }
+
+  // Extract the dictionary from SBStructuredData.
+  StructuredData::DictionarySP dict_sp;
+  if (args_dict.IsValid() && args_dict.m_impl_up) {
+    StructuredData::ObjectSP obj_sp = args_dict.m_impl_up->GetObjectSP();
+    if (obj_sp && obj_sp->GetType() != lldb::eStructuredDataTypeDictionary) {
+      error.SetErrorString("SBStructuredData argument isn't a dictionary");
+      return 0;
+    }
+    dict_sp = std::make_shared<StructuredData::Dictionary>(obj_sp);
+  }
+
+  // Create the ScriptedMetadata.
+  ScriptedMetadataSP metadata_sp =
+      std::make_shared<ScriptedMetadata>(class_name, dict_sp);
+
+  // Create the interface for calling static methods.
+  ScriptedFrameProviderInterfaceSP interface_sp =
+      target_sp->GetDebugger()
+          .GetScriptInterpreter()
+          ->CreateScriptedFrameProviderInterface();
+
+  // Create a descriptor (applies to all threads by default).
+  ScriptedFrameProviderDescriptor descriptor(metadata_sp);
+  descriptor.interface_sp = interface_sp;
+
+  llvm::Expected<uint32_t> descriptor_id_or_err =
+      target_sp->AddScriptedFrameProviderDescriptor(descriptor);
+  if (!descriptor_id_or_err) {
+    error.SetErrorString(
+        llvm::toString(descriptor_id_or_err.takeError()).c_str());
+    return 0;
+  }
+
+  // Register the descriptor with the target.
+  return *descriptor_id_or_err;
+}
+
+lldb::SBError SBTarget::RemoveScriptedFrameProvider(uint32_t provider_id) {
+  LLDB_INSTRUMENT_VA(this, provider_id);
+
+  SBError error;
+  TargetSP target_sp = GetSP();
+  if (!target_sp) {
+    error.SetErrorString("invalid target");
+    return error;
+  }
+
+  if (!provider_id) {
+    error.SetErrorString("invalid provider id");
+    return error;
+  }
+
+  if (!target_sp->RemoveScriptedFrameProviderDescriptor(provider_id)) {
+    error.SetErrorStringWithFormat("no frame provider named '%u' found",
+                                   provider_id);
+    return error;
+  }
+
+  return {};
+}
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp
index 8de6521e65b25..c2e2f26bd1e05 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -51,6 +51,7 @@
 #include "lldb/Utility/ConstString.h"
 #include "lldb/Utility/FileSpec.h"
 #include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/ScriptedMetadata.h"
 #include "lldb/Utility/State.h"
 #include "lldb/Utility/Stream.h"
 #include "lldb/Utility/StructuredData.h"
@@ -5392,6 +5393,199 @@ class CommandObjectTargetDump : public CommandObjectMultiword {
   ~CommandObjectTargetDump() override = default;
 };
 
+#pragma mark CommandObjectTargetFrameProvider
+
+#define LLDB_OPTIONS_target_frame_provider_register
+#include "CommandOptions.inc"
+
+class CommandObjectTargetFrameProviderRegister : public CommandObjectParsed {
+public:
+  CommandObjectTargetFrameProviderRegister(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "target frame-provider register",
+            "Register frame provider for all threads in this target.", nullptr,
+            eCommandRequiresTarget),
+
+        m_class_options("target frame-provider", true, 'C', 'k', 'v', 0) {
+    m_all_options.Append(&m_class_options, LLDB_OPT_SET_1 | LLDB_OPT_SET_2,
+                         LLDB_OPT_SET_ALL);
+    m_all_options.Finalize();
+
+    AddSimpleArgumentList(eArgTypeRunArgs, eArgRepeatOptional);
+  }
+
+  ~CommandObjectTargetFrameProviderRegister() override = default;
+
+  Options *GetOptions() override { return &m_all_options; }
+
+  std::optional<std::string> GetRepeatCommand(Args &current_command_args,
+                                              uint32_t index) override {
+    return std::string("");
+  }
+
+protected:
+  void DoExecute(Args &launch_args, CommandReturnObject &result) override {
+    ScriptedMetadataSP metadata_sp = std::make_shared<ScriptedMetadata>(
+        m_class_options.GetName(), m_class_options.GetStructuredData());
+
+    Target *target = m_exe_ctx.GetTargetPtr();
+    if (!target)
+      target = &GetDebugger().GetDummyTarget();
+
+    // Create the interface for calling static methods.
+    ScriptedFrameProviderInterfaceSP interface_sp =
+        GetDebugger()
+            .GetScriptInterpreter()
+            ->CreateScriptedFrameProviderInterface();
+
+    // Create a descriptor from the metadata (applies to all threads by
+    // default).
+    ScriptedFrameProviderDescriptor descriptor(metadata_sp);
+    descriptor.interface_sp = interface_sp;
+
+    auto id_or_err = target->AddScriptedFrameProviderDescriptor(descriptor);
+    if (!id_or_err) {
+      result.SetError(id_or_err.takeError());
+      return;
+    }
+
+    result.AppendMessageWithFormat(
+        "successfully registered scripted frame provider '%s' for target\n",
+        m_class_options.GetName().c_str());
+  }
+
+  OptionGroupPythonClassWithDict m_class_options;
+  OptionGroupOptions m_all_options;
+};
+
+class CommandObjectTargetFrameProviderClear : public CommandObjectParsed {
+public:
+  CommandObjectTargetFrameProviderClear(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "target frame-provider clear",
+            "Clear all registered frame providers from this target.", nullptr,
+            eCommandRequiresTarget) {}
+
+  ~CommandObjectTargetFrameProviderClear() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Target *target = m_exe_ctx.GetTargetPtr();
+    if (!target) {
+      result.AppendError("invalid target");
+      return;
+    }
+
+    target->ClearScriptedFrameProviderDescriptors();
+
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+  }
+};
+
+class CommandObjectTargetFrameProviderList : public CommandObjectParsed {
+public:
+  CommandObjectTargetFrameProviderList(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "target frame-provider list",
+            "List all registered frame providers for the target.", nullptr,
+            eCommandRequiresTarget) {}
+
+  ~CommandObjectTargetFrameProviderList() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Target *target = m_exe_ctx.GetTargetPtr();
+    if (!target)
+      target = &GetDebugger().GetDummyTarget();
+
+    const auto &descriptors = target->GetScriptedFrameProviderDescriptors();
+    if (descriptors.empty()) {
+      result.AppendMessage("no frame providers registered for this target.");
+      result.SetStatus(eReturnStatusSuccessFinishResult);
+      return;
+    }
+
+    result.AppendMessageWithFormat("%u frame provider(s) registered:\n\n",
+                                   descriptors.size());
+
+    for (const auto &entry : descriptors) {
+      const ScriptedFrameProviderDescriptor &descriptor = entry.second;
+      descriptor.Dump(&result.GetOutputStream());
+      result.GetOutputStream().PutChar('\n');
+    }
+
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+  }
+};
+
+class CommandObjectTargetFrameProviderRemove : public CommandObjectParsed {
+public:
+  CommandObjectTargetFrameProviderRemove(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "target frame-provider remove",
+            "Remove a registered frame provider from the target by name.",
+            "target frame-provider remove <provider-name>",
+            eCommandRequiresTarget) {
+    AddSimpleArgumentList(eArgTypeUnsignedInteger, eArgRepeatPlain);
+  }
+
+  ~CommandObjectTargetFrameProviderRemove() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Target *target = m_exe_ctx.GetTargetPtr();
+    if (!target)
+      target = &GetDebugger().GetDummyTarget();
+
+    if (command.GetArgumentCount() != 1) {
+      result.AppendError(
+          "target frame-provider remove requires a provider id argument");
+      return;
+    }
+
+    uint32_t provider_id = 0;
+    if (!llvm::to_integer(command[0].ref(), provider_id)) {
+      result.AppendError(
+          "target frame-provider remove requires integer provider id argument");
+      return;
+    }
+
+    if (target->RemoveScriptedFrameProviderDescriptor(provider_id)) {
+      result.AppendMessageWithFormat(
+          "successfully removed frame provider '%u'\n", provider_id);
+      result.SetStatus(eReturnStatusSuccessFinishResult);
+    } else {
+      result.AppendErrorWithFormat(
+          "no frame provider named '%u' found in target\n", provider_id);
+    }
+  }
+};
+
+class CommandObjectTargetFrameProvider : public CommandObjectMultiword {
+public:
+  CommandObjectTargetFrameProvider(CommandInterpreter &interpreter)
+      : CommandObjectMultiword(
+            interpreter, "target frame-provider",
+            "Commands for registering and viewing frame providers for the "
+            "target.",
+            "target frame-provider [<sub-command-options>] ") {
+    LoadSubCommand("register",
+                   CommandObjectSP(new CommandObjectTargetFrameProviderRegister(
+                       interpreter)));
+    LoadSubCommand("clear",
+                   CommandObjectSP(
+                       new CommandObjectTargetFrameProviderClear(interpreter)));
+    LoadSubCommand(
+        "list",
+        CommandObjectSP(new CommandObjectTargetFrameProviderList(interpreter)));
+    LoadSubCommand(
+        "remove", CommandObjectSP(
+                      new CommandObjectTargetFrameProviderRemove(interpreter)));
+  }
+
+  ~CommandObjectTargetFrameProvider() override = default;
+};
+
 #pragma mark CommandObjectMultiwordTarget
 
 // CommandObjectMultiwordTarget
@@ -5407,6 +5601,9 @@ CommandObjectMultiwordTarget::CommandObjectMultiwordTarget(
                  CommandObjectSP(new CommandObjectTargetDelete(interpreter)));
   LoadSubCommand("dump",
                  CommandObjectSP(new CommandObjectTargetDump(interpreter)));
+  LoadSubCommand(
+      "frame-provider",
+      CommandObjectSP(new CommandObjectTargetFrameProvider(interpreter)));
   LoadSubCommand("list",
                  CommandObjectSP(new CommandObjectTargetList(interpreter)));
   LoadSubCommand("select",
diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp
index 211868b51facb..69d8607a873f3 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -106,6 +106,13 @@ ScriptInterpreter::GetStatusFromSBError(const lldb::SBError &error) const {
   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)
diff --git a/lldb/source/Plugins/CMakeLists.txt b/lldb/source/Plugins/CMakeLists.txt
index 08f444e7b15e8..b6878b21ff71a 100644
--- a/lldb/source/Plugins/CMakeLists.txt
+++ b/lldb/source/Plugins/CMakeLists.txt
@@ -22,6 +22,7 @@ add_subdirectory(SymbolFile)
 add_subdirectory(SystemRuntime)
 add_subdirectory(SymbolLocator)
 add_subdirectory(SymbolVendor)
+add_subdirectory(SyntheticFrameProvider)
 add_subdirectory(Trace)
 add_subdirectory(TraceExporter)
 add_subdirectory(TypeSystem)
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
index 6519df9185df0..53d0c22e62ad7 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
@@ -7,8 +7,22 @@
 //===----------------------------------------------------------------------===//
 
 #include "ScriptedFrame.h"
-
+#include "Plugins/Process/Utility/RegisterContextMemory.h"
+
+#include "lldb/Core/Address.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Interpreter/Interfaces/ScriptedFrameInterface.h"
+#include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
+#include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Target/ExecutionContext.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/RegisterContext.h"
+#include "lldb/Target/Thread.h"
 #include "lldb/Utility/DataBufferHeap.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/Utility/StructuredData.h"
 
 using namespace lldb;
 using namespace lldb_private;
@@ -19,30 +33,44 @@ void ScriptedFrame::CheckInterpreterAndScriptObject() const {
 }
 
 llvm::Expected<std::shared_ptr<ScriptedFrame>>
-ScriptedFrame::Create(ScriptedThread &thread,
+ScriptedFrame::Create(ThreadSP thread_sp,
+                      ScriptedThreadInterfaceSP scripted_thread_interface_sp,
                       StructuredData::DictionarySP args_sp,
                       StructuredData::Generic *script_object) {
-  if (!thread.IsValid())
-    return llvm::createStringError("Invalid scripted thread.");
+  if (!thread_sp || !thread_sp->IsValid())
+    return llvm::createStringError("invalid thread");
+
+  ProcessSP process_sp = thread_sp->GetProcess();
+  if (!process_sp || !process_sp->IsValid())
+    return llvm::createStringError("invalid process");
 
-  thread.CheckInterpreterAndScriptObject();
+  ScriptInterpreter *script_interp =
+      process_sp->GetTarget().GetDebugger().GetScriptInterpreter();
+  if (!script_interp)
+    return llvm::createStringError("no script interpreter");
 
-  auto scripted_frame_interface =
-      thread.GetInterface()->CreateScriptedFrameInterface();
+  auto scripted_frame_interface = script_interp->CreateScriptedFrameInterface();
   if (!scripted_frame_interface)
     return llvm::createStringError("failed to create scripted frame interface");
 
   llvm::StringRef frame_class_name;
   if (!script_object) {
-    std::optional<std::string> class_name =
-        thread.GetInterface()->GetScriptedFramePluginName();
-    if (!class_name || class_name->empty())
+    // If no script object is provided and we have a scripted thread interface,
+    // try to get the frame class name from it.
+    if (scripted_thread_interface_sp) {
+      std::optional<std::string> class_name =
+          scripted_thread_interface_sp->GetScriptedFramePluginName();
+      if (!class_name || class_name->empty())
+        return llvm::createStringError(
+            "failed to get scripted frame class name");
+      frame_class_name = *class_name;
+    } else {
       return llvm::createStringError(
-          "failed to get scripted thread class name");
-    frame_class_name = *class_name;
+          "no script object provided and no scripted thread interface");
+    }
   }
 
-  ExecutionContext exe_ctx(thread);
+  ExecutionContext exe_ctx(thread_sp);
   auto obj_or_err = scripted_frame_interface->CreatePluginObject(
       frame_class_name, exe_ctx, args_sp, script_object);
 
@@ -62,7 +90,7 @@ ScriptedFrame::Create(ScriptedThread &thread,
   SymbolContext sc;
   Address symbol_addr;
   if (pc != LLDB_INVALID_ADDRESS) {
-    symbol_addr.SetLoadAddress(pc, &thread.GetProcess()->GetTarget());
+    symbol_addr.SetLoadAddress(pc, &process_sp->GetTarget());
     symbol_addr.CalculateSymbolContext(&sc);
   }
 
@@ -77,11 +105,11 @@ ScriptedFrame::Create(ScriptedThread &thread,
 
   if (!reg_info)
     return llvm::createStringError(
-        "failed to get scripted thread registers info");
+        "failed to get scripted frame registers info");
 
   std::shared_ptr<DynamicRegisterInfo> register_info_sp =
-      DynamicRegisterInfo::Create(
-          *reg_info, thread.GetProcess()->GetTarget().GetArchitecture());
+      DynamicRegisterInfo::Create(*reg_info,
+                                  process_sp->GetTarget().GetArchitecture());
 
   lldb::RegisterContextSP reg_ctx_sp;
 
@@ -96,32 +124,35 @@ ScriptedFrame::Create(ScriptedThread &thread,
 
     std::shared_ptr<RegisterContextMemory> reg_ctx_memory =
         std::make_shared<RegisterContextMemory>(
-            thread, frame_id, *register_info_sp, LLDB_INVALID_ADDRESS);
+            *thread_sp, frame_id, *register_info_sp, LLDB_INVALID_ADDRESS);
     if (!reg_ctx_memory)
-      return llvm::createStringError("failed to create a register context.");
+      return llvm::createStringError("failed to create a register context");
 
     reg_ctx_memory->SetAllRegisterData(data_sp);
     reg_ctx_sp = reg_ctx_memory;
   }
 
   return std::make_shared<ScriptedFrame>(
-      thread, scripted_frame_interface, frame_id, pc, sc, reg_ctx_sp,
+      thread_sp, scripted_frame_interface, frame_id, pc, sc, reg_ctx_sp,
       register_info_sp, owned_script_object_sp);
 }
 
-ScriptedFrame::ScriptedFrame(ScriptedThread &thread,
+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,
                              std::shared_ptr<DynamicRegisterInfo> reg_info_sp,
                              StructuredData::GenericSP script_object_sp)
-    : StackFrame(thread.shared_from_this(), /*frame_idx=*/id,
+    : StackFrame(thread_sp, /*frame_idx=*/id,
                  /*concrete_frame_idx=*/id, /*reg_context_sp=*/reg_ctx_sp,
                  /*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), m_register_info_sp(reg_info_sp) {}
+      m_script_object_sp(script_object_sp), m_register_info_sp(reg_info_sp) {
+  // FIXME: This should be part of the base class constructor.
+  m_stack_frame_kind = StackFrame::Kind::Synthetic;
+}
 
 ScriptedFrame::~ScriptedFrame() {}
 
@@ -164,7 +195,7 @@ std::shared_ptr<DynamicRegisterInfo> ScriptedFrame::GetDynamicRegisterInfo() {
     if (!reg_info)
       return ScriptedInterface::ErrorWithMessage<
           std::shared_ptr<DynamicRegisterInfo>>(
-          LLVM_PRETTY_FUNCTION, "Failed to get scripted frame registers info.",
+          LLVM_PRETTY_FUNCTION, "failed to get scripted frame registers info",
           error, LLDBLog::Thread);
 
     ThreadSP thread_sp = m_thread_wp.lock();
@@ -172,7 +203,7 @@ std::shared_ptr<DynamicRegisterInfo> ScriptedFrame::GetDynamicRegisterInfo() {
       return ScriptedInterface::ErrorWithMessage<
           std::shared_ptr<DynamicRegisterInfo>>(
           LLVM_PRETTY_FUNCTION,
-          "Failed to get scripted frame registers info: invalid thread.", error,
+          "failed to get scripted frame registers info: invalid thread", error,
           LLDBLog::Thread);
 
     ProcessSP process_sp = thread_sp->GetProcess();
@@ -180,8 +211,8 @@ std::shared_ptr<DynamicRegisterInfo> ScriptedFrame::GetDynamicRegisterInfo() {
       return ScriptedInterface::ErrorWithMessage<
           std::shared_ptr<DynamicRegisterInfo>>(
           LLVM_PRETTY_FUNCTION,
-          "Failed to get scripted frame registers info: invalid process.",
-          error, LLDBLog::Thread);
+          "failed to get scripted frame registers info: invalid process", error,
+          LLDBLog::Thread);
 
     m_register_info_sp = DynamicRegisterInfo::Create(
         *reg_info, process_sp->GetTarget().GetArchitecture());
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedFrame.h b/lldb/source/Plugins/Process/scripted/ScriptedFrame.h
index b6b77c4a7d160..e91e6160bac2f 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedFrame.h
+++ b/lldb/source/Plugins/Process/scripted/ScriptedFrame.h
@@ -10,21 +10,19 @@
 #define LLDB_SOURCE_PLUGINS_SCRIPTED_FRAME_H
 
 #include "ScriptedThread.h"
-#include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Target/DynamicRegisterInfo.h"
 #include "lldb/Target/StackFrame.h"
+#include "lldb/lldb-forward.h"
+#include "llvm/Support/Error.h"
+#include <memory>
 #include <string>
 
-namespace lldb_private {
-class ScriptedThread;
-}
-
 namespace lldb_private {
 
 class ScriptedFrame : public lldb_private::StackFrame {
 
 public:
-  ScriptedFrame(ScriptedThread &thread,
+  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,
@@ -33,8 +31,29 @@ class ScriptedFrame : public lldb_private::StackFrame {
 
   ~ScriptedFrame() override;
 
+  /// Create a ScriptedFrame from a object instanciated in the script
+  /// interpreter.
+  ///
+  /// \param[in] thread_sp
+  ///     The thread this frame belongs to.
+  ///
+  /// \param[in] scripted_thread_interface_sp
+  ///     The scripted thread interface (needed for ScriptedThread
+  ///     compatibility). Can be nullptr for frames on real threads.
+  ///
+  /// \param[in] args_sp
+  ///     Arguments to pass to the frame creation.
+  ///
+  /// \param[in] script_object
+  ///     The optional script object representing this frame.
+  ///
+  /// \return
+  ///     An Expected containing the ScriptedFrame shared pointer if successful,
+  ///     otherwise an error.
   static llvm::Expected<std::shared_ptr<ScriptedFrame>>
-  Create(ScriptedThread &thread, StructuredData::DictionarySP args_sp,
+  Create(lldb::ThreadSP thread_sp,
+         lldb::ScriptedThreadInterfaceSP scripted_thread_interface_sp,
+         StructuredData::DictionarySP args_sp,
          StructuredData::Generic *script_object = nullptr);
 
   bool IsInlined() override;
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
index 491efac5aadef..1dd9c48f56a59 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
@@ -210,7 +210,7 @@ bool ScriptedThread::LoadArtificialStackFrames() {
     SymbolContext sc;
     symbol_addr.CalculateSymbolContext(&sc);
 
-    return std::make_shared<StackFrame>(this->shared_from_this(), idx, idx, cfa,
+    return std::make_shared<StackFrame>(shared_from_this(), idx, idx, cfa,
                                         cfa_is_valid, pc,
                                         StackFrame::Kind::Synthetic, artificial,
                                         behaves_like_zeroth_frame, &sc);
@@ -231,8 +231,8 @@ bool ScriptedThread::LoadArtificialStackFrames() {
       return error.ToError();
     }
 
-    auto frame_or_error =
-        ScriptedFrame::Create(*this, nullptr, object_sp->GetAsGeneric());
+    auto frame_or_error = ScriptedFrame::Create(
+        shared_from_this(), GetInterface(), nullptr, object_sp->GetAsGeneric());
 
     if (!frame_or_error) {
       ScriptedInterface::ErrorWithMessage<bool>(
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
index d43036d6fe544..f6c707b2bd168 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() {
   ScriptedStopHookPythonInterface::Initialize();
   ScriptedBreakpointPythonInterface::Initialize();
   ScriptedThreadPlanPythonInterface::Initialize();
+  ScriptedFrameProviderPythonInterface::Initialize();
 }
 
 void ScriptInterpreterPythonInterfaces::Terminate() {
@@ -40,6 +41,7 @@ void ScriptInterpreterPythonInterfaces::Terminate() {
   ScriptedStopHookPythonInterface::Terminate();
   ScriptedBreakpointPythonInterface::Terminate();
   ScriptedThreadPlanPythonInterface::Terminate();
+  ScriptedFrameProviderPythonInterface::Terminate();
 }
 
 #endif
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp
index b866bf332b7b6..bb5ff209fea3a 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp
@@ -6,6 +6,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "lldb/Core/PluginManager.h"
 #include "lldb/Host/Config.h"
 #include "lldb/Target/Thread.h"
 #include "lldb/Utility/Log.h"
@@ -30,6 +31,21 @@ ScriptedFrameProviderPythonInterface::ScriptedFrameProviderPythonInterface(
     ScriptInterpreterPythonImpl &interpreter)
     : ScriptedFrameProviderInterface(), ScriptedPythonInterface(interpreter) {}
 
+bool ScriptedFrameProviderPythonInterface::AppliesToThread(
+    llvm::StringRef class_name, lldb::ThreadSP thread_sp) {
+  // If there is any issue with this method, we will just assume it also applies
+  // to this thread which is the default behavior.
+  constexpr bool fail_value = true;
+  Status error;
+  StructuredData::ObjectSP obj =
+      CallStaticMethod(class_name, "applies_to_thread", error, thread_sp);
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return fail_value;
+
+  return obj->GetBooleanValue(fail_value);
+}
+
 llvm::Expected<StructuredData::GenericSP>
 ScriptedFrameProviderPythonInterface::CreatePluginObject(
     const llvm::StringRef class_name, lldb::StackFrameListSP input_frames,
@@ -42,6 +58,18 @@ ScriptedFrameProviderPythonInterface::CreatePluginObject(
                                                      input_frames, sd_impl);
 }
 
+std::string ScriptedFrameProviderPythonInterface::GetDescription(
+    llvm::StringRef class_name) {
+  Status error;
+  StructuredData::ObjectSP obj =
+      CallStaticMethod(class_name, "get_description", error);
+  if (!ScriptedInterface::CheckStructuredDataObject(LLVM_PRETTY_FUNCTION, obj,
+                                                    error))
+    return {};
+
+  return obj->GetStringValue().str();
+}
+
 StructuredData::ObjectSP
 ScriptedFrameProviderPythonInterface::GetFrameAtIndex(uint32_t index) {
   Status error;
@@ -54,4 +82,32 @@ ScriptedFrameProviderPythonInterface::GetFrameAtIndex(uint32_t index) {
   return obj;
 }
 
+bool ScriptedFrameProviderPythonInterface::CreateInstance(
+    lldb::ScriptLanguage language, ScriptedInterfaceUsages usages) {
+  if (language != eScriptLanguagePython)
+    return false;
+
+  return true;
+}
+
+void ScriptedFrameProviderPythonInterface::Initialize() {
+  const std::vector<llvm::StringRef> ci_usages = {
+      "target frame-provider register -C <script-name> [-k key -v value ...]",
+      "target frame-provider list",
+      "target frame-provider remove <provider-name>",
+      "target frame-provider clear"};
+  const std::vector<llvm::StringRef> api_usages = {
+      "SBTarget.RegisterScriptedFrameProvider",
+      "SBTarget.RemoveScriptedFrameProvider",
+      "SBTarget.ClearScriptedFrameProvider"};
+  PluginManager::RegisterPlugin(
+      GetPluginNameStatic(),
+      llvm::StringRef("Provide scripted stack frames for threads"),
+      CreateInstance, eScriptLanguagePython, {ci_usages, api_usages});
+}
+
+void ScriptedFrameProviderPythonInterface::Terminate() {
+  PluginManager::UnregisterPlugin(CreateInstance);
+}
+
 #endif
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.h
index fd163984028d3..97a5cc7c669ea 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.h
@@ -14,17 +14,22 @@
 #if LLDB_ENABLE_PYTHON
 
 #include "ScriptedPythonInterface.h"
+#include "lldb/Core/PluginInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
 #include <optional>
 
 namespace lldb_private {
 class ScriptedFrameProviderPythonInterface
     : public ScriptedFrameProviderInterface,
-      public ScriptedPythonInterface {
+      public ScriptedPythonInterface,
+      public PluginInterface {
 public:
   ScriptedFrameProviderPythonInterface(
       ScriptInterpreterPythonImpl &interpreter);
 
+  bool AppliesToThread(llvm::StringRef class_name,
+                       lldb::ThreadSP thread_sp) override;
+
   llvm::Expected<StructuredData::GenericSP>
   CreatePluginObject(llvm::StringRef class_name,
                      lldb::StackFrameListSP input_frames,
@@ -33,10 +38,24 @@ class ScriptedFrameProviderPythonInterface
   llvm::SmallVector<AbstractMethodRequirement>
   GetAbstractMethodRequirements() const override {
     return llvm::SmallVector<AbstractMethodRequirement>(
-        {{"get_frame_at_index"}});
+        {{"get_description"}, {"get_frame_at_index"}});
   }
 
+  std::string GetDescription(llvm::StringRef class_name) override;
+
   StructuredData::ObjectSP GetFrameAtIndex(uint32_t index) override;
+
+  static void Initialize();
+  static void Terminate();
+
+  static bool CreateInstance(lldb::ScriptLanguage language,
+                             ScriptedInterfaceUsages usages);
+
+  static llvm::StringRef GetPluginNameStatic() {
+    return "ScriptedFrameProviderPythonInterface";
+  }
+
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
 };
 } // namespace lldb_private
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
index af2e0b5df4d22..ba4473cf9ec4d 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
@@ -93,6 +93,19 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::StackFrameSP>(
   return nullptr;
 }
 
+template <>
+lldb::ThreadSP
+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);
+  error = Status::FromErrorString(
+      "Couldn't cast lldb::SBThread to lldb_private::Thread.");
+
+  return nullptr;
+}
+
 template <>
 SymbolContext
 ScriptedPythonInterface::ExtractValueFromPythonObject<SymbolContext>(
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index ec1dd9910d8a6..91c4e2aae6380 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -325,6 +325,112 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     return m_object_instance_sp;
   }
 
+  /// Call a static method on a Python class without creating an instance.
+  ///
+  /// This method resolves a Python class by name and calls a static method
+  /// on it, returning the result. This is useful for calling class-level
+  /// methods that don't require an instance.
+  ///
+  /// \param class_name The fully-qualified name of the Python class.
+  /// \param method_name The name of the static method to call.
+  /// \param args Arguments to pass to the static method.
+  ///
+  /// \return The return value of the static method call, or an error.
+  template <typename T = StructuredData::ObjectSP, typename... Args>
+  T CallStaticMethod(llvm::StringRef class_name, llvm::StringRef method_name,
+                     Status &error, Args &&...args) {
+    using namespace python;
+    using Locker = ScriptInterpreterPythonImpl::Locker;
+
+    std::string caller_signature =
+        llvm::Twine(LLVM_PRETTY_FUNCTION + llvm::Twine(" (") +
+                    llvm::Twine(class_name) + llvm::Twine(".") +
+                    llvm::Twine(method_name) + llvm::Twine(")"))
+            .str();
+
+    if (class_name.empty())
+      return ErrorWithMessage<T>(caller_signature, "Missing script class name.",
+                                 error);
+
+    Locker py_lock(&m_interpreter, Locker::AcquireLock | Locker::NoSTDIN,
+                   Locker::FreeLock);
+
+    // Get the interpreter dictionary.
+    auto dict =
+        PythonModule::MainModule().ResolveName<python::PythonDictionary>(
+            m_interpreter.GetDictionaryName());
+    if (!dict.IsAllocated())
+      return ErrorWithMessage<T>(
+          caller_signature,
+          llvm::formatv("Could not find interpreter dictionary: {0}",
+                        m_interpreter.GetDictionaryName())
+              .str(),
+          error);
+
+    // Resolve the class.
+    auto class_obj =
+        PythonObject::ResolveNameWithDictionary<python::PythonCallable>(
+            class_name, dict);
+    if (!class_obj.IsAllocated())
+      return ErrorWithMessage<T>(
+          caller_signature,
+          llvm::formatv("Could not find script class: {0}", class_name).str(),
+          error);
+
+    // Get the static method from the class.
+    if (!class_obj.HasAttribute(method_name))
+      return ErrorWithMessage<T>(
+          caller_signature,
+          llvm::formatv("Class {0} does not have method {1}", class_name,
+                        method_name)
+              .str(),
+          error);
+
+    PythonCallable method =
+        class_obj.GetAttributeValue(method_name).AsType<PythonCallable>();
+    if (!method.IsAllocated())
+      return ErrorWithMessage<T>(caller_signature,
+                                 llvm::formatv("Method {0}.{1} is not callable",
+                                               class_name, method_name)
+                                     .str(),
+                                 error);
+
+    // Transform the arguments.
+    std::tuple<Args...> original_args = std::forward_as_tuple(args...);
+    auto transformed_args = TransformArgs(original_args);
+
+    // Call the static method.
+    llvm::Expected<PythonObject> expected_return_object =
+        llvm::make_error<llvm::StringError>("Not initialized.",
+                                            llvm::inconvertibleErrorCode());
+    std::apply(
+        [&method, &expected_return_object](auto &&...args) {
+          llvm::consumeError(expected_return_object.takeError());
+          expected_return_object = method(args...);
+        },
+        transformed_args);
+
+    if (llvm::Error e = expected_return_object.takeError()) {
+      error = Status::FromError(std::move(e));
+      return ErrorWithMessage<T>(
+          caller_signature, "Python static method could not be called.", error);
+    }
+
+    PythonObject py_return = std::move(expected_return_object.get());
+
+    // Re-assign reference and pointer arguments if needed.
+    if (sizeof...(Args) > 0)
+      if (!ReassignPtrsOrRefsArgs(original_args, transformed_args))
+        return ErrorWithMessage<T>(
+            caller_signature,
+            "Couldn't re-assign reference and pointer arguments.", error);
+
+    if (!py_return.IsAllocated())
+      return {};
+
+    return ExtractValueFromPythonObject<T>(py_return, error);
+  }
+
 protected:
   template <typename T = StructuredData::ObjectSP>
   T ExtractValueFromPythonObject(python::PythonObject &p, Status &error) {
@@ -593,6 +699,11 @@ lldb::StreamSP
 ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::StreamSP>(
     python::PythonObject &p, Status &error);
 
+template <>
+lldb::ThreadSP
+ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ThreadSP>(
+    python::PythonObject &p, Status &error);
+
 template <>
 lldb::StackFrameSP
 ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::StackFrameSP>(
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
index 2c971262fc34e..32948ffd30023 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
@@ -265,6 +265,7 @@ void *LLDBSWIGPython_CastPyObjectToSBLaunchInfo(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBError(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBEvent(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBStream(PyObject *data);
+void *LLDBSWIGPython_CastPyObjectToSBThread(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBFrame(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBSymbolContext(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBValue(PyObject *data);
diff --git a/lldb/source/Plugins/SyntheticFrameProvider/CMakeLists.txt b/lldb/source/Plugins/SyntheticFrameProvider/CMakeLists.txt
new file mode 100644
index 0000000000000..85b405e648c1f
--- /dev/null
+++ b/lldb/source/Plugins/SyntheticFrameProvider/CMakeLists.txt
@@ -0,0 +1 @@
+add_subdirectory(ScriptedFrameProvider)
diff --git a/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/CMakeLists.txt b/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/CMakeLists.txt
new file mode 100644
index 0000000000000..fe67d39efdf11
--- /dev/null
+++ b/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/CMakeLists.txt
@@ -0,0 +1,12 @@
+add_lldb_library(lldbPluginScriptedFrameProvider PLUGIN
+  ScriptedFrameProvider.cpp
+
+  LINK_COMPONENTS
+    Support
+
+  LINK_LIBS
+    lldbCore
+    lldbInterpreter
+    lldbTarget
+    lldbUtility
+  )
diff --git a/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/ScriptedFrameProvider.cpp b/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/ScriptedFrameProvider.cpp
new file mode 100644
index 0000000000000..17d0e925fadc6
--- /dev/null
+++ b/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/ScriptedFrameProvider.cpp
@@ -0,0 +1,215 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "ScriptedFrameProvider.h"
+#include "Plugins/Process/scripted/ScriptedFrame.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/StackFrame.h"
+#include "lldb/Target/Thread.h"
+#include "lldb/Utility/ScriptedMetadata.h"
+#include "lldb/Utility/Status.h"
+#include "llvm/Support/Error.h"
+#include <cstdint>
+
+using namespace lldb;
+using namespace lldb_private;
+
+void ScriptedFrameProvider::Initialize() {
+  PluginManager::RegisterPlugin(GetPluginNameStatic(),
+                                "Provides synthetic frames via scripting",
+                                nullptr, ScriptedFrameProvider::CreateInstance);
+}
+
+void ScriptedFrameProvider::Terminate() {
+  PluginManager::UnregisterPlugin(ScriptedFrameProvider::CreateInstance);
+}
+
+llvm::Expected<lldb::SyntheticFrameProviderSP>
+ScriptedFrameProvider::CreateInstance(
+    lldb::StackFrameListSP input_frames,
+    const ScriptedFrameProviderDescriptor &descriptor) {
+  if (!input_frames)
+    return llvm::createStringError(
+        "failed to create scripted frame provider: invalid input frames");
+
+  Thread &thread = input_frames->GetThread();
+  ProcessSP process_sp = thread.GetProcess();
+  if (!process_sp)
+    return nullptr;
+
+  if (!descriptor.IsValid())
+    return llvm::createStringError(
+        "failed to create scripted frame provider: invalid scripted metadata");
+
+  if (!descriptor.AppliesToThread(thread))
+    return nullptr;
+
+  ScriptInterpreter *script_interp =
+      process_sp->GetTarget().GetDebugger().GetScriptInterpreter();
+  if (!script_interp)
+    return llvm::createStringError("cannot create scripted frame provider: No "
+                                   "script interpreter installed");
+
+  ScriptedFrameProviderInterfaceSP interface_sp =
+      script_interp->CreateScriptedFrameProviderInterface();
+  if (!interface_sp)
+    return llvm::createStringError(
+        "cannot create scripted frame provider: script interpreter couldn't "
+        "create Scripted Frame Provider Interface");
+
+  const ScriptedMetadataSP scripted_metadata = descriptor.scripted_metadata_sp;
+
+  // If we shouldn't attach a frame provider to this thread, just exit early.
+  if (!interface_sp->AppliesToThread(scripted_metadata->GetClassName(),
+                                     thread.shared_from_this()))
+    return nullptr;
+
+  auto obj_or_err = interface_sp->CreatePluginObject(
+      scripted_metadata->GetClassName(), input_frames,
+      scripted_metadata->GetArgsSP());
+  if (!obj_or_err)
+    return obj_or_err.takeError();
+
+  StructuredData::ObjectSP object_sp = *obj_or_err;
+  if (!object_sp || !object_sp->IsValid())
+    return llvm::createStringError(
+        "cannot create scripted frame provider: failed to create valid scripted"
+        "frame provider object");
+
+  return std::make_shared<ScriptedFrameProvider>(input_frames, interface_sp,
+                                                 descriptor);
+}
+
+ScriptedFrameProvider::ScriptedFrameProvider(
+    StackFrameListSP input_frames,
+    lldb::ScriptedFrameProviderInterfaceSP interface_sp,
+    const ScriptedFrameProviderDescriptor &descriptor)
+    : SyntheticFrameProvider(input_frames), m_interface_sp(interface_sp),
+      m_descriptor(descriptor) {}
+
+ScriptedFrameProvider::~ScriptedFrameProvider() = default;
+
+std::string ScriptedFrameProvider::GetDescription() const {
+  if (!m_interface_sp)
+    return {};
+
+  return m_interface_sp->GetDescription(m_descriptor.GetName());
+}
+
+llvm::Expected<StackFrameSP>
+ScriptedFrameProvider::GetFrameAtIndex(uint32_t idx) {
+  if (!m_interface_sp)
+    return llvm::createStringError(
+        "cannot get stack frame: scripted frame provider not initialized");
+
+  auto create_frame_from_dict =
+      [this](StructuredData::Dictionary *dict,
+             uint32_t index) -> llvm::Expected<StackFrameSP> {
+    lldb::addr_t pc;
+    if (!dict->GetValueForKeyAsInteger("pc", pc))
+      return llvm::createStringError(
+          "missing 'pc' key from scripted frame dictionary");
+
+    Address symbol_addr;
+    symbol_addr.SetLoadAddress(pc, &GetThread().GetProcess()->GetTarget());
+
+    const lldb::addr_t cfa = LLDB_INVALID_ADDRESS;
+    const bool cfa_is_valid = false;
+    const bool artificial = false;
+    const bool behaves_like_zeroth_frame = false;
+    SymbolContext sc;
+    symbol_addr.CalculateSymbolContext(&sc);
+
+    ThreadSP thread_sp = GetThread().shared_from_this();
+    return std::make_shared<StackFrame>(thread_sp, index, index, cfa,
+                                        cfa_is_valid, pc,
+                                        StackFrame::Kind::Synthetic, artificial,
+                                        behaves_like_zeroth_frame, &sc);
+  };
+
+  auto create_frame_from_script_object =
+      [this](
+          StructuredData::ObjectSP object_sp) -> llvm::Expected<StackFrameSP> {
+    Status error;
+    if (!object_sp || !object_sp->GetAsGeneric())
+      return llvm::createStringError("invalid script object");
+
+    ThreadSP thread_sp = GetThread().shared_from_this();
+    auto frame_or_error = ScriptedFrame::Create(thread_sp, nullptr, nullptr,
+                                                object_sp->GetAsGeneric());
+
+    if (!frame_or_error) {
+      ScriptedInterface::ErrorWithMessage<bool>(
+          LLVM_PRETTY_FUNCTION, toString(frame_or_error.takeError()), error);
+      return error.ToError();
+    }
+
+    return *frame_or_error;
+  };
+
+  StructuredData::ObjectSP obj_sp = m_interface_sp->GetFrameAtIndex(idx);
+
+  // None/null means no more frames or error.
+  if (!obj_sp || !obj_sp->IsValid())
+    return llvm::createStringError("invalid script object returned for frame " +
+                                   llvm::Twine(idx));
+
+  StackFrameSP synth_frame_sp = nullptr;
+  if (StructuredData::UnsignedInteger *int_obj =
+          obj_sp->GetAsUnsignedInteger()) {
+    uint32_t real_frame_index = int_obj->GetValue();
+    if (real_frame_index < m_input_frames->GetNumFrames()) {
+      synth_frame_sp = m_input_frames->GetFrameAtIndex(real_frame_index);
+    }
+  } else if (StructuredData::Dictionary *dict = obj_sp->GetAsDictionary()) {
+    // Check if it's a dictionary describing a frame.
+    auto frame_from_dict_or_err = create_frame_from_dict(dict, idx);
+    if (!frame_from_dict_or_err) {
+      return llvm::createStringError(llvm::Twine(
+          "couldn't create frame from dictionary at index " + llvm::Twine(idx) +
+          ": " + toString(frame_from_dict_or_err.takeError())));
+    }
+    synth_frame_sp = *frame_from_dict_or_err;
+  } else if (obj_sp->GetAsGeneric()) {
+    // It's a ScriptedFrame object.
+    auto frame_from_script_obj_or_err = create_frame_from_script_object(obj_sp);
+    if (!frame_from_script_obj_or_err) {
+      return llvm::createStringError(
+          llvm::Twine("couldn't create frame from script object at index " +
+                      llvm::Twine(idx) + ": " +
+                      toString(frame_from_script_obj_or_err.takeError())));
+    }
+    synth_frame_sp = *frame_from_script_obj_or_err;
+  } else {
+    return llvm::createStringError(
+        llvm::Twine("invalid return type from get_frame_at_index at index " +
+                    llvm::Twine(idx)));
+  }
+
+  if (!synth_frame_sp)
+    return llvm::createStringError(
+        llvm::Twine("failed to create frame at index " + llvm::Twine(idx)));
+
+  synth_frame_sp->SetFrameIndex(idx);
+
+  return synth_frame_sp;
+}
+
+namespace lldb_private {
+void lldb_initialize_ScriptedFrameProvider() {
+  ScriptedFrameProvider::Initialize();
+}
+
+void lldb_terminate_ScriptedFrameProvider() {
+  ScriptedFrameProvider::Terminate();
+}
+} // namespace lldb_private
diff --git a/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/ScriptedFrameProvider.h b/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/ScriptedFrameProvider.h
new file mode 100644
index 0000000000000..3434bf26ade24
--- /dev/null
+++ b/lldb/source/Plugins/SyntheticFrameProvider/ScriptedFrameProvider/ScriptedFrameProvider.h
@@ -0,0 +1,53 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_PLUGINS_SYNTHETICFRAMEPROVIDER_SCRIPTEDFRAMEPROVIDER_SCRIPTEDFRAMEPROVIDER_H
+#define LLDB_PLUGINS_SYNTHETICFRAMEPROVIDER_SCRIPTEDFRAMEPROVIDER_SCRIPTEDFRAMEPROVIDER_H
+
+#include "lldb/Target/SyntheticFrameProvider.h"
+#include "lldb/Utility/ScriptedMetadata.h"
+#include "lldb/Utility/Status.h"
+#include "lldb/lldb-forward.h"
+#include "llvm/Support/Error.h"
+
+namespace lldb_private {
+
+class ScriptedFrameProvider : public SyntheticFrameProvider {
+public:
+  static llvm::StringRef GetPluginNameStatic() {
+    return "ScriptedFrameProvider";
+  }
+
+  static llvm::Expected<lldb::SyntheticFrameProviderSP>
+  CreateInstance(lldb::StackFrameListSP input_frames,
+                 const ScriptedFrameProviderDescriptor &descriptor);
+
+  static void Initialize();
+
+  static void Terminate();
+
+  ScriptedFrameProvider(lldb::StackFrameListSP input_frames,
+                        lldb::ScriptedFrameProviderInterfaceSP interface_sp,
+                        const ScriptedFrameProviderDescriptor &descriptor);
+  ~ScriptedFrameProvider() override;
+
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+
+  std::string GetDescription() const override;
+
+  /// Get a single stack frame at the specified index.
+  llvm::Expected<lldb::StackFrameSP> GetFrameAtIndex(uint32_t idx) override;
+
+private:
+  lldb::ScriptedFrameProviderInterfaceSP m_interface_sp;
+  const ScriptedFrameProviderDescriptor &m_descriptor;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_PLUGINS_SYNTHETICFRAMEPROVIDER_SCRIPTEDFRAMEPROVIDER_SCRIPTEDFRAMEPROVIDER_H
diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp
index ccf874fc03ebd..b39b36a09af4d 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -20,6 +20,7 @@
 #include "lldb/Target/StackFrame.h"
 #include "lldb/Target/StackFrameRecognizer.h"
 #include "lldb/Target/StopInfo.h"
+#include "lldb/Target/SyntheticFrameProvider.h"
 #include "lldb/Target/Target.h"
 #include "lldb/Target/Thread.h"
 #include "lldb/Target/Unwind.h"
@@ -55,6 +56,37 @@ StackFrameList::~StackFrameList() {
   Clear();
 }
 
+SyntheticStackFrameList::SyntheticStackFrameList(
+    Thread &thread, lldb::StackFrameListSP input_frames,
+    const lldb::StackFrameListSP &prev_frames_sp, bool show_inline_frames)
+    : StackFrameList(thread, prev_frames_sp, show_inline_frames),
+      m_input_frames(std::move(input_frames)) {}
+
+bool SyntheticStackFrameList::FetchFramesUpTo(
+    uint32_t end_idx, InterruptionControl allow_interrupt) {
+  // Check if the thread has a synthetic frame provider.
+  if (auto provider_sp = m_thread.GetFrameProvider()) {
+    // Use the synthetic frame provider to generate frames lazily.
+    // Keep fetching until we reach end_idx or the provider returns an error.
+    for (uint32_t idx = m_frames.size(); idx <= end_idx; idx++) {
+      auto frame_or_err = provider_sp->GetFrameAtIndex(idx);
+      if (!frame_or_err) {
+        // Provider returned error - we've reached the end.
+        LLDB_LOG_ERROR(GetLog(LLDBLog::Thread), frame_or_err.takeError(),
+                       "Frame provider reached end at index {0}: {1}", idx);
+        SetAllFramesFetched();
+        break;
+      }
+      m_frames.push_back(*frame_or_err);
+    }
+
+    return false; // Not interrupted.
+  }
+
+  // If no provider, fall back to the base implementation.
+  return StackFrameList::FetchFramesUpTo(end_idx, allow_interrupt);
+}
+
 void StackFrameList::CalculateCurrentInlinedDepth() {
   uint32_t cur_inlined_depth = GetCurrentInlinedDepth();
   if (cur_inlined_depth == UINT32_MAX) {
diff --git a/lldb/source/Target/SyntheticFrameProvider.cpp b/lldb/source/Target/SyntheticFrameProvider.cpp
index 241ce82c39be3..97ff42d1ed53e 100644
--- a/lldb/source/Target/SyntheticFrameProvider.cpp
+++ b/lldb/source/Target/SyntheticFrameProvider.cpp
@@ -8,10 +8,12 @@
 
 #include "lldb/Target/SyntheticFrameProvider.h"
 #include "lldb/Core/PluginManager.h"
+#include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
 #include "lldb/Target/Thread.h"
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/Log.h"
 #include "lldb/Utility/Status.h"
+#include "lldb/Utility/Stream.h"
 
 using namespace lldb;
 using namespace lldb_private;
@@ -21,12 +23,17 @@ SyntheticFrameProvider::SyntheticFrameProvider(StackFrameListSP input_frames)
 
 SyntheticFrameProvider::~SyntheticFrameProvider() = default;
 
-void SyntheticFrameProviderDescriptor::Dump(Stream *s) const {
+void ScriptedFrameProviderDescriptor::Dump(Stream *s) const {
   if (!s)
     return;
 
+  s->Format("  ID: {0:x}\n", GetID());
   s->Printf("  Name: %s\n", GetName().str().c_str());
 
+  std::string description = GetDescription();
+  if (!description.empty())
+    s->Printf("  Description: %s\n", description.c_str());
+
   // Show thread filter information.
   if (thread_specs.empty()) {
     s->PutCString("  Thread Filter: (applies to all threads)\n");
@@ -41,9 +48,23 @@ void SyntheticFrameProviderDescriptor::Dump(Stream *s) const {
   }
 }
 
+uint32_t ScriptedFrameProviderDescriptor::GetID() const {
+  if (!scripted_metadata_sp)
+    return 0;
+
+  return scripted_metadata_sp->GetID();
+}
+
+std::string ScriptedFrameProviderDescriptor::GetDescription() const {
+  // If we have an interface, call get_description() to fetch it.
+  if (interface_sp && scripted_metadata_sp)
+    return interface_sp->GetDescription(scripted_metadata_sp->GetClassName());
+  return {};
+}
+
 llvm::Expected<SyntheticFrameProviderSP> SyntheticFrameProvider::CreateInstance(
     StackFrameListSP input_frames,
-    const SyntheticFrameProviderDescriptor &descriptor) {
+    const ScriptedFrameProviderDescriptor &descriptor) {
   if (!input_frames)
     return llvm::createStringError(
         "cannot create synthetic frame provider: invalid input frames");
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index e53fc7a1e1bda..5544b40a8b9c0 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -3713,6 +3713,61 @@ Status Target::Attach(ProcessAttachInfo &attach_info, Stream *stream) {
   return error;
 }
 
+llvm::Expected<uint32_t> Target::AddScriptedFrameProviderDescriptor(
+    const ScriptedFrameProviderDescriptor &descriptor) {
+  if (!descriptor.IsValid())
+    return llvm::createStringError("invalid frame provider descriptor");
+
+  llvm::StringRef name = descriptor.GetName();
+  if (name.empty())
+    return llvm::createStringError(
+        "frame provider descriptor has no class name");
+
+  std::lock_guard<std::recursive_mutex> guard(
+      m_frame_provider_descriptors_mutex);
+
+  uint32_t descriptor_id = descriptor.GetID();
+  m_frame_provider_descriptors[descriptor_id] = descriptor;
+
+  // Clear frame providers on existing threads so they reload with new config.
+  if (ProcessSP process_sp = GetProcessSP())
+    for (ThreadSP thread_sp : process_sp->Threads())
+      thread_sp->ClearScriptedFrameProvider();
+
+  return descriptor_id;
+}
+
+bool Target::RemoveScriptedFrameProviderDescriptor(uint32_t id) {
+  std::lock_guard<std::recursive_mutex> guard(
+      m_frame_provider_descriptors_mutex);
+  bool removed = m_frame_provider_descriptors.erase(id);
+
+  if (removed)
+    if (ProcessSP process_sp = GetProcessSP())
+      for (ThreadSP thread_sp : process_sp->Threads())
+        thread_sp->ClearScriptedFrameProvider();
+
+  return removed;
+}
+
+void Target::ClearScriptedFrameProviderDescriptors() {
+  std::lock_guard<std::recursive_mutex> guard(
+      m_frame_provider_descriptors_mutex);
+
+  m_frame_provider_descriptors.clear();
+
+  if (ProcessSP process_sp = GetProcessSP())
+    for (ThreadSP thread_sp : process_sp->Threads())
+      thread_sp->ClearScriptedFrameProvider();
+}
+
+const llvm::DenseMap<uint32_t, ScriptedFrameProviderDescriptor> &
+Target::GetScriptedFrameProviderDescriptors() const {
+  std::lock_guard<std::recursive_mutex> guard(
+      m_frame_provider_descriptors_mutex);
+  return m_frame_provider_descriptors;
+}
+
 void Target::FinalizeFileActions(ProcessLaunchInfo &info) {
   Log *log = GetLog(LLDBLog::Process);
 
diff --git a/lldb/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp
index 8c3e19725f8cb..b40e753aca1e9 100644
--- a/lldb/source/Target/Thread.cpp
+++ b/lldb/source/Target/Thread.cpp
@@ -13,9 +13,12 @@
 #include "lldb/Core/Module.h"
 #include "lldb/Core/StructuredDataImpl.h"
 #include "lldb/Host/Host.h"
+#include "lldb/Interpreter/Interfaces/ScriptedFrameInterface.h"
+#include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
 #include "lldb/Interpreter/OptionValueFileSpecList.h"
 #include "lldb/Interpreter/OptionValueProperties.h"
 #include "lldb/Interpreter/Property.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Symbol/Function.h"
 #include "lldb/Target/ABI.h"
 #include "lldb/Target/DynamicLoader.h"
@@ -26,6 +29,7 @@
 #include "lldb/Target/ScriptedThreadPlan.h"
 #include "lldb/Target/StackFrameRecognizer.h"
 #include "lldb/Target/StopInfo.h"
+#include "lldb/Target/SyntheticFrameProvider.h"
 #include "lldb/Target/SystemRuntime.h"
 #include "lldb/Target/Target.h"
 #include "lldb/Target/ThreadPlan.h"
@@ -45,6 +49,7 @@
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/Log.h"
 #include "lldb/Utility/RegularExpression.h"
+#include "lldb/Utility/ScriptedMetadata.h"
 #include "lldb/Utility/State.h"
 #include "lldb/Utility/Stream.h"
 #include "lldb/Utility/StreamString.h"
@@ -257,6 +262,7 @@ void Thread::DestroyThread() {
   std::lock_guard<std::recursive_mutex> guard(m_frame_mutex);
   m_curr_frames_sp.reset();
   m_prev_frames_sp.reset();
+  m_frame_provider_sp.reset();
   m_prev_framezero_pc.reset();
 }
 
@@ -1439,13 +1445,76 @@ void Thread::CalculateExecutionContext(ExecutionContext &exe_ctx) {
 StackFrameListSP Thread::GetStackFrameList() {
   std::lock_guard<std::recursive_mutex> guard(m_frame_mutex);
 
-  if (!m_curr_frames_sp)
+  if (m_curr_frames_sp)
+    return m_curr_frames_sp;
+
+  // First, try to load a frame provider if we don't have one yet.
+  if (!m_frame_provider_sp) {
+    ProcessSP process_sp = GetProcess();
+    if (process_sp) {
+      Target &target = process_sp->GetTarget();
+      const auto &descriptors = target.GetScriptedFrameProviderDescriptors();
+
+      // Find first descriptor that applies to this thread.
+      for (const auto &entry : descriptors) {
+        const ScriptedFrameProviderDescriptor &descriptor = entry.second;
+        if (descriptor.IsValid() && descriptor.AppliesToThread(*this)) {
+          if (llvm::Error error = LoadScriptedFrameProvider(descriptor)) {
+            LLDB_LOG_ERROR(GetLog(LLDBLog::Thread), std::move(error),
+                           "Failed to load scripted frame provider: {0}");
+          }
+          break; // Use first matching descriptor (success or failure).
+        }
+      }
+    }
+  }
+
+  // Create the frame list based on whether we have a provider.
+  if (m_frame_provider_sp) {
+    // We have a provider - create synthetic frame list.
+    StackFrameListSP input_frames = m_frame_provider_sp->GetInputFrames();
+    m_curr_frames_sp = std::make_shared<SyntheticStackFrameList>(
+        *this, input_frames, m_prev_frames_sp, true);
+  } else {
+    // No provider - use normal unwinder frames.
     m_curr_frames_sp =
         std::make_shared<StackFrameList>(*this, m_prev_frames_sp, true);
+  }
 
   return m_curr_frames_sp;
 }
 
+llvm::Error Thread::LoadScriptedFrameProvider(
+    const ScriptedFrameProviderDescriptor &descriptor) {
+  std::lock_guard<std::recursive_mutex> guard(m_frame_mutex);
+
+  // Note: We don't create input_frames here - it will be created lazily
+  // by SyntheticStackFrameList when frames are first fetched.
+  // Creating them too early can cause crashes during thread initialization.
+
+  // Create a temporary StackFrameList just to get the thread reference for the
+  // provider. The provider won't actually use this - it will get real input
+  // frames from SyntheticStackFrameList later.
+  StackFrameListSP temp_frames =
+      std::make_shared<StackFrameList>(*this, m_prev_frames_sp, true);
+
+  auto provider_or_err =
+      SyntheticFrameProvider::CreateInstance(temp_frames, descriptor);
+  if (!provider_or_err)
+    return provider_or_err.takeError();
+
+  ClearScriptedFrameProvider();
+  m_frame_provider_sp = *provider_or_err;
+  return llvm::Error::success();
+}
+
+void Thread::ClearScriptedFrameProvider() {
+  std::lock_guard<std::recursive_mutex> guard(m_frame_mutex);
+  m_frame_provider_sp.reset();
+  m_curr_frames_sp.reset();
+  m_prev_frames_sp.reset();
+}
+
 std::optional<addr_t> Thread::GetPreviousFrameZeroPC() {
   return m_prev_framezero_pc;
 }
@@ -1466,6 +1535,7 @@ void Thread::ClearStackFrames() {
     m_prev_frames_sp.swap(m_curr_frames_sp);
   m_curr_frames_sp.reset();
 
+  m_frame_provider_sp.reset();
   m_extended_info.reset();
   m_extended_info_fetched = false;
 }
diff --git a/lldb/source/Target/ThreadSpec.cpp b/lldb/source/Target/ThreadSpec.cpp
index ba4c3aa894553..624f64e3af800 100644
--- a/lldb/source/Target/ThreadSpec.cpp
+++ b/lldb/source/Target/ThreadSpec.cpp
@@ -19,6 +19,10 @@ const char *ThreadSpec::g_option_names[static_cast<uint32_t>(
 
 ThreadSpec::ThreadSpec() : m_name(), m_queue_name() {}
 
+ThreadSpec::ThreadSpec(Thread &thread)
+    : m_index(thread.GetIndexID()), m_tid(thread.GetID()),
+      m_name(thread.GetName()), m_queue_name(thread.GetQueueName()) {}
+
 std::unique_ptr<ThreadSpec> ThreadSpec::CreateFromStructuredData(
     const StructuredData::Dictionary &spec_dict, Status &error) {
   uint32_t index = UINT32_MAX;
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/Makefile b/lldb/test/API/functionalities/scripted_frame_provider/Makefile
new file mode 100644
index 0000000000000..99998b20bcb05
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/Makefile
@@ -0,0 +1,3 @@
+CXX_SOURCES := main.cpp
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
new file mode 100644
index 0000000000000..189ca2f147f9d
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
@@ -0,0 +1,339 @@
+"""
+Test scripted frame provider functionality.
+"""
+
+import os
+
+import lldb
+from lldbsuite.test.lldbtest import TestBase
+from lldbsuite.test import lldbutil
+
+
+class ScriptedFrameProviderTestCase(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def setUp(self):
+        TestBase.setUp(self)
+        self.source = "main.cpp"
+
+    def test_replace_all_frames(self):
+        """Test that we can replace the entire stack."""
+        self.build()
+        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
+            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
+        )
+
+        # Import the test frame provider
+        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
+        self.runCmd("command script import " + script_path)
+
+        # Attach the Replace provider
+        error = lldb.SBError()
+        provider_id = target.RegisterScriptedFrameProvider(
+            "test_frame_providers.ReplaceFrameProvider",
+            lldb.SBStructuredData(),
+            error,
+        )
+        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
+        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+        # Verify we have exactly 3 synthetic frames
+        self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
+
+        # Verify frame indices and PCs (dictionary-based frames don't have custom function names)
+        frame0 = thread.GetFrameAtIndex(0)
+        self.assertIsNotNone(frame0)
+        self.assertEqual(frame0.GetPC(), 0x1000)
+
+        frame1 = thread.GetFrameAtIndex(1)
+        self.assertIsNotNone(frame1)
+        self.assertIn("thread_func", frame1.GetFunctionName())
+
+        frame2 = thread.GetFrameAtIndex(2)
+        self.assertIsNotNone(frame2)
+        self.assertEqual(frame2.GetPC(), 0x3000)
+
+    def test_prepend_frames(self):
+        """Test that we can add frames before real stack."""
+        self.build()
+        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
+            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
+        )
+
+        # Get original frame count and PC
+        original_frame_count = thread.GetNumFrames()
+        self.assertGreaterEqual(
+            original_frame_count, 2, "Should have at least 2 real frames"
+        )
+
+        # Import and attach Prepend provider
+        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
+        self.runCmd("command script import " + script_path)
+
+        error = lldb.SBError()
+        provider_id = target.RegisterScriptedFrameProvider(
+            "test_frame_providers.PrependFrameProvider",
+            lldb.SBStructuredData(),
+            error,
+        )
+        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
+        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+        # Verify we have 2 more frames
+        new_frame_count = thread.GetNumFrames()
+        self.assertEqual(new_frame_count, original_frame_count + 2)
+
+        # Verify first 2 frames are synthetic (check PCs, not function names)
+        frame0 = thread.GetFrameAtIndex(0)
+        self.assertEqual(frame0.GetPC(), 0x9000)
+
+        frame1 = thread.GetFrameAtIndex(1)
+        self.assertEqual(frame1.GetPC(), 0xA000)
+
+        # Verify frame 2 is the original real frame 0
+        frame2 = thread.GetFrameAtIndex(2)
+        self.assertIn("thread_func", frame2.GetFunctionName())
+
+    def test_append_frames(self):
+        """Test that we can add frames after real stack."""
+        self.build()
+        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
+            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
+        )
+
+        # Get original frame count
+        original_frame_count = thread.GetNumFrames()
+
+        # Import and attach Append provider
+        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
+        self.runCmd("command script import " + script_path)
+
+        error = lldb.SBError()
+        provider_id = target.RegisterScriptedFrameProvider(
+            "test_frame_providers.AppendFrameProvider",
+            lldb.SBStructuredData(),
+            error,
+        )
+        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
+        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+        # Verify we have 1 more frame
+        new_frame_count = thread.GetNumFrames()
+        self.assertEqual(new_frame_count, original_frame_count + 1)
+
+        # Verify first frames are still real
+        frame0 = thread.GetFrameAtIndex(0)
+        self.assertIn("thread_func", frame0.GetFunctionName())
+
+        frame_n_plus_1 = thread.GetFrameAtIndex(new_frame_count - 1)
+        self.assertEqual(frame_n_plus_1.GetPC(), 0x10)
+
+    def test_scripted_frame_objects(self):
+        """Test that provider can return ScriptedFrame objects."""
+        self.build()
+        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
+            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
+        )
+
+        # Import the provider that returns ScriptedFrame objects
+        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
+        self.runCmd("command script import " + script_path)
+
+        error = lldb.SBError()
+        provider_id = target.RegisterScriptedFrameProvider(
+            "test_frame_providers.ScriptedFrameObjectProvider",
+            lldb.SBStructuredData(),
+            error,
+        )
+        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
+        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+        # Verify we have 5 frames
+        self.assertEqual(
+            thread.GetNumFrames(), 5, "Should have 5 custom scripted frames"
+        )
+
+        # Verify frame properties from CustomScriptedFrame
+        frame0 = thread.GetFrameAtIndex(0)
+        self.assertIsNotNone(frame0)
+        self.assertEqual(frame0.GetFunctionName(), "custom_scripted_frame_0")
+        self.assertEqual(frame0.GetPC(), 0x5000)
+        self.assertTrue(frame0.IsSynthetic(), "Frame should be marked as synthetic")
+
+        frame1 = thread.GetFrameAtIndex(1)
+        self.assertIsNotNone(frame1)
+        self.assertEqual(frame1.GetPC(), 0x6000)
+
+        frame2 = thread.GetFrameAtIndex(2)
+        self.assertIsNotNone(frame2)
+        self.assertEqual(frame2.GetFunctionName(), "custom_scripted_frame_2")
+        self.assertEqual(frame2.GetPC(), 0x7000)
+        self.assertTrue(frame2.IsSynthetic(), "Frame should be marked as synthetic")
+
+    def test_applies_to_thread(self):
+        """Test that applies_to_thread filters which threads get the provider."""
+        self.build()
+        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
+            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
+        )
+
+        # We should have at least 2 threads (worker threads) at the breakpoint
+        num_threads = process.GetNumThreads()
+        self.assertGreaterEqual(
+            num_threads, 2, "Should have at least 2 threads at breakpoint"
+        )
+
+        # Import the test frame provider
+        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
+        self.runCmd("command script import " + script_path)
+
+        # Collect original thread info before applying provider
+        thread_info = {}
+        for i in range(num_threads):
+            t = process.GetThreadAtIndex(i)
+            thread_info[t.GetIndexID()] = {
+                "frame_count": t.GetNumFrames(),
+                "pc": t.GetFrameAtIndex(0).GetPC(),
+            }
+
+        # Register the ThreadFilterFrameProvider which only applies to thread ID 1
+        error = lldb.SBError()
+        provider_id = target.RegisterScriptedFrameProvider(
+            "test_frame_providers.ThreadFilterFrameProvider",
+            lldb.SBStructuredData(),
+            error,
+        )
+        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
+        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+        # Check each thread
+        thread_id_1_found = False
+        for i in range(num_threads):
+            t = process.GetThreadAtIndex(i)
+            thread_id = t.GetIndexID()
+
+            if thread_id == 1:
+                # Thread with ID 1 should have synthetic frame
+                thread_id_1_found = True
+                self.assertEqual(
+                    t.GetNumFrames(),
+                    1,
+                    f"Thread with ID 1 should have 1 synthetic frame",
+                )
+                self.assertEqual(
+                    t.GetFrameAtIndex(0).GetPC(),
+                    0xFFFF,
+                    f"Thread with ID 1 should have synthetic PC 0xFFFF",
+                )
+            else:
+                # Other threads should keep their original frames
+                self.assertEqual(
+                    t.GetNumFrames(),
+                    thread_info[thread_id]["frame_count"],
+                    f"Thread with ID {thread_id} should not be affected by provider",
+                )
+                self.assertEqual(
+                    t.GetFrameAtIndex(0).GetPC(),
+                    thread_info[thread_id]["pc"],
+                    f"Thread with ID {thread_id} should have its original PC",
+                )
+
+        # We should have found at least one thread with ID 1
+        self.assertTrue(
+            thread_id_1_found,
+            "Should have found a thread with ID 1 to test filtering",
+        )
+
+    def test_remove_frame_provider_by_id(self):
+        """Test that RemoveScriptedFrameProvider removes a specific provider by ID."""
+        self.build()
+        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
+            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
+        )
+
+        # Import the test frame providers
+        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
+        self.runCmd("command script import " + script_path)
+
+        # Get original frame count
+        original_frame_count = thread.GetNumFrames()
+        original_pc = thread.GetFrameAtIndex(0).GetPC()
+
+        # Register the first provider and get its ID
+        error = lldb.SBError()
+        provider_id_1 = target.RegisterScriptedFrameProvider(
+            "test_frame_providers.ReplaceFrameProvider",
+            lldb.SBStructuredData(),
+            error,
+        )
+        self.assertTrue(error.Success(), f"Failed to register provider 1: {error}")
+
+        # Verify first provider is active (3 synthetic frames)
+        self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
+        self.assertEqual(
+            thread.GetFrameAtIndex(0).GetPC(), 0x1000, "Should have first provider's PC"
+        )
+
+        # Register a second provider and get its ID
+        provider_id_2 = target.RegisterScriptedFrameProvider(
+            "test_frame_providers.PrependFrameProvider",
+            lldb.SBStructuredData(),
+            error,
+        )
+        self.assertTrue(error.Success(), f"Failed to register provider 2: {error}")
+
+        # Verify IDs are different
+        self.assertNotEqual(
+            provider_id_1, provider_id_2, "Provider IDs should be unique"
+        )
+
+        # Now remove the first provider by ID
+        result = target.RemoveScriptedFrameProvider(provider_id_1)
+        self.assertSuccess(
+            result, f"Should successfully remove provider with ID {provider_id_1}"
+        )
+
+        # After removing the first provider, the second provider should still be active
+        # The PrependFrameProvider adds 2 frames before the real stack
+        # Since ReplaceFrameProvider had 3 frames, and we removed it, we should now
+        # have the original frames (from real stack) with PrependFrameProvider applied
+        new_frame_count = thread.GetNumFrames()
+        self.assertEqual(
+            new_frame_count,
+            original_frame_count + 2,
+            "Should have original frames + 2 prepended frames",
+        )
+
+        # First two frames should be from PrependFrameProvider
+        self.assertEqual(
+            thread.GetFrameAtIndex(0).GetPC(),
+            0x9000,
+            "First frame should be from PrependFrameProvider",
+        )
+        self.assertEqual(
+            thread.GetFrameAtIndex(1).GetPC(),
+            0xA000,
+            "Second frame should be from PrependFrameProvider",
+        )
+
+        # Remove the second provider
+        result = target.RemoveScriptedFrameProvider(provider_id_2)
+        self.assertSuccess(
+            result, f"Should successfully remove provider with ID {provider_id_2}"
+        )
+
+        # After removing both providers, frames should be back to original
+        self.assertEqual(
+            thread.GetNumFrames(),
+            original_frame_count,
+            "Should restore original frame count",
+        )
+        self.assertEqual(
+            thread.GetFrameAtIndex(0).GetPC(),
+            original_pc,
+            "Should restore original PC",
+        )
+
+        # Try to remove a provider that doesn't exist
+        result = target.RemoveScriptedFrameProvider(999999)
+        self.assertTrue(result.Fail(), "Should fail to remove non-existent provider")
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/main.cpp b/lldb/test/API/functionalities/scripted_frame_provider/main.cpp
new file mode 100644
index 0000000000000..e92ed18014dd3
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/main.cpp
@@ -0,0 +1,58 @@
+// Multi-threaded test program for testing frame providers
+
+#include <condition_variable>
+#include <iostream>
+#include <mutex>
+#include <thread>
+
+std::mutex mtx;
+std::condition_variable cv;
+int ready_count = 0;
+constexpr int NUM_THREADS = 2;
+
+void thread_func(int thread_num) {
+  std::cout << "Thread " << thread_num << " started\n";
+
+  // Wait for all threads to start
+  {
+    std::unique_lock<std::mutex> lock(mtx);
+    ready_count++;
+    if (ready_count == NUM_THREADS + 1) {
+      cv.notify_all();
+    } else {
+      cv.wait(lock, [] { return ready_count == NUM_THREADS + 1; });
+    }
+  }
+
+  std::cout << "Thread " << thread_num << " at breakpoint\n"; // Break here
+}
+
+int main(int argc, char **argv) {
+  std::thread threads[NUM_THREADS];
+
+  // Create threads
+  for (int i = 0; i < NUM_THREADS; i++) {
+    threads[i] = std::thread(thread_func, i);
+  }
+
+  // Wait for all threads to start
+  {
+    std::unique_lock<std::mutex> lock(mtx);
+    ready_count++;
+    if (ready_count == NUM_THREADS + 1) {
+      cv.notify_all();
+    } else {
+      cv.wait(lock, [] { return ready_count == NUM_THREADS + 1; });
+    }
+  }
+
+  std::cout << "Main thread at barrier\n"; // All threads are now at breakpoint
+
+  // Join threads
+  for (int i = 0; i < NUM_THREADS; i++) {
+    threads[i].join();
+  }
+
+  std::cout << "All threads completed\n";
+  return 0;
+}
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py b/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py
new file mode 100644
index 0000000000000..91aa13e44339a
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py
@@ -0,0 +1,176 @@
+"""
+Test frame providers for scripted frame provider functionality.
+
+These providers demonstrate various merge strategies:
+- Replace: Replace entire stack
+- Prepend: Add frames before real stack
+- Append: Add frames after real stack
+
+It also shows the ability to mix a dictionary, a ScriptedFrame or an SBFrame
+index to create stackframes
+"""
+
+import lldb
+from lldb.plugins.scripted_process import ScriptedFrame
+from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
+
+
+class ReplaceFrameProvider(ScriptedFrameProvider):
+    """Replace entire stack with custom frames."""
+
+    def __init__(self, input_frames, args):
+        super().__init__(input_frames, args)
+        self.frames = [
+            {
+                "idx": 0,
+                "pc": 0x1000,
+            },
+            0,
+            {
+                "idx": 2,
+                "pc": 0x3000,
+            },
+        ]
+
+    @staticmethod
+    def get_description():
+        """Return a description of this provider."""
+        return "Replace entire stack with 3 custom frames"
+
+    def get_frame_at_index(self, index):
+        if index >= len(self.frames):
+            return None
+        return self.frames[index]
+
+
+class PrependFrameProvider(ScriptedFrameProvider):
+    """Prepend synthetic frames before real stack."""
+
+    def __init__(self, input_frames, args):
+        super().__init__(input_frames, args)
+
+    @staticmethod
+    def get_description():
+        """Return a description of this provider."""
+        return "Prepend 2 synthetic frames before real stack"
+
+    def get_frame_at_index(self, index):
+        if index == 0:
+            return {"pc": 0x9000}
+        elif index == 1:
+            return {"pc": 0xA000}
+        elif index - 2 < len(self.input_frames):
+            return index - 2  # Return real frame index
+        return None
+
+
+class AppendFrameProvider(ScriptedFrameProvider):
+    """Append synthetic frames after real stack."""
+
+    def __init__(self, input_frames, args):
+        super().__init__(input_frames, args)
+
+    @staticmethod
+    def get_description():
+        """Return a description of this provider."""
+        return "Append 1 synthetic frame after real stack"
+
+    def get_frame_at_index(self, index):
+        if index < len(self.input_frames):
+            return index  # Return real frame index
+        elif index == len(self.input_frames):
+            return {
+                "idx": 1,
+                "pc": 0x10,
+            }
+        return None
+
+
+class CustomScriptedFrame(ScriptedFrame):
+    """Custom scripted frame with full control over frame behavior."""
+
+    def __init__(self, thread, idx, pc, function_name):
+        # Initialize structured data args
+        args = lldb.SBStructuredData()
+        super().__init__(thread, args)
+
+        self.idx = idx
+        self.pc = pc
+        self.function_name = function_name
+
+    def get_id(self):
+        """Return the frame index."""
+        return self.idx
+
+    def get_pc(self):
+        """Return the program counter."""
+        return self.pc
+
+    def get_function_name(self):
+        """Return the function name."""
+        return self.function_name
+
+    def is_artificial(self):
+        """Mark as artificial frame."""
+        return False
+
+    def is_hidden(self):
+        """Not hidden."""
+        return False
+
+    def get_register_context(self):
+        """No register context for this test."""
+        return None
+
+
+class ScriptedFrameObjectProvider(ScriptedFrameProvider):
+    """Provider that returns ScriptedFrame objects instead of dictionaries."""
+
+    def __init__(self, input_frames, args):
+        super().__init__(input_frames, args)
+
+    @staticmethod
+    def get_description():
+        """Return a description of this provider."""
+        return "Provider returning custom ScriptedFrame objects"
+
+    def get_frame_at_index(self, index):
+        """Return ScriptedFrame objects or dictionaries based on index."""
+        if index == 0:
+            return CustomScriptedFrame(
+                self.thread, 0, 0x5000, "custom_scripted_frame_0"
+            )
+        elif index == 1:
+            return {"pc": 0x6000}
+        elif index == 2:
+            return CustomScriptedFrame(
+                self.thread, 2, 0x7000, "custom_scripted_frame_2"
+            )
+        elif index == 3:
+            return len(self.input_frames) - 2  # Real frame index
+        elif index == 4:
+            return len(self.input_frames) - 1  # Real frame index
+        return None
+
+
+class ThreadFilterFrameProvider(ScriptedFrameProvider):
+    """Provider that only applies to thread with ID 1."""
+
+    @staticmethod
+    def applies_to_thread(thread):
+        """Only apply to thread with index ID 1."""
+        return thread.GetIndexID() == 1
+
+    def __init__(self, input_frames, args):
+        super().__init__(input_frames, args)
+
+    @staticmethod
+    def get_description():
+        """Return a description of this provider."""
+        return "Provider that only applies to thread ID 1"
+
+    def get_frame_at_index(self, index):
+        """Return a single synthetic frame."""
+        if index == 0:
+            return {"pc": 0xFFFF}
+        return None
diff --git a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
index a63b740d9472f..5694aeeff3e5b 100644
--- a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
+++ b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
@@ -136,6 +136,11 @@ lldb_private::python::LLDBSWIGPython_CastPyObjectToSBStream(PyObject *data) {
   return nullptr;
 }
 
+void *
+lldb_private::python::LLDBSWIGPython_CastPyObjectToSBThread(PyObject *data) {
+  return nullptr;
+}
+
 void *
 lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFrame(PyObject *data) {
   return nullptr;



More information about the lldb-commits mailing list