Skip to content

Conversation

medismailben
Copy link
Member

@medismailben medismailben commented Jul 19, 2025

This patch introduces a new scripting affordance in lldb: ScriptedFrame.

This allows user to produce mock stackframes in scripted threads and
scripted processes from a python script.

With this change, StackFrame can be synthetized from different sources:

  • Either from a dictionary containing a load address, and a frame index,
    which is the legacy way.
  • Or by creating a ScriptedFrame python object.

One particularity of synthezising stackframes from the ScriptedFrame
python object, is that these frame have an optional PC, meaning that
they don't have a report a valid PC and they can act as shells that just
contain static information, like the frame function name, the list of
variables or registers, etc. It can also provide a symbol context.

rdar://157260006

Signed-off-by: Med Ismail Bennani ismail@bennani.ma

if (!thread.IsValid())
return llvm::createStringError("Invalid scripted thread.");

thread.CheckInterpreterAndScriptObject();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the asserts in CheckInterpreterAndScriptObject fail, do you really want to continue? Also, I think you meant Interface in the method name and not Interpreter, since that's what you actually check...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have this pattern in every scripted interface so I think it'd be better to remove it in a follow-up

thread.CheckInterpreterAndScriptObject();

auto scripted_frame_interface =
thread.GetInterface()->CreateScriptedFrameInterface();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if the check failed, this would be unsafe.

ScriptedFrame::~ScriptedFrame() {}

const char *ScriptedFrame::GetFunctionName() {
CheckInterpreterAndScriptObject();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to keep re-calling this? Seems like if either of the interpreter or script object were bad at the start, we should refuse to make the ScriptedFrame at all. Then once successfully created these objects can't go bad, can they? So checking here should be a no-op.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

return ConstString(function_name->c_str()).AsCString();
}

const char *ScriptedFrame::GetDisplayFunctionName() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been switching to returning std::string's and not ConstString backed char *'s whenever possible, the ownership is much clearer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this is just the current return type in the base lldb_private::StackFrame class. This change seems unrelated to this PR however.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that it should be a separate PR, but I think it's worth doing first instead of making things worse by going through ConstString.

std::shared_ptr<DynamicRegisterInfo>>(
LLVM_PRETTY_FUNCTION,
"Failed to get scripted frame registers info: invalid process.",
error, LLDBLog::Thread);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect these logs to show up either with LLDBLog::Thread or LLDBLog::Script.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ScriptedInterface::ErrorWithMessage will both log the message in the logging channel passed to it, append it to the Status object parameter and return the templated defaultly constructed object. That way we're sure to have enough error reporting no matter how the callers does it.

for (size_t idx = 0; idx < arr_size; idx++) {
StackFrameSP synth_frame_sp = nullptr;

auto frame_from_dict_or_err = create_frame_from_dict(idx);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem right. There are two reasons create_frame_from_dict could fail:

  1. Because the frame isn't described by a dictionary but by a script object
  2. Because the dict existed but was incorrect
    In case 1) you do want to try the scripted object instead, but not if there was a dictionary but it failed. Falling back to the script object and obscuring the fact that there was a bogus dictionary will be confusing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a given frame index, you can only have a dictionary or a script object at the time, not both. I'm not sure what failure mode you have in mind, but in this implementation, we:

  1. Try to create the synthetic frame from a dictionary (since it's the legacy option).
  2. If it succeeds, we use that.
  3. Otherwise, we try to create the synthetic frame from a script object (ScriptedFrame)
  4. If it succeeds, we consume the dictionary error and use scripted object synthetic frame.
  5. Otherwise, raise an error and log that we failed to create a synthetic frame.

Let me know if you have a different scenario in mind.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect that if there IS a dictionary provider that returns an actual error handling that index, we should report the error. People would want to know that there was this dictionary provider floating around possibly interfering with the synthetic provider they have just added...
If the dictionary provider says it isn't in the business of providing that frame, that shouldn't be an error since that just means "that index is not my job". I presume it would do that by returning an empty frame but not an error?
If the dictionary provider says "that index wasn't something I wanted to provide a frame for", you should go on and consult the synthetic one.
Also, if we are forced to handle the case where there are both synthetic and dictionary providers at the same time, we should do the synthetic one first. After all, since that's the newer feature, it makes more sense to let it override the older variant than the other way round...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the dictionary provider the dictionary object from the user script or the internal python frame dictionary parser in lldb ?

Either way, how would the dictionary provider let us know that "that index is not my job" ? If it's the former, the frame data are just stored into a python dictionary so there is no way to tell if it supports an index. In the latter, our internal parser wouldn't be able to tell if it should skip a specific index.

I think we should give the freedom to the user of specifying what ever object they want the frame array (either a dictionary containing the frame metadata or the actual scripted frame python object), try both and only fail if we failed to parse either.

Copy link
Collaborator

@jimingham jimingham Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the main thing that was bugging me was if this bit:

    if (!dict->GetValueForKeyAsInteger("pc", pc)) {
      ScriptedInterface::ErrorWithMessage<bool>(
          LLVM_PRETTY_FUNCTION,
          "Couldn't find value for key 'pc' in stackframe dictionary.", error,
           LLDBLog::Thread);
      return error.ToError();
    }

fails, you really don't want to do more work. After all, this ScriptedThread implementation provided you what it thought was a legit dictionary entry for a frame, but it was in fact bogus. In that case, you shouldn't try to do any more work, you should just report the error.

Internally there's just one array that either has the dictionary or the script object, so you won't lose the error because you will also fail to get the Script Object out. since you already know there's a Dictionary entry there.

So the code would be cleaner if you made a distinction between "there's something other than a Dictionary in this slot" and "there's a bad Dictionary in this slot".

And even if we changed the implementation in the future so that the Dictionaries and the Script Objects didn't live in the same array, this would continue to be the right behavior. If the ScriptedThread claims to return a Dictionary representation for that index but it isn't correct, we should report its badness rather than trying the Scripted Frame version.

Copy link

github-actions bot commented Aug 11, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@medismailben medismailben changed the title [lldb] Introduce ScriptedFrame [lldb] Introduce ScriptedFrame affordance Sep 3, 2025
@medismailben medismailben requested a review from labath September 3, 2025 01:12
@medismailben medismailben marked this pull request as ready for review September 3, 2025 01:12
@llvmbot
Copy link
Member

llvmbot commented Sep 4, 2025

@llvm/pr-subscribers-lldb

Author: Med Ismail Bennani (medismailben)

Changes

This patch introduces a new scripting affordance in lldb: ScriptedFrame.

This allows user to produce mock stackframes in scripted threads and
scripted processes from a python script.

With this change, StackFrame can be synthetized from different sources:

  • Either from a dictionary containing a load address, and a frame index,
    which is the legacy way.
  • Or by creating a ScriptedFrame python object.

One particularity of synthezising stackframes from the ScriptedFrame
python object, is that these frame have an optional PC, meaning that
they don't have a report a valid PC and they can act as shells that just
contain static information, like the frame function name, the list of
variables or registers, etc. It can also provide a symbol context.

rdar://157260006

Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>


Patch is 47.64 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/149622.diff

25 Files Affected:

  • (modified) lldb/bindings/python/python-wrapper.swig (+1)
  • (modified) lldb/examples/python/templates/scripted_process.py (+136)
  • (modified) lldb/include/lldb/API/SBSymbolContext.h (+1)
  • (added) lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h (+55)
  • (modified) lldb/include/lldb/Interpreter/Interfaces/ScriptedThreadInterface.h (+10)
  • (modified) lldb/include/lldb/Interpreter/ScriptInterpreter.h (+5)
  • (modified) lldb/include/lldb/Target/StackFrame.h (+17-17)
  • (modified) lldb/include/lldb/lldb-forward.h (+3)
  • (modified) lldb/source/Core/FormatEntity.cpp (+1-1)
  • (modified) lldb/source/Plugins/Process/scripted/CMakeLists.txt (+1)
  • (added) lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp (+191)
  • (added) lldb/source/Plugins/Process/scripted/ScriptedFrame.h (+63)
  • (modified) lldb/source/Plugins/Process/scripted/ScriptedThread.cpp (+71-11)
  • (modified) lldb/source/Plugins/Process/scripted/ScriptedThread.h (+4-1)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt (+1)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h (+1)
  • (added) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.cpp (+157)
  • (added) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFramePythonInterface.h (+59)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp (+2-1)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedThreadPythonInterface.cpp (+17)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedThreadPythonInterface.h (+5)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp (+5)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h (+2)
  • (modified) lldb/source/Symbol/LineEntry.cpp (+1-3)
  • (modified) lldb/test/API/functionalities/scripted_process/dummy_scripted_process.py (+74-1)
diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig
index 2c30d536a753d..c0ad456bc12e8 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -519,6 +519,7 @@ void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBMemoryRegionInfo(PyOb
   return sb_ptr;
 }
 
+
 void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBExecutionContext(PyObject *
                                                                     data) {
   lldb::SBExecutionContext *sb_ptr = NULL;
diff --git a/lldb/examples/python/templates/scripted_process.py b/lldb/examples/python/templates/scripted_process.py
index b6360b8519077..49059d533f38a 100644
--- a/lldb/examples/python/templates/scripted_process.py
+++ b/lldb/examples/python/templates/scripted_process.py
@@ -383,6 +383,142 @@ def get_extended_info(self):
         """
         return self.extended_info
 
+    def get_scripted_frame_plugin(self):
+        """Get scripted frame plugin name.
+
+        Returns:
+            str: Name of the scripted frame plugin.
+        """
+        return None
+
+
+class ScriptedFrame(metaclass=ABCMeta):
+    """
+    The base class for a scripted frame.
+
+    Most of the base class methods are `@abstractmethod` that need to be
+    overwritten by the inheriting class.
+    """
+
+    @abstractmethod
+    def __init__(self, thread, args):
+        """Construct a scripted frame.
+
+        Args:
+            thread (ScriptedThread): The thread owning this frame.
+            args (lldb.SBStructuredData): A Dictionary holding arbitrary
+                key/value pairs used by the scripted frame.
+        """
+        self.target = None
+        self.originating_thread = None
+        self.thread = None
+        self.args = None
+        self.id = None
+        self.name = None
+        self.register_info = None
+        self.register_ctx = {}
+        self.variables = []
+
+        if (
+            isinstance(thread, ScriptedThread)
+            or isinstance(thread, lldb.SBThread)
+            and thread.IsValid()
+        ):
+            self.target = thread.target
+            self.process = thread.process
+            self.originating_thread = thread
+            self.thread = self.process.GetThreadByIndexID(thread.tid)
+            self.get_register_info()
+
+    @abstractmethod
+    def get_id(self):
+        """Get the scripted frame identifier.
+
+        Returns:
+            int: The identifier of the scripted frame in the scripted thread.
+        """
+        pass
+
+    def get_pc(self):
+        """Get the scripted frame address.
+
+        Returns:
+            int: The optional address of the scripted frame in the scripted thread.
+        """
+        return None
+
+    def get_symbol_context(self):
+        """Get the scripted frame symbol context.
+
+        Returns:
+            lldb.SBSymbolContext: The symbol context of the scripted frame in the scripted thread.
+        """
+        return None
+
+    def is_inlined(self):
+        """Check if the scripted frame is inlined.
+
+        Returns:
+            bool: True if scripted frame is inlined. False otherwise.
+        """
+        return False
+
+    def is_artificial(self):
+        """Check if the scripted frame is artificial.
+
+        Returns:
+            bool: True if scripted frame is artificial. False otherwise.
+        """
+        return True
+
+    def is_hidden(self):
+        """Check if the scripted frame is hidden.
+
+        Returns:
+            bool: True if scripted frame is hidden. False otherwise.
+        """
+        return False
+
+    def get_function_name(self):
+        """Get the scripted frame function name.
+
+        Returns:
+            str: The function name of the scripted frame.
+        """
+        return self.name
+
+    def get_display_function_name(self):
+        """Get the scripted frame display function name.
+
+        Returns:
+            str: The display function name of the scripted frame.
+        """
+        return self.get_function_name()
+
+    def get_variables(self, filters):
+        """Get the scripted thread state type.
+
+        Args:
+            filter (lldb.SBVariablesOptions): The filter used to resolve the variables
+        Returns:
+            lldb.SBValueList: The SBValueList containing the SBValue for each resolved variable.
+                              Returns None by default.
+        """
+        return None
+
+    def get_register_info(self):
+        if self.register_info is None:
+            self.register_info = self.originating_thread.get_register_info()
+        return self.register_info
+
+    @abstractmethod
+    def get_register_context(self):
+        """Get the scripted thread register context
+
+        Returns:
+            str: A byte representing all register's value.
+        """
+        pass
 
 class PassthroughScriptedProcess(ScriptedProcess):
     driving_target = None
diff --git a/lldb/include/lldb/API/SBSymbolContext.h b/lldb/include/lldb/API/SBSymbolContext.h
index 128b0b65b7860..19f29c629d094 100644
--- a/lldb/include/lldb/API/SBSymbolContext.h
+++ b/lldb/include/lldb/API/SBSymbolContext.h
@@ -66,6 +66,7 @@ class LLDB_API SBSymbolContext {
   friend class SBTarget;
   friend class SBSymbolContextList;
 
+  friend class lldb_private::ScriptInterpreter;
   friend class lldb_private::python::SWIGBridge;
 
   SBSymbolContext(const lldb_private::SymbolContext &sc_ptr);
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h
new file mode 100644
index 0000000000000..8ef4b37d6ba12
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h
@@ -0,0 +1,55 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_INTERPRETER_INTERFACES_SCRIPTEDFRAMEINTERFACE_H
+#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEINTERFACE_H
+
+#include "ScriptedInterface.h"
+#include "lldb/Core/StructuredDataImpl.h"
+#include "lldb/Symbol/SymbolContext.h"
+#include "lldb/lldb-private.h"
+#include <optional>
+#include <string>
+
+namespace lldb_private {
+class ScriptedFrameInterface : virtual public ScriptedInterface {
+public:
+  virtual llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, ExecutionContext &exe_ctx,
+                     StructuredData::DictionarySP args_sp,
+                     StructuredData::Generic *script_obj = nullptr) = 0;
+
+  virtual lldb::user_id_t GetID() { return LLDB_INVALID_FRAME_ID; }
+
+  virtual lldb::addr_t GetPC() { return LLDB_INVALID_ADDRESS; }
+
+  virtual std::optional<SymbolContext> GetSymbolContext() {
+    return std::nullopt;
+  }
+
+  virtual std::optional<std::string> GetFunctionName() { return std::nullopt; }
+
+  virtual std::optional<std::string> GetDisplayFunctionName() {
+    return std::nullopt;
+  }
+
+  virtual bool IsInlined() { return false; }
+
+  virtual bool IsArtificial() { return false; }
+
+  virtual bool IsHidden() { return false; }
+
+  virtual StructuredData::DictionarySP GetRegisterInfo() { return {}; }
+
+  virtual std::optional<std::string> GetRegisterContext() {
+    return std::nullopt;
+  }
+};
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedThreadInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedThreadInterface.h
index a7cfc690b67dc..bc58f344d36f8 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedThreadInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedThreadInterface.h
@@ -44,6 +44,16 @@ class ScriptedThreadInterface : virtual public ScriptedInterface {
   }
 
   virtual StructuredData::ArraySP GetExtendedInfo() { return {}; }
+
+  virtual std::optional<std::string> GetScriptedFramePluginName() {
+    return std::nullopt;
+  }
+
+protected:
+  friend class ScriptedFrame;
+  virtual lldb::ScriptedFrameInterfaceSP CreateScriptedFrameInterface() {
+    return {};
+  }
 };
 } // namespace lldb_private
 
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index dffb9b82abf3d..024bbc90a9a39 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -26,6 +26,7 @@
 #include "lldb/Host/PseudoTerminal.h"
 #include "lldb/Host/StreamFile.h"
 #include "lldb/Interpreter/Interfaces/OperatingSystemInterface.h"
+#include "lldb/Interpreter/Interfaces/ScriptedFrameInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedPlatformInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedProcessInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
@@ -531,6 +532,10 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  virtual lldb::ScriptedFrameInterfaceSP CreateScriptedFrameInterface() {
+    return {};
+  }
+
   virtual lldb::ScriptedThreadPlanInterfaceSP
   CreateScriptedThreadPlanInterface() {
     return {};
diff --git a/lldb/include/lldb/Target/StackFrame.h b/lldb/include/lldb/Target/StackFrame.h
index 4ffbf97ce6e90..cdbe8ae3c6779 100644
--- a/lldb/include/lldb/Target/StackFrame.h
+++ b/lldb/include/lldb/Target/StackFrame.h
@@ -398,7 +398,7 @@ class StackFrame : public ExecutionContextScope,
   ///
   /// \return
   ///   true if this is an inlined frame.
-  bool IsInlined();
+  virtual bool IsInlined();
 
   /// Query whether this frame is synthetic.
   bool IsSynthetic() const;
@@ -409,12 +409,12 @@ class StackFrame : public ExecutionContextScope,
   /// Query whether this frame is artificial (e.g a synthesized result of
   /// inferring missing tail call frames from a backtrace). Artificial frames
   /// may have limited support for inspecting variables.
-  bool IsArtificial() const;
+  virtual bool IsArtificial() const;
 
   /// Query whether this frame should be hidden from backtraces. Frame
   /// recognizers can customize this behavior and hide distracting
   /// system implementation details this way.
-  bool IsHidden();
+  virtual bool IsHidden();
 
   /// Language plugins can use this API to report language-specific
   /// runtime information about this compile unit, such as additional
@@ -425,13 +425,13 @@ class StackFrame : public ExecutionContextScope,
   ///
   ///  /// \return
   ///   A C-String containing the function demangled name. Can be null.
-  const char *GetFunctionName();
+  virtual const char *GetFunctionName();
 
   /// Get the frame's demangled display name.
   ///
   ///  /// \return
   ///   A C-String containing the function demangled display name. Can be null.
-  const char *GetDisplayFunctionName();
+  virtual const char *GetDisplayFunctionName();
 
   /// Query this frame to find what frame it is in this Thread's
   /// StackFrameList.
@@ -543,18 +543,7 @@ class StackFrame : public ExecutionContextScope,
 
   bool HasCachedData() const;
 
-private:
-  /// Private methods, called from GetValueForVariableExpressionPath.
-  /// See that method for documentation of parameters and return value.
-  lldb::ValueObjectSP LegacyGetValueForVariableExpressionPath(
-      llvm::StringRef var_expr, lldb::DynamicValueType use_dynamic,
-      uint32_t options, lldb::VariableSP &var_sp, Status &error);
-
-  lldb::ValueObjectSP DILGetValueForVariableExpressionPath(
-      llvm::StringRef var_expr, lldb::DynamicValueType use_dynamic,
-      uint32_t options, lldb::VariableSP &var_sp, Status &error);
-
-  /// For StackFrame only.
+  /// For StackFrame and derived classes only.
   /// \{
   lldb::ThreadWP m_thread_wp;
   uint32_t m_frame_index;
@@ -591,6 +580,17 @@ class StackFrame : public ExecutionContextScope,
   StreamString m_disassembly;
   std::recursive_mutex m_mutex;
 
+private:
+  /// Private methods, called from GetValueForVariableExpressionPath.
+  /// See that method for documentation of parameters and return value.
+  lldb::ValueObjectSP LegacyGetValueForVariableExpressionPath(
+      llvm::StringRef var_expr, lldb::DynamicValueType use_dynamic,
+      uint32_t options, lldb::VariableSP &var_sp, Status &error);
+
+  lldb::ValueObjectSP DILGetValueForVariableExpressionPath(
+      llvm::StringRef var_expr, lldb::DynamicValueType use_dynamic,
+      uint32_t options, lldb::VariableSP &var_sp, Status &error);
+
   StackFrame(const StackFrame &) = delete;
   const StackFrame &operator=(const StackFrame &) = delete;
 };
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index 483dce98ea427..af5656b3dcad1 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -187,6 +187,7 @@ class SaveCoreOptions;
 class Scalar;
 class ScriptInterpreter;
 class ScriptInterpreterLocker;
+class ScriptedFrameInterface;
 class ScriptedMetadata;
 class ScriptedBreakpointInterface;
 class ScriptedPlatformInterface;
@@ -408,6 +409,8 @@ typedef std::shared_ptr<lldb_private::RecognizedStackFrame>
 typedef std::shared_ptr<lldb_private::ScriptSummaryFormat>
     ScriptSummaryFormatSP;
 typedef std::shared_ptr<lldb_private::ScriptInterpreter> ScriptInterpreterSP;
+typedef std::shared_ptr<lldb_private::ScriptedFrameInterface>
+    ScriptedFrameInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedMetadata> ScriptedMetadataSP;
 typedef std::unique_ptr<lldb_private::ScriptedPlatformInterface>
     ScriptedPlatformInterfaceUP;
diff --git a/lldb/source/Core/FormatEntity.cpp b/lldb/source/Core/FormatEntity.cpp
index 2ff73979e4976..491f5c6320d97 100644
--- a/lldb/source/Core/FormatEntity.cpp
+++ b/lldb/source/Core/FormatEntity.cpp
@@ -1681,7 +1681,7 @@ bool FormatEntity::Format(const Entry &entry, Stream &s,
       StackFrame *frame = exe_ctx->GetFramePtr();
       if (frame) {
         const Address &pc_addr = frame->GetFrameCodeAddress();
-        if (pc_addr.IsValid()) {
+        if (pc_addr.IsValid() || frame->IsSynthetic()) {
           if (DumpAddressAndContent(s, sc, exe_ctx, pc_addr, false))
             return true;
         }
diff --git a/lldb/source/Plugins/Process/scripted/CMakeLists.txt b/lldb/source/Plugins/Process/scripted/CMakeLists.txt
index 590166591a41e..1516ad3132e3b 100644
--- a/lldb/source/Plugins/Process/scripted/CMakeLists.txt
+++ b/lldb/source/Plugins/Process/scripted/CMakeLists.txt
@@ -1,6 +1,7 @@
 add_lldb_library(lldbPluginScriptedProcess PLUGIN
   ScriptedProcess.cpp
   ScriptedThread.cpp
+  ScriptedFrame.cpp
 
   LINK_COMPONENTS
     BinaryFormat
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
new file mode 100644
index 0000000000000..6519df9185df0
--- /dev/null
+++ b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
@@ -0,0 +1,191 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "ScriptedFrame.h"
+
+#include "lldb/Utility/DataBufferHeap.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+void ScriptedFrame::CheckInterpreterAndScriptObject() const {
+  lldbassert(m_script_object_sp && "Invalid Script Object.");
+  lldbassert(GetInterface() && "Invalid Scripted Frame Interface.");
+}
+
+llvm::Expected<std::shared_ptr<ScriptedFrame>>
+ScriptedFrame::Create(ScriptedThread &thread,
+                      StructuredData::DictionarySP args_sp,
+                      StructuredData::Generic *script_object) {
+  if (!thread.IsValid())
+    return llvm::createStringError("Invalid scripted thread.");
+
+  thread.CheckInterpreterAndScriptObject();
+
+  auto scripted_frame_interface =
+      thread.GetInterface()->CreateScriptedFrameInterface();
+  if (!scripted_frame_interface)
+    return llvm::createStringError("failed to create scripted frame interface");
+
+  llvm::StringRef frame_class_name;
+  if (!script_object) {
+    std::optional<std::string> class_name =
+        thread.GetInterface()->GetScriptedFramePluginName();
+    if (!class_name || class_name->empty())
+      return llvm::createStringError(
+          "failed to get scripted thread class name");
+    frame_class_name = *class_name;
+  }
+
+  ExecutionContext exe_ctx(thread);
+  auto obj_or_err = scripted_frame_interface->CreatePluginObject(
+      frame_class_name, exe_ctx, args_sp, script_object);
+
+  if (!obj_or_err)
+    return llvm::createStringError(
+        "failed to create script object: %s",
+        llvm::toString(obj_or_err.takeError()).c_str());
+
+  StructuredData::GenericSP owned_script_object_sp = *obj_or_err;
+
+  if (!owned_script_object_sp->IsValid())
+    return llvm::createStringError("created script object is invalid");
+
+  lldb::user_id_t frame_id = scripted_frame_interface->GetID();
+
+  lldb::addr_t pc = scripted_frame_interface->GetPC();
+  SymbolContext sc;
+  Address symbol_addr;
+  if (pc != LLDB_INVALID_ADDRESS) {
+    symbol_addr.SetLoadAddress(pc, &thread.GetProcess()->GetTarget());
+    symbol_addr.CalculateSymbolContext(&sc);
+  }
+
+  std::optional<SymbolContext> maybe_sym_ctx =
+      scripted_frame_interface->GetSymbolContext();
+  if (maybe_sym_ctx) {
+    sc = *maybe_sym_ctx;
+  }
+
+  StructuredData::DictionarySP reg_info =
+      scripted_frame_interface->GetRegisterInfo();
+
+  if (!reg_info)
+    return llvm::createStringError(
+        "failed to get scripted thread registers info");
+
+  std::shared_ptr<DynamicRegisterInfo> register_info_sp =
+      DynamicRegisterInfo::Create(
+          *reg_info, thread.GetProcess()->GetTarget().GetArchitecture());
+
+  lldb::RegisterContextSP reg_ctx_sp;
+
+  std::optional<std::string> reg_data =
+      scripted_frame_interface->GetRegisterContext();
+  if (reg_data) {
+    DataBufferSP data_sp(
+        std::make_shared<DataBufferHeap>(reg_data->c_str(), reg_data->size()));
+
+    if (!data_sp->GetByteSize())
+      return llvm::createStringError("failed to copy raw registers data");
+
+    std::shared_ptr<RegisterContextMemory> reg_ctx_memory =
+        std::make_shared<RegisterContextMemory>(
+            thread, frame_id, *register_info_sp, LLDB_INVALID_ADDRESS);
+    if (!reg_ctx_memory)
+      return llvm::createStringError("failed to create a register context.");
+
+    reg_ctx_memory->SetAllRegisterData(data_sp);
+    reg_ctx_sp = reg_ctx_memory;
+  }
+
+  return std::make_shared<ScriptedFrame>(
+      thread, scripted_frame_interface, frame_id, pc, sc, reg_ctx_sp,
+      register_info_sp, owned_script_object_sp);
+}
+
+ScriptedFrame::ScriptedFrame(ScriptedThread &thread,
+                             ScriptedFrameInterfaceSP interface_sp,
+                             lldb::user_id_t id, lldb::addr_t pc,
+                             SymbolContext &sym_ctx,
+                             lldb::RegisterContextSP reg_ctx_sp,
+                             std::shared_ptr<DynamicRegisterInfo> reg_info_sp,
+                             StructuredData::GenericSP script_object_sp)
+    : StackFrame(thread.shared_from_this(), /*frame_idx=*/id,
+                 /*concrete_frame_idx=*/id, /*reg_context_sp=*/reg_ctx_sp,
+                 /*cfa=*/0, /*pc=*/pc,
+                 /*behaves_like_zeroth_frame=*/!id, /*symbol_ctx=*/&sym_ctx),
+      m_scripted_frame_interface_sp(interface_sp),
+      m_script_object_sp(script_object_sp), m_register_info_sp(reg_info_sp) {}
+
+ScriptedFrame::~ScriptedFrame() {}
+
+const char *ScriptedFrame::GetFunctionName() {
+  CheckInterpreterAndScriptObject();
+  std::optional<std::string> function_name = GetInterface()->GetFunctionName();
+  if (!function_name)
+    return nullptr;
+  return ConstString(function_name->c_str()).AsCString();
+}
+
+const char *ScriptedFrame::GetDisplayFunctionName() {
+  CheckInterpreterAndScriptObject();
+  std::optional<std::string> function_name =
+      GetInterface()->GetDisplayFunctionName();
+  if...
[truncated]

Copy link

github-actions bot commented Sep 4, 2025

✅ With the latest revision this PR passed the Python code formatter.

@medismailben medismailben force-pushed the scripted-frame branch 2 times, most recently from df4c41d to 440e6ce Compare September 4, 2025 17:52
Copy link
Member

@JDevlieghere JDevlieghere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

This patch introduces a new scripting affordance in lldb:  `ScriptedFrame`.

This allows user to produce mock stackframes in scripted threads and
scripted processes from a python script.

With this change, StackFrame can be synthetized from different sources:
- Either from a dictionary containing a load address, and a frame index,
  which is the legacy way.
- Or by creating a ScriptedFrame python object.

One particularity of synthezising stackframes from the ScriptedFrame
python object, is that these frame have an optional PC, meaning that
they don't have a report a valid PC and they can act as shells that just
contain static information, like the frame function name, the list of
variables or registers, etc. It can also provide a symbol context.

rdar://157260006

Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
@medismailben medismailben merged commit 84b5620 into llvm:main Sep 4, 2025
9 checks passed
medismailben added a commit to medismailben/llvm-project that referenced this pull request Sep 4, 2025
This patch introduces a new scripting affordance in lldb:
`ScriptedFrame`.

This allows user to produce mock stackframes in scripted threads and
scripted processes from a python script.

With this change, StackFrame can be synthetized from different sources:
- Either from a dictionary containing a load address, and a frame index,
  which is the legacy way.
- Or by creating a ScriptedFrame python object.

One particularity of synthezising stackframes from the ScriptedFrame
python object, is that these frame have an optional PC, meaning that
they don't have a report a valid PC and they can act as shells that just
contain static information, like the frame function name, the list of
variables or registers, etc. It can also provide a symbol context.

rdar://157260006

(cherry picked from commit 84b5620)
Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
medismailben added a commit to medismailben/llvm-project that referenced this pull request Sep 5, 2025
This patch introduces a new scripting affordance in lldb:
`ScriptedFrame`.

This allows user to produce mock stackframes in scripted threads and
scripted processes from a python script.

With this change, StackFrame can be synthetized from different sources:
- Either from a dictionary containing a load address, and a frame index,
  which is the legacy way.
- Or by creating a ScriptedFrame python object.

One particularity of synthezising stackframes from the ScriptedFrame
python object, is that these frame have an optional PC, meaning that
they don't have a report a valid PC and they can act as shells that just
contain static information, like the frame function name, the list of
variables or registers, etc. It can also provide a symbol context.

rdar://157260006

(cherry picked from commit 84b5620)
Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
medismailben added a commit to medismailben/llvm-project that referenced this pull request Sep 5, 2025
This patch introduces a new scripting affordance in lldb:
`ScriptedFrame`.

This allows user to produce mock stackframes in scripted threads and
scripted processes from a python script.

With this change, StackFrame can be synthetized from different sources:
- Either from a dictionary containing a load address, and a frame index,
  which is the legacy way.
- Or by creating a ScriptedFrame python object.

One particularity of synthezising stackframes from the ScriptedFrame
python object, is that these frame have an optional PC, meaning that
they don't have a report a valid PC and they can act as shells that just
contain static information, like the frame function name, the list of
variables or registers, etc. It can also provide a symbol context.

rdar://157260006

(cherry picked from commit 84b5620)
Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
medismailben added a commit to medismailben/llvm-project that referenced this pull request Sep 5, 2025
This patch introduces a new scripting affordance in lldb:
`ScriptedFrame`.

This allows user to produce mock stackframes in scripted threads and
scripted processes from a python script.

With this change, StackFrame can be synthetized from different sources:
- Either from a dictionary containing a load address, and a frame index,
  which is the legacy way.
- Or by creating a ScriptedFrame python object.

One particularity of synthezising stackframes from the ScriptedFrame
python object, is that these frame have an optional PC, meaning that
they don't have a report a valid PC and they can act as shells that just
contain static information, like the frame function name, the list of
variables or registers, etc. It can also provide a symbol context.

rdar://157260006

(cherry picked from commit 84b5620)
Signed-off-by: Med Ismail Bennani <ismail@bennani.ma>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants