Author: Med Ismail Bennani
Date: 2026-01-17T04:10:48Z
New Revision: 17b01bbc67c484068e174d9e9a80089102bea0e8

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

LOG: [lldb] Enable chaining multiple scripted frame providers per thread 
(#172849)

This patch allows threads to have multiple SyntheticFrameProviderSP
instances that chain together sequentially. Each provider receives the
output of the previous provider as input, creating a transformation
pipeline.

It changes `Thread::m_frame_provider_sp` to a vector, adds provider
parameter to SyntheticStackFrameList to avoid calling back into
`Thread::GetFrameProvider()` during frame fetching, updated
`LoadScriptedFrameProvider()` to chain providers by wrapping each
previous provider's output in a `SyntheticStackFrameList` for the next
provider and finally, loads ALL matching providers in priority order
instead of just the first one.

The chaining works as follows:
```
  Real Unwinder Frames
      ↓
  Provider 1 (priority 10) → adds/transforms frames
      ↓
  Provider 2 (priority 20) → transforms Provider 1's output
      ↓
  Provider 3 (priority 30) → transforms Provider 2's output
      ↓
  Final frame list shown to user
  ```

This patch also adds a test for this (test_chained_frame_providers) to verify 
that 3 providers chain correctly: `AddFooFrameProvider`, `AddBarFrameProvider`, 
`AddBazFrameProvider`.

Signed-off-by: Med Ismail Bennani <[email protected]>

Added: 
    

Modified: 
    lldb/include/lldb/Target/StackFrameList.h
    lldb/include/lldb/Target/Thread.h
    lldb/source/Target/StackFrameList.cpp
    lldb/source/Target/Thread.cpp
    
lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
    
lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py

Removed: 
    


################################################################################
diff  --git a/lldb/include/lldb/Target/StackFrameList.h 
b/lldb/include/lldb/Target/StackFrameList.h
index 539c070ff0f4b..c096fe3ff61a0 100644
--- a/lldb/include/lldb/Target/StackFrameList.h
+++ b/lldb/include/lldb/Target/StackFrameList.h
@@ -243,7 +243,8 @@ class SyntheticStackFrameList : public StackFrameList {
 public:
   SyntheticStackFrameList(Thread &thread, lldb::StackFrameListSP input_frames,
                           const lldb::StackFrameListSP &prev_frames_sp,
-                          bool show_inline_frames);
+                          bool show_inline_frames,
+                          lldb::SyntheticFrameProviderSP provider_sp);
 
 protected:
   /// Override FetchFramesUpTo to lazily return frames from the provider
@@ -255,6 +256,9 @@ class SyntheticStackFrameList : public StackFrameList {
   /// The input stack frame list that the provider transforms.
   /// This could be a real StackFrameList or another SyntheticStackFrameList.
   lldb::StackFrameListSP m_input_frames;
+
+  /// The provider that transforms the input frames.
+  lldb::SyntheticFrameProviderSP m_provider;
 };
 
 } // namespace lldb_private

diff  --git a/lldb/include/lldb/Target/Thread.h 
b/lldb/include/lldb/Target/Thread.h
index 46ce192556756..bc1bec57bee5f 100644
--- a/lldb/include/lldb/Target/Thread.h
+++ b/lldb/include/lldb/Target/Thread.h
@@ -1302,8 +1302,9 @@ class Thread : public 
std::enable_shared_from_this<Thread>,
 
   void ClearScriptedFrameProvider();
 
-  lldb::SyntheticFrameProviderSP GetFrameProvider() const {
-    return m_frame_provider_sp;
+  const llvm::SmallVector<lldb::SyntheticFrameProviderSP, 0> &
+  GetFrameProviders() const {
+    return m_frame_providers;
   }
 
 protected:
@@ -1409,8 +1410,8 @@ 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;
+  /// The Scripted Frame Providers for this thread.
+  llvm::SmallVector<lldb::SyntheticFrameProviderSP, 0> m_frame_providers;
 
 private:
   bool m_extended_info_fetched; // Have we tried to retrieve the 
m_extended_info

diff  --git a/lldb/source/Target/StackFrameList.cpp 
b/lldb/source/Target/StackFrameList.cpp
index 896a760f61d26..e6112f8f3264b 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -58,23 +58,24 @@ StackFrameList::~StackFrameList() {
 
 SyntheticStackFrameList::SyntheticStackFrameList(
     Thread &thread, lldb::StackFrameListSP input_frames,
-    const lldb::StackFrameListSP &prev_frames_sp, bool show_inline_frames)
+    const lldb::StackFrameListSP &prev_frames_sp, bool show_inline_frames,
+    lldb::SyntheticFrameProviderSP provider_sp)
     : StackFrameList(thread, prev_frames_sp, show_inline_frames),
-      m_input_frames(std::move(input_frames)) {}
+      m_input_frames(std::move(input_frames)),
+      m_provider(std::move(provider_sp)) {}
 
 bool SyntheticStackFrameList::FetchFramesUpTo(
     uint32_t end_idx, InterruptionControl allow_interrupt) {
 
   size_t num_synthetic_frames = 0;
-  // 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.
+  // Use the provider to generate frames lazily.
+  if (m_provider) {
     // Keep fetching until we reach end_idx or the provider returns an error.
     for (uint32_t idx = m_frames.size(); idx <= end_idx; idx++) {
       if (allow_interrupt &&
           
m_thread.GetProcess()->GetTarget().GetDebugger().InterruptRequested())
         return true;
-      auto frame_or_err = provider_sp->GetFrameAtIndex(idx);
+      auto frame_or_err = m_provider->GetFrameAtIndex(idx);
       if (!frame_or_err) {
         // Provider returned error - we've reached the end.
         LLDB_LOG_ERROR(GetLog(LLDBLog::Thread), frame_or_err.takeError(),

diff  --git a/lldb/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp
index ed65e304fc5f2..70d8650662348 100644
--- a/lldb/source/Target/Thread.cpp
+++ b/lldb/source/Target/Thread.cpp
@@ -262,7 +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_frame_providers.clear();
   m_prev_framezero_pc.reset();
 }
 
@@ -1457,8 +1457,8 @@ StackFrameListSP Thread::GetStackFrameList() {
   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) {
+  // First, try to load frame providers if we don't have any yet.
+  if (m_frame_providers.empty()) {
     ProcessSP process_sp = GetProcess();
     if (process_sp) {
       Target &target = process_sp->GetTarget();
@@ -1483,24 +1483,24 @@ StackFrameListSP Thread::GetStackFrameList() {
                    return priority_a < priority_b;
                  });
 
-      // Load the highest priority provider that successfully instantiates.
+      // Load ALL matching providers in priority order.
       for (const auto *descriptor : applicable_descriptors) {
         if (llvm::Error error = LoadScriptedFrameProvider(*descriptor)) {
           LLDB_LOG_ERROR(GetLog(LLDBLog::Thread), std::move(error),
                          "Failed to load scripted frame provider: {0}");
           continue; // Try next provider if this one fails.
         }
-        break; // Successfully loaded provider.
       }
     }
   }
 
-  // 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();
+  // Create the frame list based on whether we have providers.
+  if (!m_frame_providers.empty()) {
+    // We have providers - use the last one in the chain.
+    // The last provider has already been chained with all previous providers.
+    StackFrameListSP input_frames = m_frame_providers.back()->GetInputFrames();
     m_curr_frames_sp = std::make_shared<SyntheticStackFrameList>(
-        *this, input_frames, m_prev_frames_sp, true);
+        *this, input_frames, m_prev_frames_sp, true, m_frame_providers.back());
   } else {
     // No provider - use normal unwinder frames.
     m_curr_frames_sp =
@@ -1514,29 +1514,39 @@ 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);
+  // Create input frames for this provider:
+  // - If no providers exist yet, use real unwinder frames.
+  // - If providers exist, wrap the previous provider in a
+  // SyntheticStackFrameList.
+  //   This creates the chain: each provider's OUTPUT becomes the next
+  //   provider's INPUT.
+  StackFrameListSP new_provider_input_frames;
+  if (m_frame_providers.empty()) {
+    // First provider gets real unwinder frames.
+    new_provider_input_frames =
+        std::make_shared<StackFrameList>(*this, m_prev_frames_sp, true);
+  } else {
+    // Subsequent providers get the previous provider's OUTPUT.
+    // We create a SyntheticStackFrameList that wraps the previous provider.
+    SyntheticFrameProviderSP prev_provider = m_frame_providers.back();
+    StackFrameListSP prev_provider_frames = prev_provider->GetInputFrames();
+    new_provider_input_frames = std::make_shared<SyntheticStackFrameList>(
+        *this, prev_provider_frames, m_prev_frames_sp, true, prev_provider);
+  }
 
-  auto provider_or_err =
-      SyntheticFrameProvider::CreateInstance(temp_frames, descriptor);
+  auto provider_or_err = SyntheticFrameProvider::CreateInstance(
+      new_provider_input_frames, descriptor);
   if (!provider_or_err)
     return provider_or_err.takeError();
 
-  ClearScriptedFrameProvider();
-  m_frame_provider_sp = *provider_or_err;
+  // Append to the chain.
+  m_frame_providers.push_back(*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_frame_providers.clear();
   m_curr_frames_sp.reset();
   m_prev_frames_sp.reset();
 }
@@ -1561,7 +1571,7 @@ void Thread::ClearStackFrames() {
     m_prev_frames_sp.swap(m_curr_frames_sp);
   m_curr_frames_sp.reset();
 
-  m_frame_provider_sp.reset();
+  m_frame_providers.clear();
   m_extended_info.reset();
   m_extended_info_fetched = false;
 }

diff  --git 
a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
 
b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
index 0a5b9d9b83951..ceca64a450686 100644
--- 
a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
+++ 
b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
@@ -638,3 +638,95 @@ def test_valid_pc_no_module_frames(self):
         frame2 = thread.GetFrameAtIndex(2)
         self.assertIsNotNone(frame2)
         self.assertIn("thread_func", frame2.GetFunctionName())
+
+    def test_chained_frame_providers(self):
+        """Test that multiple frame providers chain together."""
+        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()
+        self.assertGreaterEqual(
+            original_frame_count, 2, "Should have at least 2 real frames"
+        )
+
+        # Import the test frame providers.
+        script_path = os.path.join(self.getSourceDir(), 
"test_frame_providers.py")
+        self.runCmd("command script import " + script_path)
+
+        # Register 3 providers with 
diff erent priorities.
+        # Each provider adds 1 frame at the beginning.
+        error = lldb.SBError()
+
+        # Provider 1: Priority 10 - adds "foo" frame
+        provider_id_1 = target.RegisterScriptedFrameProvider(
+            "test_frame_providers.AddFooFrameProvider",
+            lldb.SBStructuredData(),
+            error,
+        )
+        self.assertTrue(error.Success(), f"Failed to register foo provider: 
{error}")
+
+        # Provider 2: Priority 20 - adds "bar" frame
+        provider_id_2 = target.RegisterScriptedFrameProvider(
+            "test_frame_providers.AddBarFrameProvider",
+            lldb.SBStructuredData(),
+            error,
+        )
+        self.assertTrue(error.Success(), f"Failed to register bar provider: 
{error}")
+
+        # Provider 3: Priority 30 - adds "baz" frame
+        provider_id_3 = target.RegisterScriptedFrameProvider(
+            "test_frame_providers.AddBazFrameProvider",
+            lldb.SBStructuredData(),
+            error,
+        )
+        self.assertTrue(error.Success(), f"Failed to register baz provider: 
{error}")
+
+        # Verify we have 3 more frames (one from each provider).
+        new_frame_count = thread.GetNumFrames()
+        self.assertEqual(
+            new_frame_count,
+            original_frame_count + 3,
+            "Should have original frames + 3 chained frames",
+        )
+
+        # Verify the chaining order: baz, bar, foo, then real frames.
+        # Since priority is lower = higher, the order should be:
+        # Provider 1 (priority 10) transforms real frames first -> adds "foo"
+        # Provider 2 (priority 20) transforms Provider 1's output -> adds "bar"
+        # Provider 3 (priority 30) transforms Provider 2's output -> adds "baz"
+        # So final stack is: baz, bar, foo, real frames...
+
+        frame0 = thread.GetFrameAtIndex(0)
+        self.assertIsNotNone(frame0)
+        self.assertEqual(
+            frame0.GetFunctionName(),
+            "baz",
+            "Frame 0 should be 'baz' from last provider in chain",
+        )
+        self.assertEqual(frame0.GetPC(), 0xBAD)
+
+        frame1 = thread.GetFrameAtIndex(1)
+        self.assertIsNotNone(frame1)
+        self.assertEqual(
+            frame1.GetFunctionName(),
+            "bar",
+            "Frame 1 should be 'bar' from second provider in chain",
+        )
+        self.assertEqual(frame1.GetPC(), 0xBAB)
+
+        frame2 = thread.GetFrameAtIndex(2)
+        self.assertIsNotNone(frame2)
+        self.assertEqual(
+            frame2.GetFunctionName(),
+            "foo",
+            "Frame 2 should be 'foo' from first provider in chain",
+        )
+        self.assertEqual(frame2.GetPC(), 0xF00)
+
+        # Frame 3 should be the original real frame 0.
+        frame3 = thread.GetFrameAtIndex(3)
+        self.assertIsNotNone(frame3)
+        self.assertIn("thread_func", frame3.GetFunctionName())

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
index e4367192af50d..e97d11f173045 100644
--- 
a/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py
+++ 
b/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py
@@ -380,3 +380,81 @@ def get_frame_at_index(self, index):
             # Pass through original frames
             return index - 2
         return None
+
+
+class AddFooFrameProvider(ScriptedFrameProvider):
+    """Add a single 'foo' frame at the beginning."""
+
+    def __init__(self, input_frames, args):
+        super().__init__(input_frames, args)
+
+    @staticmethod
+    def get_description():
+        """Return a description of this provider."""
+        return "Add 'foo' frame at beginning"
+
+    @staticmethod
+    def get_priority():
+        """Return priority 10 (runs first in chain)."""
+        return 10
+
+    def get_frame_at_index(self, index):
+        if index == 0:
+            # Return synthetic "foo" frame
+            return CustomScriptedFrame(self.thread, 0, 0xF00, "foo")
+        elif index - 1 < len(self.input_frames):
+            # Pass through input frames (shifted by 1)
+            return index - 1
+        return None
+
+
+class AddBarFrameProvider(ScriptedFrameProvider):
+    """Add a single 'bar' frame at the beginning."""
+
+    def __init__(self, input_frames, args):
+        super().__init__(input_frames, args)
+
+    @staticmethod
+    def get_description():
+        """Return a description of this provider."""
+        return "Add 'bar' frame at beginning"
+
+    @staticmethod
+    def get_priority():
+        """Return priority 20 (runs second in chain)."""
+        return 20
+
+    def get_frame_at_index(self, index):
+        if index == 0:
+            # Return synthetic "bar" frame
+            return CustomScriptedFrame(self.thread, 0, 0xBAB, "bar")
+        elif index - 1 < len(self.input_frames):
+            # Pass through input frames (shifted by 1)
+            return index - 1
+        return None
+
+
+class AddBazFrameProvider(ScriptedFrameProvider):
+    """Add a single 'baz' frame at the beginning."""
+
+    def __init__(self, input_frames, args):
+        super().__init__(input_frames, args)
+
+    @staticmethod
+    def get_description():
+        """Return a description of this provider."""
+        return "Add 'baz' frame at beginning"
+
+    @staticmethod
+    def get_priority():
+        """Return priority 30 (runs last in chain)."""
+        return 30
+
+    def get_frame_at_index(self, index):
+        if index == 0:
+            # Return synthetic "baz" frame
+            return CustomScriptedFrame(self.thread, 0, 0xBAD, "baz")
+        elif index - 1 < len(self.input_frames):
+            # Pass through input frames (shifted by 1)
+            return index - 1
+        return None


        
_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to