[lldb] Introduce SBFrameList for lazy frame iteration (#166651)

This patch introduces `SBFrameList`, a new SBAPI class that allows
iterating over stack frames lazily without calling
`SBThread::GetFrameAtIndex` in a loop.

The new `SBThread::GetFrames()` method returns an `SBFrameList` that
supports Python iteration (`for frame in frame_list:`), indexing
(`frame_list[0]`, `frame_list[-1]`), and length queries (`len()`).

The implementation uses `StackFrameListSP` as the opaque pointer,
sharing the thread's underlying frame list to ensure frames are
materialized on-demand.

This is particularly useful for ScriptedFrameProviders, where user
scripts will be to iterate, filter, and replace frames lazily without
materializing the entire stack upfront.

Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
This commit is contained in:
Med Ismail Bennani 2025-11-05 16:02:02 -08:00 committed by GitHub
parent 8321eaa037
commit d584d00ed2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 477 additions and 3 deletions

View File

@ -0,0 +1,41 @@
%extend lldb::SBFrameList {
#ifdef SWIGPYTHON
%nothreadallow;
#endif
std::string lldb::SBFrameList::__str__ (){
lldb::SBStream description;
if (!$self->GetDescription(description))
return std::string("<empty> lldb.SBFrameList()");
const char *desc = description.GetData();
size_t desc_len = description.GetSize();
if (desc_len > 0 && (desc[desc_len-1] == '\n' || desc[desc_len-1] == '\r'))
--desc_len;
return std::string(desc, desc_len);
}
#ifdef SWIGPYTHON
%clearnothreadallow;
#endif
#ifdef SWIGPYTHON
%pythoncode %{
def __iter__(self):
'''Iterate over all frames in a lldb.SBFrameList object.'''
return lldb_iter(self, 'GetSize', 'GetFrameAtIndex')
def __len__(self):
return int(self.GetSize())
def __getitem__(self, key):
if type(key) is not int:
return None
if key < 0:
count = len(self)
if -count <= key < count:
key %= count
frame = self.GetFrameAtIndex(key)
return frame if frame.IsValid() else None
%}
#endif
}

View File

@ -41,7 +41,8 @@ STRING_EXTENSION_OUTSIDE(SBThread)
def get_thread_frames(self):
'''An accessor function that returns a list() that contains all frames in a lldb.SBThread object.'''
frames = []
for frame in self:
frame_list = self.GetFrames()
for frame in frame_list:
frames.append(frame)
return frames

View File

@ -119,6 +119,7 @@
%include "lldb/API/SBFileSpecList.h"
%include "lldb/API/SBFormat.h"
%include "lldb/API/SBFrame.h"
%include "lldb/API/SBFrameList.h"
%include "lldb/API/SBFunction.h"
%include "lldb/API/SBHostOS.h"
%include "lldb/API/SBInstruction.h"
@ -193,6 +194,7 @@
%include "./interface/SBFileSpecExtensions.i"
%include "./interface/SBFileSpecListExtensions.i"
%include "./interface/SBFrameExtensions.i"
%include "./interface/SBFrameListExtensions.i"
%include "./interface/SBFunctionExtensions.i"
%include "./interface/SBInstructionExtensions.i"
%include "./interface/SBInstructionListExtensions.i"

View File

@ -37,6 +37,7 @@
#include "lldb/API/SBFileSpecList.h"
#include "lldb/API/SBFormat.h"
#include "lldb/API/SBFrame.h"
#include "lldb/API/SBFrameList.h"
#include "lldb/API/SBFunction.h"
#include "lldb/API/SBHostOS.h"
#include "lldb/API/SBInstruction.h"

View File

@ -76,6 +76,7 @@ class LLDB_API SBFileSpec;
class LLDB_API SBFileSpecList;
class LLDB_API SBFormat;
class LLDB_API SBFrame;
class LLDB_API SBFrameList;
class LLDB_API SBFunction;
class LLDB_API SBHostOS;
class LLDB_API SBInstruction;

View File

@ -222,6 +222,7 @@ public:
protected:
friend class SBBlock;
friend class SBExecutionContext;
friend class SBFrameList;
friend class SBInstruction;
friend class SBThread;
friend class SBValue;

View File

@ -0,0 +1,82 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#ifndef LLDB_API_SBFRAMELIST_H
#define LLDB_API_SBFRAMELIST_H
#include "lldb/API/SBDefines.h"
namespace lldb {
/// Represents a list of SBFrame objects.
///
/// SBFrameList provides a way to iterate over stack frames lazily,
/// materializing frames on-demand as they are accessed. This is more
/// efficient than eagerly creating all frames upfront.
class LLDB_API SBFrameList {
public:
SBFrameList();
SBFrameList(const lldb::SBFrameList &rhs);
~SBFrameList();
const lldb::SBFrameList &operator=(const lldb::SBFrameList &rhs);
explicit operator bool() const;
bool IsValid() const;
/// Returns the number of frames in the list.
uint32_t GetSize() const;
/// Returns the frame at the given index.
///
/// \param[in] idx
/// The index of the frame to retrieve (0-based).
///
/// \return
/// An SBFrame object for the frame at the specified index.
/// Returns an invalid SBFrame if idx is out of range.
lldb::SBFrame GetFrameAtIndex(uint32_t idx) const;
/// Get the thread associated with this frame list.
///
/// \return
/// An SBThread object representing the thread.
lldb::SBThread GetThread() const;
/// Clear all frames from this list.
void Clear();
/// Get a description of this frame list.
///
/// \param[in] description
/// The stream to write the description to.
///
/// \return
/// True if the description was successfully written.
bool GetDescription(lldb::SBStream &description) const;
protected:
friend class SBThread;
private:
SBFrameList(const lldb::StackFrameListSP &frame_list_sp);
void SetFrameList(const lldb::StackFrameListSP &frame_list_sp);
// This needs to be a shared_ptr since an SBFrameList can be passed to
// scripting affordances like ScriptedFrameProviders but also out of
// convenience because Thread::GetStackFrameList returns a StackFrameListSP.
lldb::StackFrameListSP m_opaque_sp;
};
} // namespace lldb
#endif // LLDB_API_SBFRAMELIST_H

View File

@ -81,6 +81,7 @@ protected:
friend class SBFileSpec;
friend class SBFileSpecList;
friend class SBFrame;
friend class SBFrameList;
friend class SBFunction;
friend class SBInstruction;
friend class SBInstructionList;

View File

@ -186,6 +186,8 @@ public:
lldb::SBFrame GetFrameAtIndex(uint32_t idx);
lldb::SBFrameList GetFrames();
lldb::SBFrame GetSelectedFrame();
lldb::SBFrame SetSelectedFrame(uint32_t frame_idx);
@ -244,6 +246,7 @@ private:
friend class SBSaveCoreOptions;
friend class SBExecutionContext;
friend class SBFrame;
friend class SBFrameList;
friend class SBProcess;
friend class SBDebugger;
friend class SBValue;

View File

@ -101,6 +101,9 @@ public:
/// Returns whether we have currently fetched all the frames of a stack.
bool WereAllFramesFetched() const;
/// Get the thread associated with this frame list.
Thread &GetThread() const { return m_thread; }
protected:
friend class Thread;
friend class ScriptedThread;

View File

@ -1295,6 +1295,8 @@ public:
/// an empty std::optional is returned in that case.
std::optional<lldb::addr_t> GetPreviousFrameZeroPC();
lldb::StackFrameListSP GetStackFrameList();
protected:
friend class ThreadPlan;
friend class ThreadList;
@ -1336,8 +1338,6 @@ protected:
return StructuredData::ObjectSP();
}
lldb::StackFrameListSP GetStackFrameList();
void SetTemporaryResumeState(lldb::StateType new_state) {
m_temporary_resume_state = new_state;
}

View File

@ -69,6 +69,7 @@ add_lldb_library(liblldb SHARED ${option_framework}
SBFileSpecList.cpp
SBFormat.cpp
SBFrame.cpp
SBFrameList.cpp
SBFunction.cpp
SBHostOS.cpp
SBInstruction.cpp

View File

@ -0,0 +1,97 @@
//===----------------------------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception.
//
//===----------------------------------------------------------------------===//
#include "lldb/API/SBFrameList.h"
#include "lldb/API/SBFrame.h"
#include "lldb/API/SBStream.h"
#include "lldb/API/SBThread.h"
#include "lldb/Target/StackFrameList.h"
#include "lldb/Target/Thread.h"
#include "lldb/Utility/Instrumentation.h"
using namespace lldb;
using namespace lldb_private;
SBFrameList::SBFrameList() : m_opaque_sp() { LLDB_INSTRUMENT_VA(this); }
SBFrameList::SBFrameList(const SBFrameList &rhs)
: m_opaque_sp(rhs.m_opaque_sp) {
LLDB_INSTRUMENT_VA(this, rhs);
}
SBFrameList::~SBFrameList() = default;
const SBFrameList &SBFrameList::operator=(const SBFrameList &rhs) {
LLDB_INSTRUMENT_VA(this, rhs);
if (this != &rhs)
m_opaque_sp = rhs.m_opaque_sp;
return *this;
}
SBFrameList::SBFrameList(const lldb::StackFrameListSP &frame_list_sp)
: m_opaque_sp(frame_list_sp) {}
void SBFrameList::SetFrameList(const lldb::StackFrameListSP &frame_list_sp) {
m_opaque_sp = frame_list_sp;
}
SBFrameList::operator bool() const {
LLDB_INSTRUMENT_VA(this);
return m_opaque_sp.get() != nullptr;
}
bool SBFrameList::IsValid() const {
LLDB_INSTRUMENT_VA(this);
return this->operator bool();
}
uint32_t SBFrameList::GetSize() const {
LLDB_INSTRUMENT_VA(this);
if (m_opaque_sp)
return m_opaque_sp->GetNumFrames();
return 0;
}
SBFrame SBFrameList::GetFrameAtIndex(uint32_t idx) const {
LLDB_INSTRUMENT_VA(this, idx);
SBFrame sb_frame;
if (m_opaque_sp)
sb_frame.SetFrameSP(m_opaque_sp->GetFrameAtIndex(idx));
return sb_frame;
}
SBThread SBFrameList::GetThread() const {
LLDB_INSTRUMENT_VA(this);
SBThread sb_thread;
if (m_opaque_sp)
sb_thread.SetThread(m_opaque_sp->GetThread().shared_from_this());
return sb_thread;
}
void SBFrameList::Clear() {
LLDB_INSTRUMENT_VA(this);
if (m_opaque_sp)
m_opaque_sp->Clear();
}
bool SBFrameList::GetDescription(SBStream &description) const {
LLDB_INSTRUMENT_VA(this, description);
if (!m_opaque_sp)
return false;
Stream &strm = description.ref();
m_opaque_sp->Dump(&strm);
return true;
}

View File

@ -14,6 +14,7 @@
#include "lldb/API/SBFileSpec.h"
#include "lldb/API/SBFormat.h"
#include "lldb/API/SBFrame.h"
#include "lldb/API/SBFrameList.h"
#include "lldb/API/SBProcess.h"
#include "lldb/API/SBStream.h"
#include "lldb/API/SBStructuredData.h"
@ -1102,6 +1103,26 @@ SBFrame SBThread::GetFrameAtIndex(uint32_t idx) {
return sb_frame;
}
lldb::SBFrameList SBThread::GetFrames() {
LLDB_INSTRUMENT_VA(this);
SBFrameList sb_frame_list;
llvm::Expected<StoppedExecutionContext> exe_ctx =
GetStoppedExecutionContext(m_opaque_sp);
if (!exe_ctx) {
LLDB_LOG_ERROR(GetLog(LLDBLog::API), exe_ctx.takeError(), "{0}");
return SBFrameList();
}
if (exe_ctx->HasThreadScope()) {
StackFrameListSP frame_list_sp =
exe_ctx->GetThreadPtr()->GetStackFrameList();
sb_frame_list.SetFrameList(frame_list_sp);
}
return sb_frame_list;
}
lldb::SBFrame SBThread::GetSelectedFrame() {
LLDB_INSTRUMENT_VA(this);

View File

@ -0,0 +1,3 @@
CXX_SOURCES := main.cpp
include Makefile.rules

View File

@ -0,0 +1,194 @@
"""
Test SBFrameList API.
"""
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
class FrameListAPITestCase(TestBase):
def test_frame_list_api(self):
"""Test SBThread.GetFrames() returns a valid SBFrameList."""
self.build()
self.frame_list_api()
def test_frame_list_iterator(self):
"""Test SBFrameList iterator functionality."""
self.build()
self.frame_list_iterator()
def test_frame_list_indexing(self):
"""Test SBFrameList indexing and length."""
self.build()
self.frame_list_indexing()
def test_frame_list_get_thread(self):
"""Test SBFrameList.GetThread() returns correct thread."""
self.build()
self.frame_list_get_thread()
def setUp(self):
TestBase.setUp(self)
self.main_source = "main.cpp"
def frame_list_api(self):
"""Test SBThread.GetFrames() returns a valid SBFrameList."""
exe = self.getBuildArtifact("a.out")
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
self, "Set break point at this line", lldb.SBFileSpec(self.main_source)
)
self.assertTrue(
thread.IsValid(), "There should be a thread stopped due to breakpoint"
)
# Test GetFrames() returns a valid SBFrameList
frame_list = thread.GetFrames()
self.assertTrue(frame_list.IsValid(), "Frame list should be valid")
self.assertGreater(
frame_list.GetSize(), 0, "Frame list should have at least one frame"
)
# Verify frame list size matches thread frame count
self.assertEqual(
frame_list.GetSize(),
thread.GetNumFrames(),
"Frame list size should match thread frame count",
)
# Verify frames are the same
for i in range(frame_list.GetSize()):
frame_from_list = frame_list.GetFrameAtIndex(i)
frame_from_thread = thread.GetFrameAtIndex(i)
self.assertTrue(
frame_from_list.IsValid(), f"Frame {i} from list should be valid"
)
self.assertEqual(
frame_from_list.GetPC(),
frame_from_thread.GetPC(),
f"Frame {i} PC should match",
)
def frame_list_iterator(self):
"""Test SBFrameList iterator functionality."""
exe = self.getBuildArtifact("a.out")
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
self, "Set break point at this line", lldb.SBFileSpec(self.main_source)
)
self.assertTrue(
thread.IsValid(), "There should be a thread stopped due to breakpoint"
)
frame_list = thread.GetFrames()
# Test iteration
frame_count = 0
for frame in frame_list:
self.assertTrue(frame.IsValid(), "Each frame should be valid")
frame_count += 1
self.assertEqual(
frame_count,
frame_list.GetSize(),
"Iterator should visit all frames",
)
# Test that we can iterate multiple times
second_count = 0
for frame in frame_list:
second_count += 1
self.assertEqual(
frame_count, second_count, "Should be able to iterate multiple times"
)
def frame_list_indexing(self):
"""Test SBFrameList indexing and length."""
exe = self.getBuildArtifact("a.out")
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
self, "Set break point at this line", lldb.SBFileSpec(self.main_source)
)
self.assertTrue(
thread.IsValid(), "There should be a thread stopped due to breakpoint"
)
frame_list = thread.GetFrames()
# Test len()
self.assertEqual(
len(frame_list), frame_list.GetSize(), "len() should return frame count"
)
# Test positive indexing
first_frame = frame_list[0]
self.assertTrue(first_frame.IsValid(), "First frame should be valid")
self.assertEqual(
first_frame.GetPC(),
thread.GetFrameAtIndex(0).GetPC(),
"Indexed frame should match",
)
# Test negative indexing
if len(frame_list) > 0:
last_frame = frame_list[-1]
self.assertTrue(last_frame.IsValid(), "Last frame should be valid")
self.assertEqual(
last_frame.GetPC(),
thread.GetFrameAtIndex(len(frame_list) - 1).GetPC(),
"Negative indexing should work",
)
# Test out of bounds returns None
out_of_bounds = frame_list[10000]
self.assertIsNone(out_of_bounds, "Out of bounds index should return None")
# Test bool conversion
self.assertTrue(bool(frame_list), "Non-empty frame list should be truthy")
# Test Clear()
frame_list.Clear()
# Note: Clear() clears the underlying StackFrameList cache,
# but the frame list object itself should still be valid
self.assertTrue(
frame_list.IsValid(), "Frame list should still be valid after Clear()"
)
def frame_list_get_thread(self):
"""Test SBFrameList.GetThread() returns correct thread."""
exe = self.getBuildArtifact("a.out")
target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
self, "Set break point at this line", lldb.SBFileSpec(self.main_source)
)
self.assertTrue(
thread.IsValid(), "There should be a thread stopped due to breakpoint"
)
frame_list = thread.GetFrames()
self.assertTrue(frame_list.IsValid(), "Frame list should be valid")
# Test GetThread() returns the correct thread
thread_from_list = frame_list.GetThread()
self.assertTrue(
thread_from_list.IsValid(), "Thread from frame list should be valid"
)
self.assertEqual(
thread_from_list.GetThreadID(),
thread.GetThreadID(),
"Frame list should return the correct thread",
)
# Verify it's the same thread object
self.assertEqual(
thread_from_list.GetProcess().GetProcessID(),
thread.GetProcess().GetProcessID(),
"Thread should belong to same process",
)

View File

@ -0,0 +1,22 @@
#include <stdio.h>
int c(int val) {
// Set break point at this line
return val + 3;
}
int b(int val) {
int result = c(val);
return result;
}
int a(int val) {
int result = b(val);
return result;
}
int main() {
int result = a(1);
printf("Result: %d\n", result);
return 0;
}