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 dead11fba19fe..d4cc2f05c39e3 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 @@ -506,8 +505,11 @@ 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; + typedef std::shared_ptr ValueImplSP; ValueImplSP m_opaque_sp; void SetSP(ValueImplSP impl_sp); 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/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/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 539c070ff0f4b..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); @@ -243,7 +250,9 @@ 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, + uint64_t provider_id); protected: /// Override FetchFramesUpTo to lazily return frames from the provider @@ -255,6 +264,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/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/include/lldb/Target/Thread.h b/lldb/include/lldb/Target/Thread.h index 46ce192556756..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,13 +1299,20 @@ 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(); - lldb::SyntheticFrameProviderSP GetFrameProvider() const { - return m_frame_provider_sp; + const llvm::DenseMap & + GetFrameProviders() const { + return m_frame_providers; } protected: @@ -1383,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 @@ -1409,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 Provider, if any. - lldb::SyntheticFrameProviderSP m_frame_provider_sp; + /// 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/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/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/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/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/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/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); 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 896a760f61d26..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; @@ -58,23 +59,37 @@ StackFrameList::~StackFrameList() { SyntheticStackFrameList::SyntheticStackFrameList( Thread &thread, lldb::StackFrameListSP input_frames, - const lldb::StackFrameListSP &prev_frames_sp, bool show_inline_frames) - : StackFrameList(thread, prev_frames_sp, show_inline_frames), - m_input_frames(std::move(input_frames)) {} + const lldb::StackFrameListSP &prev_frames_sp, bool 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)) {} 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) { + // 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; - auto frame_or_err = provider_sp->GetFrameAtIndex(idx); + + // 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(), @@ -88,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); } @@ -374,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); } @@ -409,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); @@ -490,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 { @@ -525,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); } @@ -550,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; @@ -607,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/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/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp index ed65e304fc5f2..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_frame_provider_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(); } @@ -1457,24 +1461,23 @@ 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(); 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). @@ -1483,60 +1486,135 @@ StackFrameListSP Thread::GetStackFrameList() { return priority_a < priority_b; }); - // Load the highest priority provider that successfully instantiates. - for (const auto *descriptor : applicable_descriptors) { + // Load ALL matching providers in priority order. + 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}"); 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(); - m_curr_frames_sp = std::make_shared( - *this, input_frames, m_prev_frames_sp, true); + // Create the frame list based on whether we have providers. + 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. + 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); - // 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); + StackFrameListSP input_frames; + if (m_frame_providers.empty()) { + // 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 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(temp_frames, descriptor); + SyntheticFrameProvider::CreateInstance(input_frames, descriptor); if (!provider_or_err) return provider_or_err.takeError(); - ClearScriptedFrameProvider(); - m_frame_provider_sp = *provider_or_err; + 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_provider_sp.reset(); + 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(); } @@ -1560,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(); - m_frame_provider_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/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; +} diff --git a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py index 0a5b9d9b83951..8c2d8f0d5ad52 100644 --- a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py +++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py @@ -638,3 +638,552 @@ 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(), 0xBAC) + + 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(), 0xBAA) + + 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()) + + 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_source_breakpoint( + self, + "Breakpoint for variable tests", + 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 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()) + 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 + # 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) + + 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}") + + 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 diff --git a/lldb/test/API/functionalities/scripted_frame_provider/main.cpp b/lldb/test/API/functionalities/scripted_frame_provider/main.cpp index 0298e88e4de16..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"; @@ -29,6 +47,14 @@ 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; // 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); } @@ -45,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/functionalities/scripted_frame_provider/test_frame_providers.py b/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py index e4367192af50d..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 @@ -380,3 +380,163 @@ 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, 0xBAA, "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, 0xBAC, "baz") + elif index - 1 < len(self.input_frames): + # 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 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; +}