[Lldb-commits] [lldb] [lldb] Make `print` delegate to synthetic frames. (PR #178602)
Aman LaChapelle via lldb-commits
lldb-commits at lists.llvm.org
Thu Jan 29 11:52:33 PST 2026
https://github.com/bzcheeseman updated https://github.com/llvm/llvm-project/pull/178602
>From 1aed1198ba62322841e183f96f3e02958aefbd9f Mon Sep 17 00:00:00 2001
From: bzcheeseman <aman.lachapelle at gmail.com>
Date: Wed, 28 Jan 2026 16:35:22 -0800
Subject: [PATCH 1/2] [lldb] Add support for ScriptedFrame to provide
values/variables.
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.
stack-info: PR: https://github.com/llvm/llvm-project/pull/178575, branch: users/bzcheeseman/stack/6
---
.../Interfaces/ScriptedFrameInterface.h | 9 ++
.../Process/scripted/ScriptedFrame.cpp | 85 +++++++++++++++++++
.../Plugins/Process/scripted/ScriptedFrame.h | 16 ++++
.../ScriptedFramePythonInterface.cpp | 28 ++++++
.../Interfaces/ScriptedFramePythonInterface.h | 6 ++
.../TestScriptedFrameProvider.py | 53 ++++++++++++
.../scripted_frame_provider/main.cpp | 4 +
.../test_frame_providers.py | 82 ++++++++++++++++++
8 files changed, 283 insertions(+)
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h
index 8ef4b37d6ba12..1f6f55727f406 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<std::string> GetRegisterContext() {
return std::nullopt;
}
+
+ virtual lldb::ValueObjectListSP GetVariables() { return {}; }
+
+ 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..2a4da16d237b0 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,84 @@ lldb::RegisterContextSP ScriptedFrame::GetRegisterContext() {
return m_reg_context_sp;
}
+
+VariableList *ScriptedFrame::GetVariableList(bool get_file_globals,
+ Status *error_ptr) {
+ // Fetch variables from the interface.
+ ValueObjectListSP maybe_variables =
+ m_scripted_frame_interface_sp->GetVariables();
+ if (maybe_variables) {
+ m_variable_list_sp = std::make_shared<VariableList>();
+ ValueObjectListSP value_list_sp = std::move(maybe_variables);
+
+ 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);
+ }
+ }
+ return m_variable_list_sp.get();
+}
+
+lldb::VariableListSP
+ScriptedFrame::GetInScopeVariableList(bool get_file_globals,
+ bool must_have_valid_location) {
+ // Fetch values from the interface.
+ ValueObjectListSP maybe_variables =
+ m_scripted_frame_interface_sp->GetVariables();
+ if (!maybe_variables)
+ return {};
+
+ // Convert what we can into a variable.
+ VariableListSP out = std::make_shared<VariableList>();
+ ValueObjectListSP value_list_sp = std::move(maybe_variables);
+
+ 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;
+
+ out->AddVariable(var);
+ }
+ return out;
+}
+
+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..f5d1993fceed9 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);
}
@@ -82,6 +97,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<std::string> ScriptedFramePythonInterface::GetRegisterContext() {
return obj->GetAsString()->GetValue().str();
}
+lldb::ValueObjectListSP ScriptedFramePythonInterface::GetVariables() {
+ Status error;
+ auto vals = Dispatch<lldb::ValueObjectListSP>("get_variables", error);
+
+ if (error.Fail()) {
+ return ErrorWithMessage<lldb::ValueObjectListSP>(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<lldb::ValueObjectSP>("get_value_for_variable_expression",
+ dispatch_error, expr.data(), options,
+ status);
+
+ if (dispatch_error.Fail()) {
+ return ErrorWithMessage<lldb::ValueObjectSP>(
+ 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<std::string> 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 9c57d52269d244a10f16ce884d40f4da5de24690 Mon Sep 17 00:00:00 2001
From: bzcheeseman <aman.lachapelle at gmail.com>
Date: Wed, 28 Jan 2026 22:20:00 -0800
Subject: [PATCH 2/2] [lldb] Make `print` delegate to synthetic frames.
This patch is more of a proposal in that it's a pretty dramatic change to the way that `print` works. It completely delegates getting values to the frame if the frame is synthetic, and does not redirect at all if the frame fails.
For this patch, the main goal was to allow the synthetic frame to bubble up its own errors in expression evaluation, rather than having errors come back with an extra "could not find identifier <blah>" or worse, simply get swallowed. If there's a better way to handle this, I'm more than happy to change this as long as the core goals of 'delegate variable/value extraction to the synthetic frame', and 'allow the synthetic frame to give back errors that are displayed to the user' can be met.
stack-info: PR: https://github.com/llvm/llvm-project/pull/178602, branch: users/bzcheeseman/stack/7
---
.../Commands/CommandObjectDWIMPrint.cpp | 25 +++++++++++++++++--
1 file changed, 23 insertions(+), 2 deletions(-)
diff --git a/lldb/source/Commands/CommandObjectDWIMPrint.cpp b/lldb/source/Commands/CommandObjectDWIMPrint.cpp
index 40f00c90bbbfb..68bb87221ed03 100644
--- a/lldb/source/Commands/CommandObjectDWIMPrint.cpp
+++ b/lldb/source/Commands/CommandObjectDWIMPrint.cpp
@@ -143,8 +143,7 @@ void CommandObjectDWIMPrint::DoExecute(StringRef command,
maybe_add_hint(output);
result.GetOutputStream() << output;
} else {
- llvm::Error error =
- valobj.Dump(result.GetOutputStream(), dump_options);
+ llvm::Error error = valobj.Dump(result.GetOutputStream(), dump_options);
if (error) {
result.AppendError(toString(std::move(error)));
return;
@@ -155,6 +154,28 @@ void CommandObjectDWIMPrint::DoExecute(StringRef command,
result.SetStatus(eReturnStatusSuccessFinishResult);
};
+ // If the frame is synthetic, then we handle all printing through the
+ // GetValueForVariableExpressionPath. This is so that the synthetic frame has
+ // the ability to take over anything it needs to.
+ if (frame && frame->IsSynthetic()) {
+ VariableSP var_sp;
+ Status status;
+ auto valobj_sp = frame->GetValueForVariableExpressionPath(
+ expr, eval_options.GetUseDynamic(),
+ StackFrame::eExpressionPathOptionsAllowDirectIVarAccess, var_sp,
+ status);
+
+ // Something failed, print the error and return immediately.
+ if (!status.Success()) {
+ result.AppendError(status.AsCString());
+ return;
+ }
+
+ // Otherwise, simply print the object.
+ dump_val_object(*valobj_sp);
+ return;
+ }
+
// First, try `expr` as a _limited_ frame variable expression path: only the
// dot operator (`.`) is permitted for this case.
//
More information about the lldb-commits
mailing list