[Lldb-commits] [lldb] [lldb] Enable chaining multiple scripted frame providers per thread (PR #172849)
Med Ismail Bennani via lldb-commits
lldb-commits at lists.llvm.org
Fri Dec 19 04:07:20 PST 2025
https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/172849
>From 53791c7a2c24c8402ae69f4a2611ddf88843f387 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Thu, 18 Dec 2025 14:11:57 +0100
Subject: [PATCH] [lldb] Enable chaining multiple scripted frame providers per
thread
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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 <ismail at bennani.ma>
---
lldb/include/lldb/Target/StackFrameList.h | 6 +-
lldb/include/lldb/Target/Thread.h | 8 +-
lldb/source/Target/StackFrameList.cpp | 13 +--
lldb/source/Target/Thread.cpp | 58 +++++++-----
.../TestScriptedFrameProvider.py | 92 +++++++++++++++++++
.../test_frame_providers.py | 78 ++++++++++++++++
6 files changed, 220 insertions(+), 35 deletions(-)
diff --git a/lldb/include/lldb/Target/StackFrameList.h b/lldb/include/lldb/Target/StackFrameList.h
index 539c070ff0f4b..42a0be9b196b9 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);
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..808bb024d4d64 100644
--- a/lldb/include/lldb/Target/Thread.h
+++ b/lldb/include/lldb/Target/Thread.h
@@ -1302,8 +1302,8 @@ class Thread : public std::enable_shared_from_this<Thread>,
void ClearScriptedFrameProvider();
- lldb::SyntheticFrameProviderSP GetFrameProvider() const {
- return m_frame_provider_sp;
+ const std::vector<lldb::SyntheticFrameProviderSP> &GetFrameProviders() const {
+ return m_frame_providers;
}
protected:
@@ -1409,8 +1409,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.
+ std::vector<lldb::SyntheticFrameProviderSP> 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..b1af2bb65494a 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)
: 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)) {
+}
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 b833918c27818..9fdd6724843e4 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();
}
@@ -1448,8 +1448,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();
@@ -1474,24 +1474,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 =
@@ -1505,29 +1505,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 input_frames;
+ if (m_frame_providers.empty()) {
+ // First provider gets real unwinder frames
+ 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_input = prev_provider->GetInputFrames();
+ input_frames = std::make_shared<SyntheticStackFrameList>(
+ *this, prev_input, m_prev_frames_sp, true, prev_provider);
+ }
auto provider_or_err =
- SyntheticFrameProvider::CreateInstance(temp_frames, descriptor);
+ SyntheticFrameProvider::CreateInstance(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();
}
@@ -1552,7 +1562,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 different 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
More information about the lldb-commits
mailing list