From 671e287b9c4c78e6d58494bc3ec80b941092c60c Mon Sep 17 00:00:00 2001 From: Med Ismail Bennani Date: Fri, 16 Jan 2026 20:10:48 -0800 Subject: [PATCH 1/7] [lldb] Enable chaining multiple scripted frame providers per thread (#172849) 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 (cherry picked from commit 17b01bbc67c484068e174d9e9a80089102bea0e8) --- lldb/include/lldb/Target/StackFrameList.h | 6 +- lldb/include/lldb/Target/Thread.h | 9 +- lldb/source/Target/StackFrameList.cpp | 13 +-- lldb/source/Target/Thread.cpp | 60 +++++++----- .../TestScriptedFrameProvider.py | 92 +++++++++++++++++++ .../test_frame_providers.py | 78 ++++++++++++++++ 6 files changed, 222 insertions(+), 36 deletions(-) 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, void ClearScriptedFrameProvider(); - lldb::SyntheticFrameProviderSP GetFrameProvider() const { - return m_frame_provider_sp; + const llvm::SmallVector & + GetFrameProviders() const { + return m_frame_providers; } protected: @@ -1409,8 +1410,8 @@ class Thread : public std::enable_shared_from_this, /// 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 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 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( - *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 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(*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(*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( + *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 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 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 From 5d50168a207adc174831f91623b953fdc7b89779 Mon Sep 17 00:00:00 2001 From: Leandro Lupori Date: Fri, 23 Jan 2026 18:04:57 -0300 Subject: [PATCH 2/7] [lldb] Fix test_chained_frame_providers on 32-bit Arm (#177668) PC addresses must always be 16-bit aligned on 32-bit Arm CPUs. Fixes #177666 (cherry picked from commit cd70e2d8367493785f1591045db2151540b14b91) --- .../scripted_frame_provider/TestScriptedFrameProvider.py | 4 ++-- .../scripted_frame_provider/test_frame_providers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py index ceca64a450686..964d213b16887 100644 --- a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py +++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py @@ -706,7 +706,7 @@ def test_chained_frame_providers(self): "baz", "Frame 0 should be 'baz' from last provider in chain", ) - self.assertEqual(frame0.GetPC(), 0xBAD) + self.assertEqual(frame0.GetPC(), 0xBAC) frame1 = thread.GetFrameAtIndex(1) self.assertIsNotNone(frame1) @@ -715,7 +715,7 @@ def test_chained_frame_providers(self): "bar", "Frame 1 should be 'bar' from second provider in chain", ) - self.assertEqual(frame1.GetPC(), 0xBAB) + self.assertEqual(frame1.GetPC(), 0xBAA) frame2 = thread.GetFrameAtIndex(2) self.assertIsNotNone(frame2) 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 e97d11f173045..6233041f68a51 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 @@ -427,7 +427,7 @@ def get_priority(): def get_frame_at_index(self, index): if index == 0: # Return synthetic "bar" frame - return CustomScriptedFrame(self.thread, 0, 0xBAB, "bar") + return CustomScriptedFrame(self.thread, 0, 0xBAA, "bar") elif index - 1 < len(self.input_frames): # Pass through input frames (shifted by 1) return index - 1 @@ -453,7 +453,7 @@ def get_priority(): def get_frame_at_index(self, index): if index == 0: # Return synthetic "baz" frame - return CustomScriptedFrame(self.thread, 0, 0xBAD, "baz") + return CustomScriptedFrame(self.thread, 0, 0xBAC, "baz") elif index - 1 < len(self.input_frames): # Pass through input frames (shifted by 1) return index - 1 From 341793b5924094328480d64878ebedf9f1745b16 Mon Sep 17 00:00:00 2001 From: Aman LaChapelle Date: Thu, 29 Jan 2026 11:12:20 -0800 Subject: [PATCH 3/7] [lldb] Move ValueImpl and ValueLocker to ValueObject, NFC. (#178573) This patch moves ValueImpl and ValueLocker to ValueObject.{h,cpp}. This follows the example set in TypeImpl/SBType, where we have something that SBType uses internally that needs to be exposed in the layer below. In this case, SBValue uses ValueImpl, which wraps ValueObject. The wrapper helps avoid bugs, so we want to keep it, but the script interpreter needs to use it and said interpreter is conceptually *below* the SB layer...which means we can't use methods on SBValue. This patch is purely the code motion part of that, future patches will actually make use of this moved code. (cherry picked from commit 58f623c504d14743e465f988848f9c16dc220a3b) --- lldb/include/lldb/API/SBValue.h | 7 +- lldb/include/lldb/ValueObject/ValueObject.h | 78 +++++++++ lldb/source/API/SBValue.cpp | 166 -------------------- lldb/source/ValueObject/ValueObject.cpp | 91 +++++++++++ 4 files changed, 172 insertions(+), 170 deletions(-) diff --git a/lldb/include/lldb/API/SBValue.h b/lldb/include/lldb/API/SBValue.h index dead11fba19fe..0583f2a4983f6 100644 --- a/lldb/include/lldb/API/SBValue.h +++ b/lldb/include/lldb/API/SBValue.h @@ -13,10 +13,9 @@ #include "lldb/API/SBDefines.h" #include "lldb/API/SBType.h" +namespace lldb_private { class ValueImpl; class ValueLocker; - -namespace lldb_private { namespace python { class SWIGBridge; } @@ -490,7 +489,7 @@ class LLDB_API SBValue { /// \return /// A ValueObjectSP of the best kind (static, dynamic or synthetic) we /// can cons up, in accordance with the SBValue's settings. - lldb::ValueObjectSP GetSP(ValueLocker &value_locker) const; + lldb::ValueObjectSP GetSP(lldb_private::ValueLocker &value_locker) const; // these calls do the right thing WRT adjusting their settings according to // the target's preferences @@ -507,7 +506,7 @@ class LLDB_API SBValue { bool use_synthetic, const char *name); private: - typedef std::shared_ptr ValueImplSP; + typedef std::shared_ptr ValueImplSP; ValueImplSP m_opaque_sp; void SetSP(ValueImplSP impl_sp); diff --git a/lldb/include/lldb/ValueObject/ValueObject.h b/lldb/include/lldb/ValueObject/ValueObject.h index 3f9f2b5de8dbe..8a528bad15f94 100644 --- a/lldb/include/lldb/ValueObject/ValueObject.h +++ b/lldb/include/lldb/ValueObject/ValueObject.h @@ -1121,6 +1121,84 @@ class ValueObject { const ValueObject &operator=(const ValueObject &) = delete; }; +// The two classes below are used by the public SBValue API implementation. This +// is useful here because we need them in order to access the underlying +// ValueObject from SBValue without introducing a back-dependency from the API +// library to the more core libs. + +class ValueImpl { +public: + ValueImpl() = default; + + ValueImpl(lldb::ValueObjectSP in_valobj_sp, + lldb::DynamicValueType use_dynamic, bool use_synthetic, + const char *name = nullptr); + + ValueImpl(const ValueImpl &rhs) = default; + + ValueImpl &operator=(const ValueImpl &rhs); + + bool IsValid(); + + lldb::ValueObjectSP GetRootSP() { return m_valobj_sp; } + + lldb::ValueObjectSP GetSP(Process::StopLocker &stop_locker, + std::unique_lock &lock, + Status &error); + + void SetUseDynamic(lldb::DynamicValueType use_dynamic) { + m_use_dynamic = use_dynamic; + } + + void SetUseSynthetic(bool use_synthetic) { m_use_synthetic = use_synthetic; } + + lldb::DynamicValueType GetUseDynamic() { return m_use_dynamic; } + + bool GetUseSynthetic() { return m_use_synthetic; } + + // All the derived values that we would make from the m_valobj_sp will share + // the ExecutionContext with m_valobj_sp, so we don't need to do the + // calculations in GetSP to return the Target, Process, Thread or Frame. It + // is convenient to provide simple accessors for these, which I do here. + lldb::TargetSP GetTargetSP() { + return m_valobj_sp ? m_valobj_sp->GetTargetSP() : lldb::TargetSP{}; + } + + lldb::ProcessSP GetProcessSP() { + return m_valobj_sp ? m_valobj_sp->GetProcessSP() : lldb::ProcessSP{}; + } + + lldb::ThreadSP GetThreadSP() { + return m_valobj_sp ? m_valobj_sp->GetThreadSP() : lldb::ThreadSP{}; + } + + lldb::StackFrameSP GetFrameSP() { + return m_valobj_sp ? m_valobj_sp->GetFrameSP() : lldb::StackFrameSP{}; + } + +private: + lldb::ValueObjectSP m_valobj_sp; + lldb::DynamicValueType m_use_dynamic; + bool m_use_synthetic; + ConstString m_name; +}; + +class ValueLocker { +public: + ValueLocker() = default; + + lldb::ValueObjectSP GetLockedSP(ValueImpl &in_value) { + return in_value.GetSP(m_stop_locker, m_lock, m_lock_error); + } + + Status &GetError() { return m_lock_error; } + +private: + Process::StopLocker m_stop_locker; + std::unique_lock m_lock; + Status m_lock_error; +}; + } // namespace lldb_private #endif // LLDB_VALUEOBJECT_VALUEOBJECT_H diff --git a/lldb/source/API/SBValue.cpp b/lldb/source/API/SBValue.cpp index 5b67270859da2..adc03785602e1 100644 --- a/lldb/source/API/SBValue.cpp +++ b/lldb/source/API/SBValue.cpp @@ -52,172 +52,6 @@ using namespace lldb; using namespace lldb_private; -class ValueImpl { -public: - ValueImpl() = default; - - ValueImpl(lldb::ValueObjectSP in_valobj_sp, - lldb::DynamicValueType use_dynamic, bool use_synthetic, - const char *name = nullptr) - : m_use_dynamic(use_dynamic), m_use_synthetic(use_synthetic), - m_name(name) { - if (in_valobj_sp) { - if ((m_valobj_sp = in_valobj_sp->GetQualifiedRepresentationIfAvailable( - lldb::eNoDynamicValues, false))) { - if (!m_name.IsEmpty()) - m_valobj_sp->SetName(m_name); - } - } - } - - ValueImpl(const ValueImpl &rhs) = default; - - ValueImpl &operator=(const ValueImpl &rhs) { - if (this != &rhs) { - m_valobj_sp = rhs.m_valobj_sp; - m_use_dynamic = rhs.m_use_dynamic; - m_use_synthetic = rhs.m_use_synthetic; - m_name = rhs.m_name; - } - return *this; - } - - bool IsValid() { - if (m_valobj_sp.get() == nullptr) - return false; - else { - // FIXME: This check is necessary but not sufficient. We for sure don't - // want to touch SBValues whose owning - // targets have gone away. This check is a little weak in that it - // enforces that restriction when you call IsValid, but since IsValid - // doesn't lock the target, you have no guarantee that the SBValue won't - // go invalid after you call this... Also, an SBValue could depend on - // data from one of the modules in the target, and those could go away - // independently of the target, for instance if a module is unloaded. - // But right now, neither SBValues nor ValueObjects know which modules - // they depend on. So I have no good way to make that check without - // tracking that in all the ValueObject subclasses. - TargetSP target_sp = m_valobj_sp->GetTargetSP(); - return target_sp && target_sp->IsValid(); - } - } - - lldb::ValueObjectSP GetRootSP() { return m_valobj_sp; } - - lldb::ValueObjectSP GetSP(Process::StopLocker &stop_locker, - std::unique_lock &lock, - Status &error) { - if (!m_valobj_sp) { - error = Status::FromErrorString("invalid value object"); - return m_valobj_sp; - } - - lldb::ValueObjectSP value_sp = m_valobj_sp; - - Target *target = value_sp->GetTargetSP().get(); - // If this ValueObject holds an error, then it is valuable for that. - if (value_sp->GetError().Fail()) - return value_sp; - - if (!target) - return ValueObjectSP(); - - lock = std::unique_lock(target->GetAPIMutex()); - - ProcessSP process_sp(value_sp->GetProcessSP()); - if (process_sp && !stop_locker.TryLock(&process_sp->GetRunLock())) { - // We don't allow people to play around with ValueObject if the process - // is running. If you want to look at values, pause the process, then - // look. - error = Status::FromErrorString("process must be stopped."); - return ValueObjectSP(); - } - - if (m_use_dynamic != eNoDynamicValues) { - ValueObjectSP dynamic_sp = value_sp->GetDynamicValue(m_use_dynamic); - if (dynamic_sp) - value_sp = dynamic_sp; - } - - if (m_use_synthetic) { - ValueObjectSP synthetic_sp = value_sp->GetSyntheticValue(); - if (synthetic_sp) - value_sp = synthetic_sp; - } - - if (!value_sp) - error = Status::FromErrorString("invalid value object"); - if (!m_name.IsEmpty()) - value_sp->SetName(m_name); - - return value_sp; - } - - void SetUseDynamic(lldb::DynamicValueType use_dynamic) { - m_use_dynamic = use_dynamic; - } - - void SetUseSynthetic(bool use_synthetic) { m_use_synthetic = use_synthetic; } - - lldb::DynamicValueType GetUseDynamic() { return m_use_dynamic; } - - bool GetUseSynthetic() { return m_use_synthetic; } - - // All the derived values that we would make from the m_valobj_sp will share - // the ExecutionContext with m_valobj_sp, so we don't need to do the - // calculations in GetSP to return the Target, Process, Thread or Frame. It - // is convenient to provide simple accessors for these, which I do here. - TargetSP GetTargetSP() { - if (m_valobj_sp) - return m_valobj_sp->GetTargetSP(); - else - return TargetSP(); - } - - ProcessSP GetProcessSP() { - if (m_valobj_sp) - return m_valobj_sp->GetProcessSP(); - else - return ProcessSP(); - } - - ThreadSP GetThreadSP() { - if (m_valobj_sp) - return m_valobj_sp->GetThreadSP(); - else - return ThreadSP(); - } - - StackFrameSP GetFrameSP() { - if (m_valobj_sp) - return m_valobj_sp->GetFrameSP(); - else - return StackFrameSP(); - } - -private: - lldb::ValueObjectSP m_valobj_sp; - lldb::DynamicValueType m_use_dynamic; - bool m_use_synthetic; - ConstString m_name; -}; - -class ValueLocker { -public: - ValueLocker() = default; - - ValueObjectSP GetLockedSP(ValueImpl &in_value) { - return in_value.GetSP(m_stop_locker, m_lock, m_lock_error); - } - - Status &GetError() { return m_lock_error; } - -private: - Process::StopLocker m_stop_locker; - std::unique_lock m_lock; - Status m_lock_error; -}; - SBValue::SBValue() { LLDB_INSTRUMENT_VA(this); } SBValue::SBValue(const lldb::ValueObjectSP &value_sp) { diff --git a/lldb/source/ValueObject/ValueObject.cpp b/lldb/source/ValueObject/ValueObject.cpp index 121054e3e92ed..5b7c65a102e5d 100644 --- a/lldb/source/ValueObject/ValueObject.cpp +++ b/lldb/source/ValueObject/ValueObject.cpp @@ -3768,3 +3768,94 @@ ValueObjectSP ValueObject::Persist() { lldb::ValueObjectSP ValueObject::GetVTable() { return ValueObjectVTable::Create(*this); } + +ValueImpl::ValueImpl(lldb::ValueObjectSP in_valobj_sp, + lldb::DynamicValueType use_dynamic, bool use_synthetic, + const char *name) + : m_use_dynamic(use_dynamic), m_use_synthetic(use_synthetic), m_name(name) { + if (in_valobj_sp) { + if ((m_valobj_sp = in_valobj_sp->GetQualifiedRepresentationIfAvailable( + lldb::eNoDynamicValues, false))) { + if (!m_name.IsEmpty()) + m_valobj_sp->SetName(m_name); + } + } +} + +ValueImpl &ValueImpl::operator=(const ValueImpl &rhs) { + if (this != &rhs) { + m_valobj_sp = rhs.m_valobj_sp; + m_use_dynamic = rhs.m_use_dynamic; + m_use_synthetic = rhs.m_use_synthetic; + m_name = rhs.m_name; + } + return *this; +} + +bool ValueImpl::IsValid() { + if (m_valobj_sp.get() == nullptr) + return false; + + // FIXME: This check is necessary but not sufficient. We for sure don't + // want to touch SBValues whose owning + // targets have gone away. This check is a little weak in that it + // enforces that restriction when you call IsValid, but since IsValid + // doesn't lock the target, you have no guarantee that the SBValue won't + // go invalid after you call this... Also, an SBValue could depend on + // data from one of the modules in the target, and those could go away + // independently of the target, for instance if a module is unloaded. + // But right now, neither SBValues nor ValueObjects know which modules + // they depend on. So I have no good way to make that check without + // tracking that in all the ValueObject subclasses. + TargetSP target_sp = m_valobj_sp->GetTargetSP(); + return target_sp && target_sp->IsValid(); +} + +lldb::ValueObjectSP +ValueImpl::GetSP(Process::StopLocker &stop_locker, + std::unique_lock &lock, Status &error) { + if (!m_valobj_sp) { + error = Status::FromErrorString("invalid value object"); + return m_valobj_sp; + } + + lldb::ValueObjectSP value_sp = m_valobj_sp; + + Target *target = value_sp->GetTargetSP().get(); + // If this ValueObject holds an error, then it is valuable for that. + if (value_sp->GetError().Fail()) + return value_sp; + + if (!target) + return ValueObjectSP(); + + lock = std::unique_lock(target->GetAPIMutex()); + + ProcessSP process_sp(value_sp->GetProcessSP()); + if (process_sp && !stop_locker.TryLock(&process_sp->GetRunLock())) { + // We don't allow people to play around with ValueObject if the process + // is running. If you want to look at values, pause the process, then + // look. + error = Status::FromErrorString("process must be stopped."); + return ValueObjectSP(); + } + + if (m_use_dynamic != eNoDynamicValues) { + ValueObjectSP dynamic_sp = value_sp->GetDynamicValue(m_use_dynamic); + if (dynamic_sp) + value_sp = dynamic_sp; + } + + if (m_use_synthetic) { + ValueObjectSP synthetic_sp = value_sp->GetSyntheticValue(); + if (synthetic_sp) + value_sp = synthetic_sp; + } + + if (!value_sp) + error = Status::FromErrorString("invalid value object"); + if (!m_name.IsEmpty()) + value_sp->SetName(m_name); + + return value_sp; +} From 4baddb742729f124010360bcfde293a332267a7a Mon Sep 17 00:00:00 2001 From: Aman LaChapelle Date: Thu, 29 Jan 2026 11:38:50 -0800 Subject: [PATCH 4/7] [lldb] Add conversions for SBValueList and SBValue to the python bridge. (#178574) This patch adds support for: - PyObject -> SBValueList (which was surprisingly not there before!) - PyObject -> SBValue - SBValue -> ValueObjectSP using the ScriptInterpreter These three are the main remaining plumbing changes necessary before we can get to the meat of actually using ScriptedFrame to provide values to the printer/etc. Future patches build off this change in order to allow ScriptedFrames to provide variables and get values for variable expressions. (cherry picked from commit 8122d0e4bc8b536e0bb6bb5fae97e8216986ee28) --- lldb/bindings/python/python-wrapper.swig | 12 ++++++ lldb/include/lldb/API/SBValue.h | 3 ++ .../lldb/Interpreter/ScriptInterpreter.h | 3 ++ lldb/source/Interpreter/ScriptInterpreter.cpp | 10 +++++ .../Interfaces/ScriptedPythonInterface.cpp | 38 +++++++++++++++++++ .../Interfaces/ScriptedPythonInterface.h | 14 +++++++ .../Python/SWIGPythonBridge.h | 1 + 7 files changed, 81 insertions(+) diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig index 0ba152166522b..bf59569920470 100644 --- a/lldb/bindings/python/python-wrapper.swig +++ b/lldb/bindings/python/python-wrapper.swig @@ -545,6 +545,18 @@ void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBValue(PyObject * data return sb_ptr; } +void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBValueList(PyObject * data) { + lldb::SBValueList *sb_ptr = NULL; + + int valid_cast = + SWIG_ConvertPtr(data, (void **)&sb_ptr, SWIGTYPE_p_lldb__SBValueList, 0); + + if (valid_cast == -1) + return NULL; + + return sb_ptr; +} + void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBMemoryRegionInfo(PyObject * data) { lldb::SBMemoryRegionInfo *sb_ptr = NULL; diff --git a/lldb/include/lldb/API/SBValue.h b/lldb/include/lldb/API/SBValue.h index 0583f2a4983f6..d4cc2f05c39e3 100644 --- a/lldb/include/lldb/API/SBValue.h +++ b/lldb/include/lldb/API/SBValue.h @@ -505,6 +505,9 @@ class LLDB_API SBValue { void SetSP(const lldb::ValueObjectSP &sp, lldb::DynamicValueType use_dynamic, bool use_synthetic, const char *name); +protected: + friend class lldb_private::ScriptInterpreter; + private: typedef std::shared_ptr ValueImplSP; ValueImplSP m_opaque_sp; diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h index 0b91d6756552d..557d73a415452 100644 --- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h +++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h @@ -609,6 +609,9 @@ class ScriptInterpreter : public PluginInterface { lldb::StackFrameListSP GetOpaqueTypeFromSBFrameList(const lldb::SBFrameList &exe_ctx) const; + lldb::ValueObjectSP + GetOpaqueTypeFromSBValue(const lldb::SBValue &value) const; + protected: Debugger &m_debugger; lldb::ScriptLanguage m_script_lang; diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp index 7bad10ff3ea61..5e8478c2670bb 100644 --- a/lldb/source/Interpreter/ScriptInterpreter.cpp +++ b/lldb/source/Interpreter/ScriptInterpreter.cpp @@ -15,6 +15,7 @@ #include "lldb/Utility/Status.h" #include "lldb/Utility/Stream.h" #include "lldb/Utility/StringList.h" +#include "lldb/ValueObject/ValueObject.h" #if defined(_WIN32) #include "lldb/Host/windows/ConnectionGenericFileWindows.h" #endif @@ -162,6 +163,15 @@ lldb::StackFrameListSP ScriptInterpreter::GetOpaqueTypeFromSBFrameList( return frame_list.m_opaque_sp; } +lldb::ValueObjectSP +ScriptInterpreter::GetOpaqueTypeFromSBValue(const lldb::SBValue &value) const { + if (!value.m_opaque_sp) + return lldb::ValueObjectSP(); + + lldb_private::ValueLocker locker; + return locker.GetLockedSP(*value.m_opaque_sp); +} + lldb::ScriptLanguage ScriptInterpreter::StringToLanguage(const llvm::StringRef &language) { if (language.equals_insensitive(LanguageToString(eScriptLanguageNone))) diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp index ba4473cf9ec4d..f5fd8b2d2d802 100644 --- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp +++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp @@ -18,6 +18,7 @@ #include "../ScriptInterpreterPythonImpl.h" #include "ScriptedPythonInterface.h" #include "lldb/Symbol/SymbolContext.h" +#include "lldb/ValueObject/ValueObjectList.h" #include using namespace lldb; @@ -273,4 +274,41 @@ ScriptedPythonInterface::ExtractValueFromPythonObject( return m_interpreter.GetOpaqueTypeFromSBFrameList(*sb_frame_list); } +template <> +lldb::ValueObjectSP +ScriptedPythonInterface::ExtractValueFromPythonObject( + python::PythonObject &p, Status &error) { + lldb::SBValue *sb_value = reinterpret_cast( + python::LLDBSWIGPython_CastPyObjectToSBValue(p.get())); + if (!sb_value) { + error = Status::FromErrorStringWithFormat( + "couldn't cast lldb::SBValue to lldb::ValueObjectSP"); + return {}; + } + + return m_interpreter.GetOpaqueTypeFromSBValue(*sb_value); +} + +template <> +lldb::ValueObjectListSP +ScriptedPythonInterface::ExtractValueFromPythonObject( + python::PythonObject &p, Status &error) { + lldb::SBValueList *sb_value_list = reinterpret_cast( + python::LLDBSWIGPython_CastPyObjectToSBValueList(p.get())); + + if (!sb_value_list) { + error = Status::FromErrorStringWithFormat( + "couldn't cast lldb::SBValueList to lldb::ValueObjectListSP"); + return {}; + } + + lldb::ValueObjectListSP out = std::make_shared(); + for (uint32_t i = 0, e = sb_value_list->GetSize(); i < e; ++i) { + SBValue value = sb_value_list->GetValueAtIndex(i); + out->Append(m_interpreter.GetOpaqueTypeFromSBValue(value)); + } + + return out; +} + #endif diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h index b737f945845f6..5e3df8f18c2be 100644 --- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h +++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h @@ -656,6 +656,10 @@ class ScriptedPythonInterface : virtual public ScriptedInterface { return python::SWIGBridge::ToSWIGWrapper(arg); } + python::PythonObject Transform(lldb::ValueObjectSP arg) { + return python::SWIGBridge::ToSWIGWrapper(arg); + } + template void ReverseTransform(T &original_arg, U transformed_arg, Status &error) { // If U is not a PythonObject, don't touch it! @@ -814,6 +818,16 @@ lldb::StackFrameListSP ScriptedPythonInterface::ExtractValueFromPythonObject( python::PythonObject &p, Status &error); +template <> +lldb::ValueObjectSP +ScriptedPythonInterface::ExtractValueFromPythonObject( + python::PythonObject &p, Status &error); + +template <> +lldb::ValueObjectListSP +ScriptedPythonInterface::ExtractValueFromPythonObject( + python::PythonObject &p, Status &error); + } // namespace lldb_private #endif // LLDB_ENABLE_PYTHON diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h index 32948ffd30023..9f68445d0d72b 100644 --- a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h +++ b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h @@ -269,6 +269,7 @@ void *LLDBSWIGPython_CastPyObjectToSBThread(PyObject *data); void *LLDBSWIGPython_CastPyObjectToSBFrame(PyObject *data); void *LLDBSWIGPython_CastPyObjectToSBSymbolContext(PyObject *data); void *LLDBSWIGPython_CastPyObjectToSBValue(PyObject *data); +void *LLDBSWIGPython_CastPyObjectToSBValueList(PyObject *data); void *LLDBSWIGPython_CastPyObjectToSBMemoryRegionInfo(PyObject *data); void *LLDBSWIGPython_CastPyObjectToSBExecutionContext(PyObject *data); void *LLDBSWIGPython_CastPyObjectToSBFrameList(PyObject *data); From 6b5252ee78670efdf0966084b1d3b5f1829e18ba Mon Sep 17 00:00:00 2001 From: Aman LaChapelle Date: Thu, 29 Jan 2026 21:24:17 -0800 Subject: [PATCH 5/7] [lldb] Add support for ScriptedFrame to provide values/variables. (#178575) This patch adds plumbing to support the implementations of StackFrame::Get{*}Variable{*} on ScriptedFrame. The major pieces required are: - A modification to ScriptedFrameInterface, so that we can actually call the python methods. - A corresponding update to the python implementation to call the python methods. - An implementation in ScriptedFrame that can get the variable list on construction inside ScriptedFrame::Create, and pass that list into the ScriptedFrame so it can get those values on request. There is a major caveat, which is that if the values from the python side don't have variables attached, right now, they won't be passed into the scripted frame to be stored in the variable list. Future discussions around adding support for 'extended variables' when printing frame variables may create a reason to change the VariableListSP into a ValueObjectListSP, and generate the VariableListSP on the fly, but that should be addressed at a later time. This patch also adds tests to the frame provider test suite to prove these changes all plumb together correctly. Related radar: rdar://165708771 (cherry picked from commit 10f2611c2173783efae8aebc32d1515013271b64) --- .../Interfaces/ScriptedFrameInterface.h | 9 ++ .../Process/scripted/ScriptedFrame.cpp | 66 +++++++++++++++ .../Plugins/Process/scripted/ScriptedFrame.h | 21 +++++ .../ScriptedFramePythonInterface.cpp | 28 +++++++ .../Interfaces/ScriptedFramePythonInterface.h | 6 ++ .../TestScriptedFrameProvider.py | 53 ++++++++++++ .../scripted_frame_provider/main.cpp | 4 + .../test_frame_providers.py | 82 +++++++++++++++++++ 8 files changed, 269 insertions(+) diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h index 8ef4b37d6ba12..00994d65fd601 100644 --- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h +++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h @@ -10,6 +10,7 @@ #define LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEINTERFACE_H #include "ScriptedInterface.h" +#include "lldb/API/SBValueList.h" #include "lldb/Core/StructuredDataImpl.h" #include "lldb/Symbol/SymbolContext.h" #include "lldb/lldb-private.h" @@ -49,6 +50,14 @@ class ScriptedFrameInterface : virtual public ScriptedInterface { virtual std::optional GetRegisterContext() { return std::nullopt; } + + virtual lldb::ValueObjectListSP GetVariables() { return nullptr; } + + virtual lldb::ValueObjectSP + GetValueObjectForVariableExpression(llvm::StringRef expr, uint32_t options, + Status &error) { + return nullptr; + } }; } // namespace lldb_private diff --git a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp index 70ce101c6c834..7462c467eb7da 100644 --- a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp +++ b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp @@ -9,6 +9,7 @@ #include "ScriptedFrame.h" #include "Plugins/Process/Utility/RegisterContextMemory.h" +#include "lldb/API/SBDeclaration.h" #include "lldb/Core/Address.h" #include "lldb/Core/Debugger.h" #include "lldb/Core/Module.h" @@ -20,6 +21,7 @@ #include "lldb/Symbol/CompileUnit.h" #include "lldb/Symbol/SymbolContext.h" #include "lldb/Symbol/SymbolFile.h" +#include "lldb/Symbol/VariableList.h" #include "lldb/Target/ExecutionContext.h" #include "lldb/Target/Process.h" #include "lldb/Target/RegisterContext.h" @@ -28,6 +30,8 @@ #include "lldb/Utility/LLDBLog.h" #include "lldb/Utility/Log.h" #include "lldb/Utility/StructuredData.h" +#include "lldb/ValueObject/ValueObject.h" +#include "lldb/ValueObject/ValueObjectList.h" using namespace lldb; using namespace lldb_private; @@ -265,3 +269,65 @@ lldb::RegisterContextSP ScriptedFrame::GetRegisterContext() { return m_reg_context_sp; } + +VariableList *ScriptedFrame::GetVariableList(bool get_file_globals, + Status *error_ptr) { + PopulateVariableListFromInterface(); + return m_variable_list_sp.get(); +} + +lldb::VariableListSP +ScriptedFrame::GetInScopeVariableList(bool get_file_globals, + bool must_have_valid_location) { + PopulateVariableListFromInterface(); + return m_variable_list_sp; +} + +void ScriptedFrame::PopulateVariableListFromInterface() { + // Fetch values from the interface. + ValueObjectListSP value_list_sp = GetInterface()->GetVariables(); + if (!value_list_sp) + return; + + // Convert what we can into a variable. + m_variable_list_sp = std::make_shared(); + for (uint32_t i = 0, e = value_list_sp->GetSize(); i < e; ++i) { + ValueObjectSP v = value_list_sp->GetValueObjectAtIndex(i); + if (!v) + continue; + + VariableSP var = v->GetVariable(); + // TODO: We could in theory ask the scripted frame to *produce* a + // variable for this value object. + if (!var) + continue; + + m_variable_list_sp->AddVariable(var); + } +} + +lldb::ValueObjectSP ScriptedFrame::GetValueObjectForFrameVariable( + const lldb::VariableSP &variable_sp, lldb::DynamicValueType use_dynamic) { + // Fetch values from the interface. + ValueObjectListSP values = m_scripted_frame_interface_sp->GetVariables(); + if (!values) + return {}; + + return values->FindValueObjectByValueName(variable_sp->GetName().AsCString()); +} + +lldb::ValueObjectSP ScriptedFrame::GetValueForVariableExpressionPath( + llvm::StringRef var_expr, lldb::DynamicValueType use_dynamic, + uint32_t options, lldb::VariableSP &var_sp, Status &error) { + // Unless the frame implementation knows how to create variables (which it + // doesn't), we can't construct anything for the variable. This may seem + // somewhat out of place, but it's basically because of how this API is used - + // the print command uses this API to fill in var_sp; and this implementation + // can't do that! + // FIXME: We should make it possible for the frame implementation to create + // Variable objects. + (void)var_sp; + // Otherwise, delegate to the scripted frame interface pointer. + return m_scripted_frame_interface_sp->GetValueObjectForVariableExpression( + var_expr, options, error); +} diff --git a/lldb/source/Plugins/Process/scripted/ScriptedFrame.h b/lldb/source/Plugins/Process/scripted/ScriptedFrame.h index 0545548e912e6..fe154792c745b 100644 --- a/lldb/source/Plugins/Process/scripted/ScriptedFrame.h +++ b/lldb/source/Plugins/Process/scripted/ScriptedFrame.h @@ -63,6 +63,21 @@ class ScriptedFrame : public lldb_private::StackFrame { lldb::RegisterContextSP GetRegisterContext() override; + VariableList *GetVariableList(bool get_file_globals, + lldb_private::Status *error_ptr) override; + + lldb::VariableListSP + GetInScopeVariableList(bool get_file_globals, + bool must_have_valid_location = false) override; + + lldb::ValueObjectSP + GetValueObjectForFrameVariable(const lldb::VariableSP &variable_sp, + lldb::DynamicValueType use_dynamic) override; + + lldb::ValueObjectSP GetValueForVariableExpressionPath( + llvm::StringRef var_expr, lldb::DynamicValueType use_dynamic, + uint32_t options, lldb::VariableSP &var_sp, Status &error) override; + bool isA(const void *ClassID) const override { return ClassID == &ID || StackFrame::isA(ClassID); } @@ -75,6 +90,11 @@ class ScriptedFrame : public lldb_private::StackFrame { CreateRegisterContext(ScriptedFrameInterface &interface, Thread &thread, lldb::user_id_t frame_id); + // Populate m_variable_list_sp from the scripted frame interface. Right now + // this doesn't take any options because the implementation can't really do + // anything with those options anyway, so there's no point. + void PopulateVariableListFromInterface(); + ScriptedFrame(const ScriptedFrame &) = delete; const ScriptedFrame &operator=(const ScriptedFrame &) = delete; @@ -82,6 +102,7 @@ class ScriptedFrame : public lldb_private::StackFrame { lldb::ScriptedFrameInterfaceSP m_scripted_frame_interface_sp; lldb_private::StructuredData::GenericSP m_script_object_sp; + lldb::VariableListSP m_variable_list_sp; static char ID; }; diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.cpp index 20ca7a2c01356..9cc7b04fc9dba 100644 --- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.cpp +++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.cpp @@ -154,4 +154,32 @@ std::optional ScriptedFramePythonInterface::GetRegisterContext() { return obj->GetAsString()->GetValue().str(); } +lldb::ValueObjectListSP ScriptedFramePythonInterface::GetVariables() { + Status error; + auto vals = Dispatch("get_variables", error); + + if (error.Fail()) { + return ErrorWithMessage(LLVM_PRETTY_FUNCTION, + error.AsCString(), error); + } + + return vals; +} + +lldb::ValueObjectSP +ScriptedFramePythonInterface::GetValueObjectForVariableExpression( + llvm::StringRef expr, uint32_t options, Status &status) { + Status dispatch_error; + auto val = Dispatch("get_value_for_variable_expression", + dispatch_error, expr.data(), options, + status); + + if (dispatch_error.Fail()) { + return ErrorWithMessage( + LLVM_PRETTY_FUNCTION, dispatch_error.AsCString(), dispatch_error); + } + + return val; +} + #endif diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.h index 3aff237ae65d5..d8ac093106bbd 100644 --- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.h +++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.h @@ -52,6 +52,12 @@ class ScriptedFramePythonInterface : public ScriptedFrameInterface, StructuredData::DictionarySP GetRegisterInfo() override; std::optional GetRegisterContext() override; + + lldb::ValueObjectListSP GetVariables() override; + + lldb::ValueObjectSP + GetValueObjectForVariableExpression(llvm::StringRef expr, uint32_t options, + Status &status) override; }; } // namespace lldb_private diff --git a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py index 964d213b16887..7dd74013b90f8 100644 --- a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py +++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py @@ -730,3 +730,56 @@ def test_chained_frame_providers(self): frame3 = thread.GetFrameAtIndex(3) self.assertIsNotNone(frame3) self.assertIn("thread_func", frame3.GetFunctionName()) + + def test_get_values(self): + """Test a frame that provides values.""" + self.build() + # Set the breakpoint after the variable_in_main variable exists and can be queried. + target, process, thread, bkpt = lldbutil.run_to_line_breakpoint( + self, lldb.SBFileSpec(self.source), 35, 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 a provider that can provide variables. + error = lldb.SBError() + target.RegisterScriptedFrameProvider( + "test_frame_providers.ValueProvidingFrameProvider", + lldb.SBStructuredData(), + error, + ) + self.assertTrue(error.Success(), f"Failed to register provider: {error}") + + # Verify we have 1 more frame. + new_frame_count = thread.GetNumFrames() + self.assertEqual( + new_frame_count, + original_frame_count + 1, + "Should have original frames + 1 extra frames", + ) + + # Check that we can get variables from this frame. + frame0 = thread.GetFrameAtIndex(0) + self.assertIsNotNone(frame0) + # Get every variable visible at this point + variables = frame0.GetVariables(True, True, True, False) + self.assertTrue(variables.IsValid() and variables.GetSize() == 1) + + # Check that we can get values from paths. `_handler_one` is a special + # value we provide through only our expression handler in the frame + # implementation. + one = frame0.GetValueForVariablePath("_handler_one") + self.assertEqual(one.unsigned, 1) + var = frame0.GetValueForVariablePath("variable_in_main") + # The names won't necessarily match, but the values should (the frame renames the SBValue) + self.assertEqual(var.unsigned, variables.GetValueAtIndex(0).unsigned) + varp1 = frame0.GetValueForVariablePath("variable_in_main + 1") + self.assertEqual(varp1.unsigned, 124) diff --git a/lldb/test/API/functionalities/scripted_frame_provider/main.cpp b/lldb/test/API/functionalities/scripted_frame_provider/main.cpp index 0298e88e4de16..e1d346c29052b 100644 --- a/lldb/test/API/functionalities/scripted_frame_provider/main.cpp +++ b/lldb/test/API/functionalities/scripted_frame_provider/main.cpp @@ -29,6 +29,10 @@ void thread_func(int thread_num) { int main(int argc, char **argv) { std::thread threads[NUM_THREADS]; + // Used as an existing C++ variable we can anchor on. + int variable_in_main = 123; + (void)variable_in_main; + for (int i = 0; i < NUM_THREADS; i++) { threads[i] = std::thread(thread_func, i); } 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 6233041f68a51..3a30e4fa96d6e 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 @@ -458,3 +458,85 @@ def get_frame_at_index(self, index): # Pass through input frames (shifted by 1) return index - 1 return None + + +class ValueProvidingFrame(ScriptedFrame): + """Scripted frame with a valid PC but no associated module.""" + + def __init__(self, thread, idx, pc, function_name, variable): + args = lldb.SBStructuredData() + super().__init__(thread, args) + + self.idx = idx + self.pc = pc + self.function_name = function_name + self.variable = variable + + 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): + """Not artificial.""" + return False + + def is_hidden(self): + """Not hidden.""" + return False + + def get_register_context(self): + """No register context.""" + return None + + def get_variables(self): + """""" + out = lldb.SBValueList() + out.Append(self.variable) + return out + + def get_value_for_variable_expression(self, expr, options, error: lldb.SBError): + out = lldb.SBValue() + if expr == "_handler_one": + out = self.variable.CreateValueFromExpression("_handler_one", "(uint32_t)1") + elif self.variable.name in expr: + out = self.variable.CreateValueFromExpression("_expr", expr) + + if out.IsValid(): + return out + + error.SetErrorString(f"expression {expr} failed") + return None + + +class ValueProvidingFrameProvider(ScriptedFrameProvider): + """Add a single 'value-provider' 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 'value-provider' frame at beginning" + + def get_frame_at_index(self, index): + if index == 0: + f = self.input_frames.GetFrameAtIndex(index) + # Find some variable we can give to the frame. + variable = f.FindVariable("variable_in_main") + # Return synthetic "value-provider" frame + return ValueProvidingFrame( + self.thread, 0, 0xF00, "value-provider", variable + ) + elif index - 1 < len(self.input_frames): + # Pass through input frames (shifted by 1) + return index - 1 + return None From 6fc6d00fe61c1ac7f70d995d39bd6e8b94624f5d Mon Sep 17 00:00:00 2001 From: Med Ismail Bennani Date: Mon, 2 Feb 2026 19:12:35 -0800 Subject: [PATCH 6/7] [lldb] Fix variable access in old SBFrames after inferior function calls (#178823) When a user holds an SBFrame reference and then triggers an inferior function call (via expression evaluation or GetExtendedBacktraceThread), variables in that frame become inaccessible with "register fp is not available" errors. This happens because inferior function calls execute through ThreadPlanCallFunction, which calls ClearStackFrames() during cleanup to invalidate the unwinder state. ExecutionContextRef objects in the old SBFrames were tracking StackFrameLists via weak_ptr, which became stale when ClearStackFrames() created new instances. The fix uses stable StackFrameList identifiers that persist across ClearStackFrames(): - ID = 0: Normal unwinder frames (constant across all instances) - ID = sequential counter: Scripted frame provider instances ExecutionContextRef now stores the frame list ID instead of a weak_ptr, allowing it to resolve to the current StackFrameList with fresh unwinder state after an inferior function call completes. The Thread object preserves the provider chain configuration (m_provider_chain_ids and m_next_provider_id) across ClearStackFrames() so that recreated StackFrameLists get the same IDs. When providers need to be recreated, GetStackFrameList() rebuilds them from the persisted configuration. This commit also fixes a deadlock when Python scripted frame providers call back into LLDB during frame fetching. The m_list_mutex is now released before calling GetFrameAtIndex() on the Python scripted frame provider to prevent same-thread deadlock. A dedicated m_unwinder_frames_sp member ensures GetFrameListByIdentifier(0) always returns the current unwinder frames, and proper cleanup in DestroyThread() and ClearStackFrames() to prevent modules from lingering after a Thread (and its StackFrameLists) gets destroyed. Added test validates that variables remain accessible after GetExtendedBacktraceThread triggers an inferior function call to fetch libdispatch Queue Info. rdar://167027676 Signed-off-by: Med Ismail Bennani (cherry picked from commit c373d7632ac1fe8374b7f2b9536d7e746ed24786) --- lldb/include/lldb/Target/ExecutionContext.h | 19 +- lldb/include/lldb/Target/StackFrame.h | 12 +- lldb/include/lldb/Target/StackFrameList.h | 12 +- lldb/include/lldb/Target/Thread.h | 31 +- lldb/include/lldb/lldb-defines.h | 1 + lldb/include/lldb/lldb-types.h | 1 + lldb/source/Target/ExecutionContext.cpp | 74 +++- lldb/source/Target/StackFrameList.cpp | 40 ++- lldb/source/Target/Thread.cpp | 143 ++++++-- .../TestScriptedFrameProvider.py | 333 +++++++++++++++++- .../scripted_frame_provider/main.cpp | 32 +- .../macosx/extended-backtrace-api/Makefile | 3 + .../TestExtendedBacktraceAPI.py | 144 ++++++++ .../API/macosx/extended-backtrace-api/main.m | 53 +++ 14 files changed, 811 insertions(+), 87 deletions(-) create mode 100644 lldb/test/API/macosx/extended-backtrace-api/Makefile create mode 100644 lldb/test/API/macosx/extended-backtrace-api/TestExtendedBacktraceAPI.py create mode 100644 lldb/test/API/macosx/extended-backtrace-api/main.m diff --git a/lldb/include/lldb/Target/ExecutionContext.h b/lldb/include/lldb/Target/ExecutionContext.h index 8637234c4fb95..47bcd729abcdd 100644 --- a/lldb/include/lldb/Target/ExecutionContext.h +++ b/lldb/include/lldb/Target/ExecutionContext.h @@ -13,10 +13,13 @@ #include "lldb/Host/ProcessRunLock.h" #include "lldb/Target/StackID.h" +#include "lldb/Target/SyntheticFrameProvider.h" #include "lldb/lldb-private.h" namespace lldb_private { +struct StoppedExecutionContext; + //===----------------------------------------------------------------------===// /// Execution context objects refer to objects in the execution of the program /// that is being debugged. The consist of one or more of the following @@ -270,9 +273,12 @@ class ExecutionContextRef { void ClearFrame() { m_stack_id.Clear(); - m_frame_list_wp.reset(); + m_frame_list_id.reset(); } + friend llvm::Expected + GetStoppedExecutionContext(const ExecutionContextRef *exe_ctx_ref_ptr); + protected: // Member variables lldb::TargetWP m_target_wp; ///< A weak reference to a target @@ -283,13 +289,10 @@ class ExecutionContextRef { /// backing object changes StackID m_stack_id; ///< The stack ID that this object refers to in case the ///< backing object changes - mutable lldb::StackFrameListWP - m_frame_list_wp; ///< Weak reference to the - ///< frame list that contains - ///< this frame. If we can create a valid - ///< StackFrameListSP from it, we must use it to resolve - ///< the StackID, otherwise, we should ask the Thread's - ///< StackFrameList. + /// A map of identifiers to scripted frame providers used in this thread. + mutable std::optional< + std::pair> + m_frame_list_id; }; /// \class ExecutionContext ExecutionContext.h diff --git a/lldb/include/lldb/Target/StackFrame.h b/lldb/include/lldb/Target/StackFrame.h index 46922448d6e59..5cba9afe2a7e8 100644 --- a/lldb/include/lldb/Target/StackFrame.h +++ b/lldb/include/lldb/Target/StackFrame.h @@ -542,17 +542,17 @@ class StackFrame : public ExecutionContextScope, virtual lldb::RecognizedStackFrameSP GetRecognizedFrame(); - /// Get the StackFrameList that contains this frame. + /// Get the identifier of the StackFrameList that contains this frame. /// - /// Returns the StackFrameList that contains this frame, allowing + /// Returns the StackFrameList identifier that contains this frame, allowing /// frames to resolve execution contexts without calling /// Thread::GetStackFrameList(), which can cause circular dependencies /// during frame provider initialization. /// /// \return - /// The StackFrameList that contains this frame, or nullptr if not set. - virtual lldb::StackFrameListSP GetContainingStackFrameList() const { - return m_frame_list_wp.lock(); + /// The identifier of the containing StackFrameList + lldb::frame_list_id_t GetContainingStackFrameListIdentifier() const { + return m_frame_list_id; } protected: @@ -598,8 +598,8 @@ class StackFrame : public ExecutionContextScope, /// be the first address of its function). True for actual frame zero as /// well as any other frame with the same trait. bool m_behaves_like_zeroth_frame; + lldb::frame_list_id_t m_frame_list_id = 0; lldb::VariableListSP m_variable_list_sp; - lldb::StackFrameListWP m_frame_list_wp; /// Value objects for each variable in m_variable_list_sp. ValueObjectList m_variable_list_value_objects; std::optional m_recognized_frame_sp; diff --git a/lldb/include/lldb/Target/StackFrameList.h b/lldb/include/lldb/Target/StackFrameList.h index c096fe3ff61a0..715781abb83a3 100644 --- a/lldb/include/lldb/Target/StackFrameList.h +++ b/lldb/include/lldb/Target/StackFrameList.h @@ -24,7 +24,8 @@ class StackFrameList : public std::enable_shared_from_this { public: // Constructors and Destructors StackFrameList(Thread &thread, const lldb::StackFrameListSP &prev_frames_sp, - bool show_inline_frames); + bool show_inline_frames, + lldb::frame_list_id_t provider_id = 0); virtual ~StackFrameList(); @@ -104,6 +105,9 @@ class StackFrameList : public std::enable_shared_from_this { /// Get the thread associated with this frame list. Thread &GetThread() const { return m_thread; } + /// Get the unique identifier for this frame list. + lldb::frame_list_id_t GetIdentifier() const { return m_identifier; } + protected: friend class Thread; friend class ScriptedFrameProvider; @@ -212,6 +216,9 @@ class StackFrameList : public std::enable_shared_from_this { /// Whether or not to show synthetic (inline) frames. Immutable. const bool m_show_inlined_frames; + /// Unique identifier for this frame list instance. + lldb::frame_list_id_t m_identifier = 0; + /// Returns true if fetching frames was interrupted, false otherwise. virtual bool FetchFramesUpTo(uint32_t end_idx, InterruptionControl allow_interrupt); @@ -244,7 +251,8 @@ class SyntheticStackFrameList : public StackFrameList { SyntheticStackFrameList(Thread &thread, lldb::StackFrameListSP input_frames, const lldb::StackFrameListSP &prev_frames_sp, bool show_inline_frames, - lldb::SyntheticFrameProviderSP provider_sp); + lldb::SyntheticFrameProviderSP provider_sp, + uint64_t provider_id); protected: /// Override FetchFramesUpTo to lazily return frames from the provider diff --git a/lldb/include/lldb/Target/Thread.h b/lldb/include/lldb/Target/Thread.h index bc1bec57bee5f..9cc86a37c63e5 100644 --- a/lldb/include/lldb/Target/Thread.h +++ b/lldb/include/lldb/Target/Thread.h @@ -19,6 +19,7 @@ #include "lldb/Target/ExecutionContextScope.h" #include "lldb/Target/RegisterCheckpoint.h" #include "lldb/Target/StackFrameList.h" +#include "lldb/Target/SyntheticFrameProvider.h" #include "lldb/Utility/Broadcaster.h" #include "lldb/Utility/CompletionRequest.h" #include "lldb/Utility/Event.h" @@ -26,6 +27,7 @@ #include "lldb/Utility/UnimplementedError.h" #include "lldb/Utility/UserID.h" #include "lldb/lldb-private.h" +#include "llvm/ADT/DenseMap.h" #include "llvm/Support/MemoryBuffer.h" #define LLDB_THREAD_MAX_STOP_EXC_DATA 8 @@ -1297,12 +1299,18 @@ class Thread : public std::enable_shared_from_this, lldb::StackFrameListSP GetStackFrameList(); + /// Get a frame list by its unique identifier. + lldb::StackFrameListSP GetFrameListByIdentifier(lldb::frame_list_id_t id); + llvm::Error LoadScriptedFrameProvider(const ScriptedFrameProviderDescriptor &descriptor); + llvm::Expected + GetScriptedFrameProviderDescriptorForID(lldb::frame_list_id_t id) const; + void ClearScriptedFrameProvider(); - const llvm::SmallVector & + const llvm::DenseMap & GetFrameProviders() const { return m_frame_providers; } @@ -1384,6 +1392,8 @@ class Thread : public std::enable_shared_from_this, m_state_mutex; ///< Multithreaded protection for m_state. mutable std::recursive_mutex m_frame_mutex; ///< Multithreaded protection for m_state. + lldb::StackFrameListSP + m_unwinder_frames_sp; ///< The unwinder frame list (ID 0). lldb::StackFrameListSP m_curr_frames_sp; ///< The stack frames that get lazily ///populated after a thread stops. lldb::StackFrameListSP m_prev_frames_sp; ///< The previous stack frames from @@ -1410,8 +1420,23 @@ class Thread : public std::enable_shared_from_this, /// The Thread backed by this thread, if any. lldb::ThreadWP m_backed_thread; - /// The Scripted Frame Providers for this thread. - llvm::SmallVector m_frame_providers; + /// Map from frame list ID to its frame provider. + /// Cleared in ClearStackFrames(), repopulated in GetStackFrameList(). + llvm::DenseMap + m_frame_providers; + + /// Ordered chain of provider IDs. + /// Persists across ClearStackFrames() to maintain stable provider IDs. + std::vector> + m_provider_chain_ids; + + /// Map from frame list identifier to frame list weak pointer. + mutable llvm::DenseMap + m_frame_lists_by_id; + + /// Counter for assigning unique provider IDs. Starts at 1 since 0 is + /// reserved for normal unwinder frames. Persists across ClearStackFrames. + lldb::frame_list_id_t m_next_provider_id = 1; private: bool m_extended_info_fetched; // Have we tried to retrieve the m_extended_info diff --git a/lldb/include/lldb/lldb-defines.h b/lldb/include/lldb/lldb-defines.h index 52bf7c5cce947..8e1029387d2da 100644 --- a/lldb/include/lldb/lldb-defines.h +++ b/lldb/include/lldb/lldb-defines.h @@ -89,6 +89,7 @@ #define LLDB_INVALID_PROCESS_ID 0 #define LLDB_INVALID_THREAD_ID 0 #define LLDB_INVALID_FRAME_ID UINT32_MAX +#define LLDB_UNWINDER_FRAME_LIST_ID 0 #define LLDB_INVALID_SIGNAL_NUMBER INT32_MAX #define LLDB_INVALID_SYMBOL_ID UINT32_MAX #define LLDB_INVALID_OFFSET UINT64_MAX // Must match max of lldb::offset_t diff --git a/lldb/include/lldb/lldb-types.h b/lldb/include/lldb/lldb-types.h index e309fc8833ce9..bb4c34ef8e1f5 100644 --- a/lldb/include/lldb/lldb-types.h +++ b/lldb/include/lldb/lldb-types.h @@ -83,6 +83,7 @@ typedef uint64_t user_id_t; typedef uint64_t pid_t; typedef uint64_t tid_t; typedef uint64_t offset_t; +typedef uint32_t frame_list_id_t; typedef int32_t break_id_t; typedef int32_t watch_id_t; typedef uint32_t wp_resource_id_t; diff --git a/lldb/source/Target/ExecutionContext.cpp b/lldb/source/Target/ExecutionContext.cpp index b16ff26266c53..e4b2f07d8d8d1 100644 --- a/lldb/source/Target/ExecutionContext.cpp +++ b/lldb/source/Target/ExecutionContext.cpp @@ -160,6 +160,15 @@ lldb_private::GetStoppedExecutionContext( auto thread_sp = exe_ctx_ref_ptr->GetThreadSP(); auto frame_sp = exe_ctx_ref_ptr->GetFrameSP(); + + if (!frame_sp && exe_ctx_ref_ptr->m_frame_list_id) { + return llvm::createStringError( + "attempted to create a StoppedExecutionContext but " + "ScriptedFrameProvider (name = %s - id = %u) is no longer available", + exe_ctx_ref_ptr->m_frame_list_id->first.GetName().str().c_str(), + exe_ctx_ref_ptr->m_frame_list_id->second); + } + return StoppedExecutionContext(target_sp, process_sp, thread_sp, frame_sp, std::move(api_lock), std::move(stop_locker)); } @@ -466,12 +475,25 @@ operator=(const ExecutionContext &exe_ctx) { else m_tid = LLDB_INVALID_THREAD_ID; lldb::StackFrameSP frame_sp(exe_ctx.GetFrameSP()); - if (frame_sp) { - m_stack_id = frame_sp->GetStackID(); - m_frame_list_wp = frame_sp->GetContainingStackFrameList(); + + if (frame_sp && thread_sp) { + lldb::frame_list_id_t frame_list_id = + frame_sp->GetContainingStackFrameListIdentifier(); + auto frame_list_descriptor_or_err = + thread_sp->GetScriptedFrameProviderDescriptorForID(frame_list_id); + if (frame_list_descriptor_or_err) { + m_stack_id = frame_sp->GetStackID(); + m_frame_list_id = {*frame_list_descriptor_or_err, frame_list_id}; + } else { + LLDB_LOG_ERROR(GetLog(LLDBLog::Process), + frame_list_descriptor_or_err.takeError(), + "Failed to fetch scripted frame provider descriptor: {0}"); + m_stack_id.Clear(); + m_frame_list_id.reset(); + } } else { m_stack_id.Clear(); - m_frame_list_wp.reset(); + m_frame_list_id.reset(); } return *this; } @@ -512,11 +534,25 @@ void ExecutionContextRef::SetThreadSP(const lldb::ThreadSP &thread_sp) { } void ExecutionContextRef::SetFrameSP(const lldb::StackFrameSP &frame_sp) { - if (frame_sp) { + if (!frame_sp) { + Clear(); + return; + } + + lldb::ThreadSP thread_sp = frame_sp->GetThread(); + lldb::frame_list_id_t frame_list_id = + frame_sp->GetContainingStackFrameListIdentifier(); + auto frame_list_descriptor_or_err = + thread_sp->GetScriptedFrameProviderDescriptorForID(frame_list_id); + + if (frame_list_descriptor_or_err) { m_stack_id = frame_sp->GetStackID(); - m_frame_list_wp = frame_sp->GetContainingStackFrameList(); - SetThreadSP(frame_sp->GetThread()); + m_frame_list_id = {*frame_list_descriptor_or_err, frame_list_id}; + SetThreadSP(thread_sp); } else { + LLDB_LOG_ERROR(GetLog(LLDBLog::Process), + frame_list_descriptor_or_err.takeError(), + "Failed to fetch scripted frame provider descriptor: {0}"); ClearFrame(); ClearThread(); m_process_wp.reset(); @@ -641,21 +677,23 @@ lldb::ThreadSP ExecutionContextRef::GetThreadSP() const { } lldb::StackFrameSP ExecutionContextRef::GetFrameSP() const { - if (m_stack_id.IsValid()) { - // Try the remembered frame list first to avoid circular dependencies - // during frame provider initialization. - if (auto frame_list_sp = m_frame_list_wp.lock()) { + lldb::ThreadSP thread_sp(GetThreadSP()); + if (!thread_sp || !m_stack_id.IsValid()) + return lldb::StackFrameSP(); + + // Try the remembered frame list first to avoid circular dependencies + // during frame provider initialization. + if (m_frame_list_id) { + if (auto frame_list_sp = + thread_sp->GetFrameListByIdentifier(m_frame_list_id->second)) { if (auto frame_sp = frame_list_sp->GetFrameWithStackID(m_stack_id)) return frame_sp; } - - // Fallback: ask the thread, which might re-trigger the frame provider - // initialization. - lldb::ThreadSP thread_sp(GetThreadSP()); - if (thread_sp) - return thread_sp->GetFrameWithStackID(m_stack_id); } - return lldb::StackFrameSP(); + + // Fallback: ask the thread, which might re-trigger the frame provider + // initialization. + return thread_sp->GetFrameWithStackID(m_stack_id); } ExecutionContext diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp index e6112f8f3264b..94329e77460d9 100644 --- a/lldb/source/Target/StackFrameList.cpp +++ b/lldb/source/Target/StackFrameList.cpp @@ -38,12 +38,13 @@ using namespace lldb_private; // StackFrameList constructor StackFrameList::StackFrameList(Thread &thread, const lldb::StackFrameListSP &prev_frames_sp, - bool show_inline_frames) + bool show_inline_frames, + lldb::frame_list_id_t provider_id) : m_thread(thread), m_prev_frames_sp(prev_frames_sp), m_frames(), m_selected_frame_idx(), m_concrete_frames_fetched(0), m_current_inlined_depth(UINT32_MAX), m_current_inlined_pc(LLDB_INVALID_ADDRESS), - m_show_inlined_frames(show_inline_frames) { + m_show_inlined_frames(show_inline_frames), m_identifier(provider_id) { if (prev_frames_sp) { m_current_inlined_depth = prev_frames_sp->m_current_inlined_depth; m_current_inlined_pc = prev_frames_sp->m_current_inlined_pc; @@ -59,8 +60,8 @@ StackFrameList::~StackFrameList() { SyntheticStackFrameList::SyntheticStackFrameList( Thread &thread, lldb::StackFrameListSP input_frames, const lldb::StackFrameListSP &prev_frames_sp, bool show_inline_frames, - lldb::SyntheticFrameProviderSP provider_sp) - : StackFrameList(thread, prev_frames_sp, show_inline_frames), + lldb::SyntheticFrameProviderSP provider_sp, uint64_t provider_id) + : StackFrameList(thread, prev_frames_sp, show_inline_frames, provider_id), m_input_frames(std::move(input_frames)), m_provider(std::move(provider_sp)) {} @@ -70,12 +71,25 @@ bool SyntheticStackFrameList::FetchFramesUpTo( size_t num_synthetic_frames = 0; // Use the provider to generate frames lazily. if (m_provider) { + // Get starting index under lock. + uint32_t start_idx = 0; + { + std::shared_lock guard(m_list_mutex); + start_idx = m_frames.size(); + } + // Keep fetching until we reach end_idx or the provider returns an error. - for (uint32_t idx = m_frames.size(); idx <= end_idx; idx++) { + for (uint32_t idx = start_idx; idx <= end_idx; idx++) { if (allow_interrupt && m_thread.GetProcess()->GetTarget().GetDebugger().InterruptRequested()) return true; + + // Call Python WITHOUT holding lock - prevents deadlock. auto frame_or_err = m_provider->GetFrameAtIndex(idx); + + // Acquire lock to modify m_frames. + std::unique_lock guard(m_list_mutex); + if (!frame_or_err) { // Provider returned error - we've reached the end. LLDB_LOG_ERROR(GetLog(LLDBLog::Thread), frame_or_err.takeError(), @@ -89,7 +103,7 @@ bool SyntheticStackFrameList::FetchFramesUpTo( GetThread().GetProcess().get()); // Set the frame list weak pointer so ExecutionContextRef can resolve // the frame without calling Thread::GetStackFrameList(). - frame_sp->m_frame_list_wp = shared_from_this(); + frame_sp->m_frame_list_id = GetIdentifier(); m_frames.push_back(frame_sp); } @@ -375,7 +389,7 @@ void StackFrameList::SynthesizeTailCallFrames(StackFrame &next_frame) { m_thread.shared_from_this(), frame_idx, concrete_frame_idx, cfa, cfa_is_valid, pc, StackFrame::Kind::Regular, artificial, behaves_like_zeroth_frame, &sc); - synth_frame->m_frame_list_wp = shared_from_this(); + synth_frame->m_frame_list_id = GetIdentifier(); m_frames.push_back(synth_frame); LLDB_LOG(log, "Pushed frame {0} at {1:x}", callee->GetDisplayName(), pc); } @@ -410,6 +424,10 @@ bool StackFrameList::GetFramesUpTo(uint32_t end_idx, return false; } + // Release lock before FetchFramesUpTo which may call Python. + // FetchFramesUpTo will acquire locks as needed. + guard.unlock(); + // We're adding concrete and inlined frames now: was_interrupted = FetchFramesUpTo(end_idx, allow_interrupt); @@ -491,7 +509,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx, unwind_frame_sp = std::make_shared( m_thread.shared_from_this(), m_frames.size(), idx, reg_ctx_sp, cfa, pc, behaves_like_zeroth_frame, nullptr); - unwind_frame_sp->m_frame_list_wp = shared_from_this(); + unwind_frame_sp->m_frame_list_id = GetIdentifier(); m_frames.push_back(unwind_frame_sp); } } else { @@ -526,7 +544,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx, // although its concrete index will stay the same. SynthesizeTailCallFrames(*unwind_frame_sp.get()); - unwind_frame_sp->m_frame_list_wp = shared_from_this(); + unwind_frame_sp->m_frame_list_id = GetIdentifier(); m_frames.push_back(unwind_frame_sp); } @@ -551,7 +569,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx, unwind_frame_sp->GetRegisterContextSP(), cfa, next_frame_address, behaves_like_zeroth_frame, &next_frame_sc)); - frame_sp->m_frame_list_wp = shared_from_this(); + frame_sp->m_frame_list_id = GetIdentifier(); m_frames.push_back(frame_sp); unwind_sc = next_frame_sc; curr_frame_address = next_frame_address; @@ -608,7 +626,7 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx, prev_frame->UpdatePreviousFrameFromCurrentFrame(*curr_frame); // Now copy the fixed up previous frame into the current frames so the // pointer doesn't change. - prev_frame_sp->m_frame_list_wp = shared_from_this(); + prev_frame_sp->m_frame_list_id = GetIdentifier(); m_frames[curr_frame_idx] = prev_frame_sp; #if defined(DEBUG_STACK_FRAMES) diff --git a/lldb/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp index 70d8650662348..2c95c2d209b45 100644 --- a/lldb/source/Target/Thread.cpp +++ b/lldb/source/Target/Thread.cpp @@ -29,7 +29,6 @@ #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" @@ -57,6 +56,8 @@ #include "lldb/ValueObject/ValueObjectConstResult.h" #include "lldb/lldb-enumerations.h" +#include "llvm/Support/MathExtras.h" + #include #include @@ -262,7 +263,10 @@ void Thread::DestroyThread() { std::lock_guard guard(m_frame_mutex); m_curr_frames_sp.reset(); m_prev_frames_sp.reset(); + m_unwinder_frames_sp.reset(); m_frame_providers.clear(); + m_provider_chain_ids.clear(); + m_frame_lists_by_id.clear(); m_prev_framezero_pc.reset(); } @@ -1465,16 +1469,15 @@ StackFrameListSP Thread::GetStackFrameList() { const auto &descriptors = target.GetScriptedFrameProviderDescriptors(); // Collect all descriptors that apply to this thread. - std::vector - applicable_descriptors; + std::vector thread_descriptors; for (const auto &entry : descriptors) { const ScriptedFrameProviderDescriptor &descriptor = entry.second; if (descriptor.IsValid() && descriptor.AppliesToThread(*this)) - applicable_descriptors.push_back(&descriptor); + thread_descriptors.push_back(&descriptor); } // Sort by priority (lower number = higher priority). - llvm::sort(applicable_descriptors, + llvm::sort(thread_descriptors, [](const ScriptedFrameProviderDescriptor *a, const ScriptedFrameProviderDescriptor *b) { // nullopt (no priority) sorts last (UINT32_MAX). @@ -1484,7 +1487,7 @@ StackFrameListSP Thread::GetStackFrameList() { }); // Load ALL matching providers in priority order. - for (const auto *descriptor : applicable_descriptors) { + for (const auto *descriptor : thread_descriptors) { if (llvm::Error error = LoadScriptedFrameProvider(*descriptor)) { LLDB_LOG_ERROR(GetLog(LLDBLog::Thread), std::move(error), "Failed to load scripted frame provider: {0}"); @@ -1495,58 +1498,123 @@ StackFrameListSP Thread::GetStackFrameList() { } // Create the frame list based on whether we have providers. - if (!m_frame_providers.empty()) { + if (!m_provider_chain_ids.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( - *this, input_frames, m_prev_frames_sp, true, m_frame_providers.back()); + auto [last_desc, last_id] = m_provider_chain_ids.back(); + auto it = m_frame_providers.find(last_id); + if (it != m_frame_providers.end()) { + SyntheticFrameProviderSP last_provider = it->second; + StackFrameListSP input_frames = last_provider->GetInputFrames(); + m_curr_frames_sp = std::make_shared( + *this, input_frames, m_prev_frames_sp, true, last_provider, last_id); + } else { + LLDB_LOG(GetLog(LLDBLog::Thread), + "Missing frame provider (id = {0}) in Thread #{1:x}}", last_id, + GetID()); + } + } + + if (!m_curr_frames_sp) { + // No provider - use normal unwinder frames with stable ID = 0. + m_unwinder_frames_sp = std::make_shared( + *this, m_prev_frames_sp, true, /*provider_id=*/0); + m_curr_frames_sp = m_unwinder_frames_sp; } else { - // No provider - use normal unwinder frames. - m_curr_frames_sp = - std::make_shared(*this, m_prev_frames_sp, true); + // Register this frame list by its identifier for later lookup. + m_frame_lists_by_id.insert( + {m_curr_frames_sp->GetIdentifier(), m_curr_frames_sp}); } return m_curr_frames_sp; } +lldb::StackFrameListSP +Thread::GetFrameListByIdentifier(lldb::frame_list_id_t id) { + std::lock_guard guard(m_frame_mutex); + + // ID 0 is reserved for the unwinder frame list. Always return the unwinder + // frame list for ID 0. + if (id == 0) { + return m_unwinder_frames_sp; + } + + auto it = m_frame_lists_by_id.find(id); + if (it != m_frame_lists_by_id.end()) { + return it->second.lock(); + } + return nullptr; +} + llvm::Error Thread::LoadScriptedFrameProvider( const ScriptedFrameProviderDescriptor &descriptor) { std::lock_guard guard(m_frame_mutex); - // 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; + StackFrameListSP input_frames; if (m_frame_providers.empty()) { - // First provider gets real unwinder frames. - new_provider_input_frames = - std::make_shared(*this, m_prev_frames_sp, true); + // First provider gets real unwinder frames with stable ID = 0. + m_unwinder_frames_sp = + std::make_shared(*this, m_prev_frames_sp, true, + /*provider_id=*/0); + input_frames = m_unwinder_frames_sp; } 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( - *this, prev_provider_frames, m_prev_frames_sp, true, prev_provider); - } - - auto provider_or_err = SyntheticFrameProvider::CreateInstance( - new_provider_input_frames, descriptor); + // Subsequent providers wrap the previous provider. + auto [last_desc, last_id] = m_provider_chain_ids.back(); + auto it = m_frame_providers.find(last_id); + if (it == m_frame_providers.end()) + return llvm::createStringError("Previous frame provider not found"); + SyntheticFrameProviderSP last_provider = it->second; + StackFrameListSP last_provider_frames = last_provider->GetInputFrames(); + input_frames = std::make_shared( + *this, last_provider_frames, m_prev_frames_sp, true, last_provider, + last_id); + } + + auto provider_or_err = + SyntheticFrameProvider::CreateInstance(input_frames, descriptor); if (!provider_or_err) return provider_or_err.takeError(); - // Append to the chain. - m_frame_providers.push_back(*provider_or_err); + if (m_next_provider_id == std::numeric_limits::max()) + m_next_provider_id = 1; + else + m_next_provider_id++; + + lldb::frame_list_id_t provider_id = m_next_provider_id; + m_frame_providers.insert({provider_id, *provider_or_err}); + + // Add to the provider chain. + m_provider_chain_ids.push_back({descriptor, provider_id}); + return llvm::Error::success(); } +llvm::Expected +Thread::GetScriptedFrameProviderDescriptorForID( + lldb::frame_list_id_t id) const { + if (id == LLDB_UNWINDER_FRAME_LIST_ID) + return ScriptedFrameProviderDescriptor(); + + auto it = llvm::find_if( + m_provider_chain_ids, + [id](const std::pair &provider_id_pair) { + return provider_id_pair.second == id; + }); + + if (it == m_provider_chain_ids.end()) + return llvm::createStringError( + "Couldn't find ScriptedFrameProviderDescriptor for id = %u.", id); + + return it->first; +} + void Thread::ClearScriptedFrameProvider() { std::lock_guard guard(m_frame_mutex); m_frame_providers.clear(); + m_provider_chain_ids.clear(); + m_next_provider_id = 1; // Reset counter. + m_unwinder_frames_sp.reset(); m_curr_frames_sp.reset(); m_prev_frames_sp.reset(); } @@ -1570,8 +1638,13 @@ void Thread::ClearStackFrames() { if (m_curr_frames_sp && m_curr_frames_sp->WereAllFramesFetched()) m_prev_frames_sp.swap(m_curr_frames_sp); m_curr_frames_sp.reset(); + m_unwinder_frames_sp.reset(); + // Clear the provider instances, but keep the chain configuration + // (m_provider_chain_ids and m_next_provider_id) so provider IDs + // remain stable across ClearStackFrames() calls. m_frame_providers.clear(); + m_frame_lists_by_id.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 7dd74013b90f8..8397d60eedbb5 100644 --- a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py +++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py @@ -735,8 +735,11 @@ def test_get_values(self): """Test a frame that provides values.""" self.build() # Set the breakpoint after the variable_in_main variable exists and can be queried. - target, process, thread, bkpt = lldbutil.run_to_line_breakpoint( - self, lldb.SBFileSpec(self.source), 35, only_one_thread=False + target, process, thread, bkpt = lldbutil.run_to_source_breakpoint( + self, + "Breakpoint for variable tests", + lldb.SBFileSpec(self.source), + only_one_thread=False, ) # Get original frame count. @@ -771,7 +774,8 @@ def test_get_values(self): self.assertIsNotNone(frame0) # Get every variable visible at this point variables = frame0.GetVariables(True, True, True, False) - self.assertTrue(variables.IsValid() and variables.GetSize() == 1) + self.assertTrue(variables.IsValid()) + self.assertEqual(variables.GetSize(), 1) # Check that we can get values from paths. `_handler_one` is a special # value we provide through only our expression handler in the frame @@ -783,3 +787,326 @@ def test_get_values(self): self.assertEqual(var.unsigned, variables.GetValueAtIndex(0).unsigned) varp1 = frame0.GetValueForVariablePath("variable_in_main + 1") self.assertEqual(varp1.unsigned, 124) + + def test_frame_validity_after_step(self): + """Test that SBFrame references from ScriptedFrameProvider remain valid after stepping. + + This test verifies that ExecutionContextRef properly handles frame list identifiers + when the underlying stack changes. After stepping, old frame references should become + invalid gracefully without crashing. + """ + 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) + + # Register a provider that prepends synthetic frames. + error = lldb.SBError() + provider_id = target.RegisterScriptedFrameProvider( + "test_frame_providers.PrependFrameProvider", + lldb.SBStructuredData(), + error, + ) + self.assertTrue(error.Success(), f"Failed to register provider: {error}") + + # Get frame references before stepping. + frame0_before = thread.GetFrameAtIndex(0) + frame1_before = thread.GetFrameAtIndex(1) + frame2_before = thread.GetFrameAtIndex(2) + + self.assertIsNotNone(frame0_before) + self.assertIsNotNone(frame1_before) + self.assertIsNotNone(frame2_before) + + # Verify frames are valid and have expected PCs. + self.assertTrue(frame0_before.IsValid(), "Frame 0 should be valid before step") + self.assertTrue(frame1_before.IsValid(), "Frame 1 should be valid before step") + self.assertTrue(frame2_before.IsValid(), "Frame 2 should be valid before step") + + pc0_before = frame0_before.GetPC() + pc1_before = frame1_before.GetPC() + pc2_before = frame2_before.GetPC() + + self.assertEqual(pc0_before, 0x9000, "Frame 0 should have synthetic PC 0x9000") + self.assertEqual(pc1_before, 0xA000, "Frame 1 should have synthetic PC 0xA000") + + # Step the thread, which will invalidate the old frame list. + thread.StepInstruction(False) + + # After stepping, the frame list has changed. Old frame references should + # detect this and become invalid, but shouldn't crash. + # The key here is that GetPC() and other operations should handle the + # "frame provider no longer available" case gracefully. + + # Try to access the old frames - they should either: + # 1. Return invalid/default values gracefully, or + # 2. Still work if the frame provider is re-applied. + + # Get new frames after stepping. + frame0_after = thread.GetFrameAtIndex(0) + self.assertIsNotNone(frame0_after) + self.assertTrue( + frame0_after.IsValid(), "New frame 0 should be valid after step" + ) + + # The old frame references might or might not be valid depending on whether + # the frame provider is still active. What's important is that accessing + # them doesn't crash and handles the situation gracefully. + # We'll just verify we can call methods on them without crashing. + try: + _ = frame0_before.GetPC() + _ = frame0_before.IsValid() + _ = frame0_before.GetFunctionName() + except Exception as e: + self.fail(f"Accessing old frame reference should not crash: {e}") + + def test_provider_lifecycle_with_frame_validity(self): + """Test provider registration/removal at breakpoints and SBFrame validity across lifecycle. + + This test verifies: + 1. Registering a provider while stopped at a breakpoint. + 2. SBFrame references from synthetic frames persist across continues. + 3. SBFrame references can access variables in real frames while provider is active. + 4. Removing a provider while stopped at a breakpoint. + 5. SBFrame references from removed provider don't crash when accessed. + """ + self.build() + target = self.dbg.CreateTarget(self.getBuildArtifact("a.out")) + self.assertTrue(target.IsValid(), "Target should be valid") + + # Import the test frame provider. + script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py") + self.runCmd("command script import " + script_path) + + # Set up breakpoints at the return statements in foo, bar, and baz. + # This ensures local variables are initialized. + bp_foo = target.BreakpointCreateBySourceRegex( + "Break in foo", lldb.SBFileSpec(self.source) + ) + bp_bar = target.BreakpointCreateBySourceRegex( + "Break in bar", lldb.SBFileSpec(self.source) + ) + bp_baz = target.BreakpointCreateBySourceRegex( + "Break in baz", lldb.SBFileSpec(self.source) + ) + + self.assertTrue(bp_foo.IsValid(), "Breakpoint at foo should be valid") + self.assertTrue(bp_bar.IsValid(), "Breakpoint at bar should be valid") + self.assertTrue(bp_baz.IsValid(), "Breakpoint at baz should be valid") + + # Launch the process. + process = target.LaunchSimple(None, None, self.get_process_working_directory()) + self.assertTrue(process.IsValid(), "Process should be valid") + + # We should hit the foo breakpoint first. + self.assertEqual( + process.GetState(), lldb.eStateStopped, "Process should be stopped at foo" + ) + thread = process.GetSelectedThread() + self.assertIsNotNone(thread, "Should have a selected thread") + + # Register the provider at foo breakpoint. + 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") + + # Get individual frames BEFORE getting the full backtrace. + # This tests accessing frames before forcing evaluation of all frames. + frame0 = thread.GetFrameAtIndex(0) + frame1 = thread.GetFrameAtIndex(1) + frame2 = thread.GetFrameAtIndex(2) + + self.assertIsNotNone(frame0, "Frame 0 should exist") + self.assertIsNotNone(frame1, "Frame 1 should exist") + self.assertIsNotNone(frame2, "Frame 2 should exist") + + # First two frames should be synthetic with expected PCs. + pc0 = frame0.GetPC() + pc1 = frame1.GetPC() + + self.assertEqual(pc0, 0x9000, "Frame 0 should have synthetic PC 0x9000") + self.assertEqual(pc1, 0xA000, "Frame 1 should have synthetic PC 0xA000") + + # Frame 2 should be the real foo frame. + self.assertIn("foo", frame2.GetFunctionName(), "Frame 2 should be in foo") + + # Save references to the synthetic frames. + saved_frames = [frame0, frame1, frame2] + + # Test accessing saved frames at foo BEFORE getting full backtrace. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[1].GetPC() + _ = saved_frames[2].GetFunctionName() + except Exception as e: + self.fail( + f"Accessing saved frames at foo before full backtrace should not crash: {e}" + ) + + # Now verify the provider is active by checking frame count. + # PrependFrameProvider adds 2 synthetic frames. + # This forces a full backtrace evaluation. + original_frame_count = thread.GetNumFrames() + self.assertGreater( + original_frame_count, + 2, + "Should have at least synthetic frames + real frames", + ) + + # Test accessing saved frames at foo AFTER getting full backtrace. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[1].GetPC() + _ = saved_frames[2].GetFunctionName() + except Exception as e: + self.fail( + f"Accessing saved frames at foo after full backtrace should not crash: {e}" + ) + + # Verify we can access variables in frame2 (real frame). + foo_local = frame2.FindVariable("foo_local") + self.assertTrue(foo_local.IsValid(), "Should find foo_local variable") + self.assertEqual( + foo_local.GetValueAsUnsigned(), 20, "foo_local should be 20 (10 * 2)" + ) + + # Continue to bar breakpoint. + threads = lldbutil.continue_to_breakpoint(process, bp_bar) + self.assertIsNotNone(threads, "Should have stopped at bar breakpoint") + self.assertEqual(len(threads), 1, "Should have one thread stopped at bar") + thread = threads[0] + + # Verify the saved frames are still accessible without crashing at bar. + # Do this BEFORE getting the full backtrace. + # Note: They might not be "valid" in the traditional sense since we've moved + # to a different execution context, but they shouldn't crash. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[1].GetPC() + except Exception as e: + self.fail( + f"Accessing saved frames at bar before full backtrace should not crash: {e}" + ) + + # Verify the provider is still active by getting frame count. + # This forces full backtrace evaluation. + current_frame_count = thread.GetNumFrames() + self.assertGreater( + current_frame_count, 2, "Should still have synthetic frames at bar" + ) + + # Access the saved frames again AFTER getting the full backtrace. + # This ensures that forcing a full backtrace evaluation doesn't break + # the saved frame references. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[1].GetPC() + except Exception as e: + self.fail( + f"Accessing saved frames at bar after full backtrace should not crash: {e}" + ) + + # Get current frames at bar. + bar_frame0 = thread.GetFrameAtIndex(0) + bar_frame1 = thread.GetFrameAtIndex(1) + bar_frame2 = thread.GetFrameAtIndex(2) + + # Verify current frames have synthetic PCs. + self.assertEqual( + bar_frame0.GetPC(), 0x9000, "Frame 0 at bar should have synthetic PC" + ) + self.assertEqual( + bar_frame1.GetPC(), 0xA000, "Frame 1 at bar should have synthetic PC" + ) + self.assertIn("bar", bar_frame2.GetFunctionName(), "Frame 2 should be in bar") + + # Verify we can access variables in the bar frame. + bar_local = bar_frame2.FindVariable("bar_local") + self.assertTrue(bar_local.IsValid(), "Should find bar_local variable") + self.assertEqual( + bar_local.GetValueAsUnsigned(), 25, "bar_local should be 25 (5 * 5)" + ) + + # Continue to baz breakpoint. + threads = lldbutil.continue_to_breakpoint(process, bp_baz) + self.assertIsNotNone(threads, "Should have stopped at baz breakpoint") + self.assertEqual(len(threads), 1, "Should have one thread stopped at baz") + thread = threads[0] + + # Verify the saved frames are still accessible without crashing at baz. + # Do this BEFORE getting the full backtrace. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[1].GetPC() + _ = saved_frames[2].GetFunctionName() + except Exception as e: + self.fail( + f"Accessing saved frames at baz before full backtrace should not crash: {e}" + ) + + # Get the frame count to force full backtrace evaluation at baz. + baz_frame_count = thread.GetNumFrames() + self.assertGreater( + baz_frame_count, 2, "Should still have synthetic frames at baz" + ) + + # Verify the saved frames are still accessible AFTER getting full backtrace at baz. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[1].GetPC() + _ = saved_frames[2].GetFunctionName() + except Exception as e: + self.fail( + f"Accessing saved frames at baz after full backtrace should not crash: {e}" + ) + + # Now manually remove the provider. + result = target.RemoveScriptedFrameProvider(provider_id) + self.assertSuccess( + result, f"Should successfully remove provider with ID {provider_id}" + ) + # Verify frames no longer have synthetic frames. + final_frame_count = thread.GetNumFrames() + + # Without the provider, we should have fewer frames (no synthetic ones). + self.assertLess( + final_frame_count, + original_frame_count, + "Frame count should decrease after provider removal", + ) + + # First frame should now be the real baz frame (no synthetic frames). + baz_frame0 = thread.GetFrameAtIndex(0) + self.assertIn( + "baz", baz_frame0.GetFunctionName(), "Frame 0 should now be real baz frame" + ) + + # The synthetic PC values should no longer appear. + for i in range(final_frame_count): + frame = thread.GetFrameAtIndex(i) + pc = frame.GetPC() + self.assertNotEqual( + pc, 0x9000, f"Frame {i} should not have synthetic PC 0x9000" + ) + self.assertNotEqual( + pc, 0xA000, f"Frame {i} should not have synthetic PC 0xA000" + ) + + # Verify the originally saved frames are now truly invalid/stale. + # They should still not crash when accessed. + try: + _ = saved_frames[0].GetPC() + _ = saved_frames[0].IsValid() + _ = saved_frames[1].GetPC() + _ = saved_frames[1].IsValid() + except Exception as e: + self.fail(f"Accessing invalidated frames should not crash: {e}") diff --git a/lldb/test/API/functionalities/scripted_frame_provider/main.cpp b/lldb/test/API/functionalities/scripted_frame_provider/main.cpp index e1d346c29052b..e17226a459033 100644 --- a/lldb/test/API/functionalities/scripted_frame_provider/main.cpp +++ b/lldb/test/API/functionalities/scripted_frame_provider/main.cpp @@ -10,6 +10,24 @@ std::condition_variable cv; int ready_count = 0; constexpr int NUM_THREADS = 2; +int foo(int x) { + int foo_local = x * 2; + int foo_result = foo_local + 1; + return foo_result; // Break in foo. +} + +int bar(int x) { + int bar_local = x * x; + int bar_result = bar_local - 3; + return bar_result; // Break in bar. +} + +int baz(int x) { + int baz_local = x + 7; + int baz_result = baz_local / 2; + return baz_result; // Break in baz. +} + void thread_func(int thread_num) { std::cout << "Thread " << thread_num << " started\n"; @@ -31,7 +49,11 @@ int main(int argc, char **argv) { // Used as an existing C++ variable we can anchor on. int variable_in_main = 123; - (void)variable_in_main; + (void)variable_in_main; // Breakpoint for variable tests. + + // Call foo for first breakpoint. + int result_foo = foo(10); + (void)result_foo; for (int i = 0; i < NUM_THREADS; i++) { threads[i] = std::thread(thread_func, i); @@ -49,6 +71,14 @@ int main(int argc, char **argv) { std::cout << "Main thread at barrier\n"; + // Call bar for second breakpoint. + int result_bar = bar(5); + (void)result_bar; + + // Call baz for third breakpoint. + int result_baz = baz(11); + (void)result_baz; + for (int i = 0; i < NUM_THREADS; i++) threads[i].join(); diff --git a/lldb/test/API/macosx/extended-backtrace-api/Makefile b/lldb/test/API/macosx/extended-backtrace-api/Makefile new file mode 100644 index 0000000000000..845553d5e3f2f --- /dev/null +++ b/lldb/test/API/macosx/extended-backtrace-api/Makefile @@ -0,0 +1,3 @@ +OBJC_SOURCES := main.m + +include Makefile.rules diff --git a/lldb/test/API/macosx/extended-backtrace-api/TestExtendedBacktraceAPI.py b/lldb/test/API/macosx/extended-backtrace-api/TestExtendedBacktraceAPI.py new file mode 100644 index 0000000000000..0e9ee0755065e --- /dev/null +++ b/lldb/test/API/macosx/extended-backtrace-api/TestExtendedBacktraceAPI.py @@ -0,0 +1,144 @@ +"""Test SBThread.GetExtendedBacktraceThread API with queue debugging.""" + +import os +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil + + +class TestExtendedBacktraceAPI(TestBase): + NO_DEBUG_INFO_TESTCASE = True + + def setUp(self): + TestBase.setUp(self) + self.main_source = "main.m" + + @skipUnlessDarwin + @add_test_categories(["objc", "pyapi"]) + def test_extended_backtrace_thread_api(self): + """Test GetExtendedBacktraceThread with queue debugging.""" + self.build() + exe = self.getBuildArtifact("a.out") + + # Get Xcode developer directory path. + # Try DEVELOPER_DIR environment variable first, then fall back to xcode-select. + xcode_dev_path = os.environ.get("DEVELOPER_DIR") + + if not xcode_dev_path: + import subprocess + + xcode_dev_path = ( + subprocess.check_output(["xcode-select", "-p"]).decode("utf-8").strip() + ) + + # Check for libBacktraceRecording.dylib. + libbtr_path = os.path.join( + xcode_dev_path, "usr/lib/libBacktraceRecording.dylib" + ) + + self.assertTrue( + os.path.isfile(libbtr_path), + f"libBacktraceRecording.dylib is not present at {libbtr_path}", + ) + + self.assertTrue( + os.path.isfile("/usr/lib/system/introspection/libdispatch.dylib"), + "introspection libdispatch dylib not installed.", + ) + + # Create launch info with environment variables for libBacktraceRecording. + launch_info = lldb.SBLaunchInfo(None) + launch_info.SetWorkingDirectory(self.get_process_working_directory()) + launch_info.SetEnvironmentEntries( + [ + f"DYLD_INSERT_LIBRARIES={libbtr_path}", + "DYLD_LIBRARY_PATH=/usr/lib/system/introspection", + ], + True, + ) + + # Launch the process and run to breakpoint. + target, process, thread, bp = lldbutil.run_to_name_breakpoint( + self, "do_work_level_5", launch_info=launch_info, bkpt_module="a.out" + ) + + self.assertTrue(target.IsValid(), VALID_TARGET) + self.assertTrue(process.IsValid(), PROCESS_IS_VALID) + self.assertTrue(thread.IsValid(), "Stopped thread is valid") + self.assertTrue(bp.IsValid(), VALID_BREAKPOINT) + + # Call GetNumQueues to ensure queue information is loaded. + num_queues = process.GetNumQueues() + + # Check that we can find the com.apple.main-thread queue. + main_thread_queue_found = False + for i in range(num_queues): + queue = process.GetQueueAtIndex(i) + if queue.GetName() == "com.apple.main-thread": + main_thread_queue_found = True + break + + # Verify we have at least 5 frames. + self.assertGreaterEqual( + thread.GetNumFrames(), + 5, + "Thread should have at least 5 frames in backtrace", + ) + + # Get frame 2 BEFORE calling GetExtendedBacktraceThread. + # This mimics what Xcode does - it has the frame objects ready. + frame2 = thread.GetFrameAtIndex(2) + self.assertTrue(frame2.IsValid(), "Frame 2 is valid") + + # Now test GetExtendedBacktraceThread. + # This is the critical part - getting the extended backtrace calls into + # libBacktraceRecording which does an inferior function call, and this + # invalidates/clears the unwinder state. + extended_thread = thread.GetExtendedBacktraceThread("libdispatch") + + # This should be valid since we injected libBacktraceRecording. + self.assertTrue( + extended_thread.IsValid(), + "Extended backtrace thread for 'libdispatch' should be valid with libBacktraceRecording loaded", + ) + + # The extended thread should have frames. + self.assertGreater( + extended_thread.GetNumFrames(), + 0, + "Extended backtrace thread should have at least one frame", + ) + + # Test frame 2 on the extended backtrace thread. + self.assertGreater( + extended_thread.GetNumFrames(), + 2, + "Extended backtrace thread should have at least 3 frames to access frame 2", + ) + + extended_frame2 = extended_thread.GetFrameAtIndex(2) + self.assertTrue(extended_frame2.IsValid(), "Extended thread frame 2 is valid") + + # NOW try to access variables from frame 2 of the ORIGINAL thread. + # This is the key test - after GetExtendedBacktraceThread() has executed + # an inferior function call, the unwinder state may be invalidated. + # Xcode exhibits this bug where variables show "register fp is not available" + # after extended backtrace retrieval. + + # Set frame 2 as the selected frame so expect_var_path works. + thread.SetSelectedFrame(2) + + variables = frame2.GetVariables(False, True, False, True) + self.assertGreater( + variables.GetSize(), 0, "Frame 2 should have at least one variable" + ) + + # Test all variables in frame 2, like Xcode does. + # Use expect_var_path to verify each variable is accessible without errors. + for i in range(variables.GetSize()): + var = variables.GetValueAtIndex(i) + var_name = var.GetName() + + # This will fail if the variable contains "not available" or has errors. + self.expect_var_path(var_name) diff --git a/lldb/test/API/macosx/extended-backtrace-api/main.m b/lldb/test/API/macosx/extended-backtrace-api/main.m new file mode 100644 index 0000000000000..8f2186845a651 --- /dev/null +++ b/lldb/test/API/macosx/extended-backtrace-api/main.m @@ -0,0 +1,53 @@ +#include +#include +#include +#include + +void do_work_level_5(void) { + // Frame 0 will have these variables. + int frame0_var = 100; + const char *frame0_string = "frame_zero"; + float frame0_float = 1.5f; + + // This is where we'll set the breakpoint. + printf("Level 5 work executing\n"); // Break here. + while (1) + sleep(1); +} + +void do_work_level_4(void) { + // Frame 1 will have these variables. + int frame1_var = 200; + const char *frame1_string = "frame_one"; + long frame1_long = 9876543210L; + + do_work_level_5(); +} + +void do_work_level_3(void) { + // Frame 2 will have these variables. + int test_variable = 42; + const char *test_string = "test_value"; + double test_double = 3.14159; + + do_work_level_4(); +} + +void do_work_level_2(void) { do_work_level_3(); } + +void do_work_level_1(void *context) { do_work_level_2(); } + +int main(int argc, const char *argv[]) { + // Create a serial dispatch queue. + dispatch_queue_t worker_queue = + dispatch_queue_create("com.test.worker_queue", DISPATCH_QUEUE_SERIAL); + dispatch_queue_t submitter_queue = + dispatch_queue_create("com.test.submitter_queue", DISPATCH_QUEUE_SERIAL); + + // Submit work from one queue to another to create extended backtrace. + dispatch_async_f(submitter_queue, &worker_queue, do_work_level_1); + + // Keep main thread alive. + dispatch_main(); + return 0; +} From bc604c7ed648834b430c144c02f5cb87a0b4711f Mon Sep 17 00:00:00 2001 From: Adrian Vogelsgesang Date: Thu, 5 Feb 2026 21:00:23 +0100 Subject: [PATCH 7/7] [lldb] Broadcast `eBroadcastBitStackChanged` when frame providers change (#171482) We want to reload the call stack whenever the frame providers are updated. To do so, we now emit a `eBroadcastBitStackChanged` on all threads whenever any changes to the frame providers take place. I found this very useful while iterating on a frame provider in lldb-dap. So far, the new frame provider only took effect after continuing execution. Now the backtrace in VS-Code gets refreshed immediately upon running `target frame-provider add`. (cherry picked from commit 943782be5aec5db854065145dd73a618c3a775be) --- lldb/include/lldb/Target/Target.h | 6 ++ lldb/source/Target/Target.cpp | 59 ++++++++------ .../TestScriptedFrameProvider.py | 77 +++++++++++++++++++ 3 files changed, 119 insertions(+), 23 deletions(-) diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h index 812a638910b3b..b2aea3a6ddc39 100644 --- a/lldb/include/lldb/Target/Target.h +++ b/lldb/include/lldb/Target/Target.h @@ -776,6 +776,12 @@ class Target : public std::enable_shared_from_this, const llvm::DenseMap & GetScriptedFrameProviderDescriptors() const; +protected: + /// Invalidate all potentially cached frame providers for all threads + /// and trigger a stack changed event for all threads. + void InvalidateThreadFrameProviders(); + +public: // This part handles the breakpoints. BreakpointList &GetBreakpointList(bool internal = false); diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp index f3e058c6cbc9b..1ff115e980cf9 100644 --- a/lldb/source/Target/Target.cpp +++ b/lldb/source/Target/Target.cpp @@ -3725,47 +3725,45 @@ llvm::Expected Target::AddScriptedFrameProviderDescriptor( if (!descriptor.IsValid()) return llvm::createStringError("invalid frame provider descriptor"); + uint32_t descriptor_id = descriptor.GetID(); + llvm::StringRef name = descriptor.GetName(); if (name.empty()) return llvm::createStringError( "frame provider descriptor has no class name"); - std::lock_guard guard( - m_frame_provider_descriptors_mutex); - - uint32_t descriptor_id = descriptor.GetID(); - m_frame_provider_descriptors[descriptor_id] = descriptor; + { + std::unique_lock guard( + m_frame_provider_descriptors_mutex); + 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(); + InvalidateThreadFrameProviders(); return descriptor_id; } bool Target::RemoveScriptedFrameProviderDescriptor(uint32_t id) { - std::lock_guard guard( - m_frame_provider_descriptors_mutex); - bool removed = m_frame_provider_descriptors.erase(id); + bool removed = false; + { + std::lock_guard guard( + m_frame_provider_descriptors_mutex); + removed = m_frame_provider_descriptors.erase(id); + } if (removed) - if (ProcessSP process_sp = GetProcessSP()) - for (ThreadSP thread_sp : process_sp->Threads()) - thread_sp->ClearScriptedFrameProvider(); - + InvalidateThreadFrameProviders(); return removed; } void Target::ClearScriptedFrameProviderDescriptors() { - std::lock_guard guard( - m_frame_provider_descriptors_mutex); - - m_frame_provider_descriptors.clear(); + { + std::lock_guard 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(); + InvalidateThreadFrameProviders(); } const llvm::DenseMap & @@ -3775,6 +3773,21 @@ Target::GetScriptedFrameProviderDescriptors() const { return m_frame_provider_descriptors; } +void Target::InvalidateThreadFrameProviders() { + ProcessSP process_sp = GetProcessSP(); + if (!process_sp) + return; + for (ThreadSP thread_sp : process_sp->Threads()) { + // Clear frame providers on existing threads so they reload with new config. + thread_sp->ClearScriptedFrameProvider(); + // Notify threads that the stack traces might have changed. + if (thread_sp->EventTypeHasListeners(Thread::eBroadcastBitStackChanged)) { + auto data_sp = std::make_shared(thread_sp); + thread_sp->BroadcastEvent(Thread::eBroadcastBitStackChanged, data_sp); + } + } +} + void Target::FinalizeFileActions(ProcessLaunchInfo &info) { Log *log = GetLog(LLDBLog::Process); diff --git a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py index 8397d60eedbb5..8c2d8f0d5ad52 100644 --- a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py +++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py @@ -1110,3 +1110,80 @@ def test_provider_lifecycle_with_frame_validity(self): _ = saved_frames[1].IsValid() except Exception as e: self.fail(f"Accessing invalidated frames should not crash: {e}") + + def test_event_broadcasting(self): + """Test that adding/removing frame providers broadcasts eBroadcastBitStackChanged.""" + self.build() + + listener = lldb.SBListener("stack_changed_listener") + listener.StartListeningForEventClass( + self.dbg, + lldb.SBThread.GetBroadcasterClassName(), + lldb.SBThread.eBroadcastBitStackChanged, + ) + + target, process, thread, bkpt = lldbutil.run_to_source_breakpoint( + self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False + ) + + expected_thread_ids = { + process.GetThreadAtIndex(i).GetIndexID() + for i in range(process.GetNumThreads()) + } + + def collect_stack_changed_thread_ids(count): + event = lldb.SBEvent() + thread_ids = set() + for _ in range(count): + if not listener.WaitForEvent(5, event): + break + self.assertEqual( + event.GetType(), + lldb.SBThread.eBroadcastBitStackChanged, + "Event should be stack changed", + ) + thread_ids.add(lldb.SBThread.GetThreadFromEvent(event).GetIndexID()) + return thread_ids + + # Import the test frame provider. + script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py") + self.runCmd("command script import " + script_path) + + # 1. Test registration. + error = lldb.SBError() + provider_id = target.RegisterScriptedFrameProvider( + "test_frame_providers.ReplaceFrameProvider", + lldb.SBStructuredData(), + error, + ) + self.assertSuccess(error, f"Failed to register provider: {error}") + self.assertEqual( + collect_stack_changed_thread_ids(len(expected_thread_ids)), + expected_thread_ids, + "All threads should broadcast eBroadcastBitStackChanged on registration", + ) + + # 2. Test removal. + result = target.RemoveScriptedFrameProvider(provider_id) + self.assertSuccess(result, f"Failed to remove provider: {result}") + self.assertEqual( + collect_stack_changed_thread_ids(len(expected_thread_ids)), + expected_thread_ids, + "All threads should broadcast eBroadcastBitStackChanged on removal", + ) + + # 3. Test clear. + target.RegisterScriptedFrameProvider( + "test_frame_providers.ReplaceFrameProvider", + lldb.SBStructuredData(), + error, + ) + # Consume registration + collect_stack_changed_thread_ids(len(expected_thread_ids)) + + self.runCmd("target frame-provider clear") + self.assertEqual( + collect_stack_changed_thread_ids(len(expected_thread_ids)), + expected_thread_ids, + "All threads should broadcast eBroadcastBitStackChanged on clear", + ) \ No newline at end of file