[Lldb-commits] [lldb] [lldb/Interpreter] Surface Python exceptions from scripted extensions (PR #198153)
Med Ismail Bennani via lldb-commits
lldb-commits at lists.llvm.org
Fri May 22 12:21:06 PDT 2026
https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/198153
>From 26c7c17411eba8a7095a36d0e90806140a6ac54b Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Thu, 21 May 2026 08:37:43 -0700
Subject: [PATCH] [lldb/Interpreter] Surface Python exceptions from scripted
extensions
Python exceptions in scripted affordance methods were silently consumed
by llvm::consumeError, leaving users with a generic "Failed to create
script object." or just a null result.
Dispatch and CallStaticMethod now extract the Python backtrace via
PythonException::ReadBacktrace and populate the Status out-parameter
with "Python exception in <script-class> method '<method>':\n<traceback>"
(class name pulled from ScriptedInterface's metadata accessor, or
"<unknown>" if unset). ErrorWithMessage no longer overwrites a Status
that already carries a detailed message.
For entry points with no error-return channel - ScriptedProcess and
OperatingSystemPython CreateInstance/ctor, ScriptedThreadPlan::DidPush,
BreakpointResolverScripted ctor - the detailed error is broadcast via
Debugger::ReportError. ScriptedThread::Create propagates it through its
existing llvm::Expected return via llvm::toString instead of a generic
message.
ScriptedProcess::CreateInstance returns nullptr quietly when the launch
info doesn't request a scripted process, so the diagnostic only fires
on real failures. ScriptedProcess::DoAttach / DoReadMemory /
DoWriteMemory wrap their affordance Status with the failing operation.
ScriptedInterface gains an optional ErrorCallback for command handlers
that want to forward errors directly to a CommandReturnObject; if unset,
Status / ReportError remain the surfacing path.
ScriptedBreakpointInterface, ScriptedFrameProviderInterface and
ScriptedStopHookInterface now inherit virtually from ScriptedInterface
to disambiguate the metadata subobject in their *PythonInterface
subclasses.
A new test under lldb/test/API/functionalities/scripted_extensions/
listens for eBroadcastBitError and asserts each malformed extension
produces a user-visible error.
Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
---
.../Interfaces/ScriptedBreakpointInterface.h | 2 +-
.../ScriptedFrameProviderInterface.h | 2 +-
.../Interfaces/ScriptedInterface.h | 36 +++-
.../Interfaces/ScriptedStopHookInterface.h | 2 +-
.../lldb/Interpreter/ScriptInterpreter.h | 4 +
.../Breakpoint/BreakpointResolverScripted.cpp | 8 +-
.../Python/OperatingSystemPython.cpp | 8 +-
.../Process/scripted/ScriptedProcess.cpp | 60 +++++-
.../Process/scripted/ScriptedProcess.h | 14 +-
.../Process/scripted/ScriptedThread.cpp | 7 +-
.../Interfaces/ScriptedPythonInterface.h | 132 +++++++++++-
lldb/source/Target/ScriptedThreadPlan.cpp | 5 +
.../scripted_extensions/Makefile | 3 +
.../TestScriptedExtensionsDiagnostics.py | 124 +++++++++++
.../scripted_extensions/main.c | 1 +
.../malformed_scripted_extensions.py | 195 ++++++++++++++++++
16 files changed, 567 insertions(+), 36 deletions(-)
create mode 100644 lldb/test/API/functionalities/scripted_extensions/Makefile
create mode 100644 lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py
create mode 100644 lldb/test/API/functionalities/scripted_extensions/main.c
create mode 100644 lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h
index 982f231d9f0b2..7328aa26db65d 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h
@@ -15,7 +15,7 @@
#include "lldb/lldb-private.h"
namespace lldb_private {
-class ScriptedBreakpointInterface : public ScriptedInterface {
+class ScriptedBreakpointInterface : virtual public ScriptedInterface {
public:
virtual llvm::Expected<StructuredData::GenericSP>
CreatePluginObject(const ScriptedMetadata &scripted_metadata,
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
index 757a99cb2387b..8c94df430c103 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
@@ -14,7 +14,7 @@
#include "ScriptedInterface.h"
namespace lldb_private {
-class ScriptedFrameProviderInterface : public ScriptedInterface {
+class ScriptedFrameProviderInterface : virtual public ScriptedInterface {
public:
virtual bool AppliesToThread(llvm::StringRef class_name,
lldb::ThreadSP thread_sp) {
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
index 3dbc009a58311..e3d84ed6c3582 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
@@ -20,6 +20,7 @@
#include "llvm/Support/Compiler.h"
+#include <functional>
#include <optional>
#include <string>
@@ -37,6 +38,18 @@ class ScriptedInterface {
return m_scripted_metadata;
}
+ /// Set error callback to surface Python exceptions directly to users.
+ ///
+ /// This allows command handlers to receive Python exception details
+ /// immediately rather than relying on diagnostic broadcasts.
+ ///
+ /// \param callback Function to call with Status containing exception details.
+ virtual void SetErrorCallback(std::function<void(const Status &)> callback) {}
+
+ /// Clear the error callback.
+ virtual void ClearErrorCallback() {}
+
+
struct AbstractMethodRequirement {
llvm::StringLiteral name;
size_t min_arg_count = 0;
@@ -62,18 +75,20 @@ class ScriptedInterface {
static Ret ErrorWithMessage(llvm::StringRef caller_name,
llvm::StringRef error_msg, Status &error,
LLDBLog log_category = LLDBLog::Process) {
+ // Log the error for debugging (includes function signature for context).
LLDB_LOGF(GetLog(log_category), "%s ERROR = %s", caller_name.data(),
error_msg.data());
- std::string full_error_message =
- llvm::Twine(caller_name + llvm::Twine(" ERROR = ") +
- llvm::Twine(error_msg))
- .str();
- if (const char *detailed_error = error.AsCString())
- full_error_message +=
- llvm::Twine(llvm::Twine(" (") + llvm::Twine(detailed_error) +
- llvm::Twine(")"))
- .str();
- error = Status(std::move(full_error_message));
+
+ // For user-facing messages, just pass through the Status if it already
+ // has detailed information (like Python tracebacks); otherwise set it.
+ llvm::StringRef existing_error = error.AsCString();
+ if (!error.Fail() || existing_error.empty()) {
+ // Status is empty, populate it with the simple error message.
+ error = Status::FromErrorString(error_msg.data());
+ }
+ // If Status already has content, leave it as-is (it has the Python
+ // traceback).
+
return {};
}
@@ -105,4 +120,5 @@ class ScriptedInterface {
std::optional<ScriptedMetadata> m_scripted_metadata;
};
} // namespace lldb_private
+
#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h
index c68498cba1632..ba325cfdf38b2 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h
@@ -14,7 +14,7 @@
#include "ScriptedInterface.h"
namespace lldb_private {
-class ScriptedStopHookInterface : public ScriptedInterface {
+class ScriptedStopHookInterface : virtual public ScriptedInterface {
public:
virtual llvm::Expected<StructuredData::GenericSP>
CreatePluginObject(const ScriptedMetadata &scripted_metadata,
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 58af82fb48390..2cc3890d86916 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -663,6 +663,10 @@ class ScriptInterpreter : public PluginInterface {
lldb::TargetSP GetOpaqueTypeFromSBTarget(const lldb::SBTarget &target) const;
+ /// Get the debugger associated with this script interpreter.
+ Debugger &GetDebugger() { return m_debugger; }
+ const Debugger &GetDebugger() const { return m_debugger; }
+
protected:
Debugger &m_debugger;
lldb::ScriptLanguage m_script_lang;
diff --git a/lldb/source/Breakpoint/BreakpointResolverScripted.cpp b/lldb/source/Breakpoint/BreakpointResolverScripted.cpp
index 0719c8b634cd3..0fdb185537c9a 100644
--- a/lldb/source/Breakpoint/BreakpointResolverScripted.cpp
+++ b/lldb/source/Breakpoint/BreakpointResolverScripted.cpp
@@ -20,6 +20,7 @@
#include "lldb/Target/Target.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/StreamString.h"
+#include "llvm/Support/FormatVariadic.h"
using namespace lldb;
using namespace lldb_private;
@@ -94,7 +95,12 @@ void BreakpointResolverScripted::CreateImplementationIfNeeded(
m_interface_sp->CreatePluginObject(scripted_metadata, breakpoint_sp);
if (!obj_or_err) {
m_interface_sp.reset();
- m_error = Status::FromError(obj_or_err.takeError());
+ std::string msg = llvm::toString(obj_or_err.takeError());
+ Debugger::ReportError(
+ llvm::formatv("Failed to create BreakpointResolverScripted: {0}", msg)
+ .str(),
+ target.GetDebugger().GetID());
+ m_error = Status(msg);
return;
}
StructuredData::ObjectSP object_sp = *obj_or_err;
diff --git a/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp b/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
index 8553d18bbead7..3a86c219aeee4 100644
--- a/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
+++ b/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
@@ -33,6 +33,7 @@
#include "lldb/Utility/StreamString.h"
#include "lldb/Utility/StructuredData.h"
#include "lldb/ValueObject/ValueObjectVariable.h"
+#include "llvm/Support/FormatVariadic.h"
#include <memory>
@@ -121,7 +122,12 @@ OperatingSystemPython::OperatingSystemPython(lldb_private::Process *process,
scripted_metadata, exe_ctx, nullptr);
if (!obj_or_err) {
- llvm::consumeError(obj_or_err.takeError());
+ std::string msg = llvm::toString(obj_or_err.takeError());
+ if (process)
+ Debugger::ReportError(
+ llvm::formatv("Failed to create OperatingSystemPython: {0}", msg)
+ .str(),
+ process->GetTarget().GetDebugger().GetID());
return;
}
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
index 1531d492562cd..d5c6f082bd700 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
@@ -27,6 +27,10 @@
#include "Plugins/ObjectFile/Placeholder/ObjectFilePlaceholder.h"
+#include "llvm/Support/Error.h"
+
+#include <string>
+
using namespace lldb;
using namespace lldb_private;
@@ -57,11 +61,27 @@ lldb::ProcessSP ScriptedProcess::CreateInstance(lldb::TargetSP target_sp,
ScriptedMetadata scripted_metadata(target_sp->GetProcessLaunchInfo());
+ // CreateInstance is invoked for every process plugin during process
+ // creation; if the launch info doesn't request a scripted process, bail
+ // out silently rather than treating the missing class name as an error.
+ if (!scripted_metadata)
+ return nullptr;
+
Status error;
auto process_sp = std::shared_ptr<ScriptedProcess>(
new ScriptedProcess(target_sp, listener_sp, scripted_metadata, error));
if (error.Fail() || !process_sp || !process_sp->m_interface_up) {
+ // CreateInstance returns nullptr on failure with no Status output
+ // parameter, so we must report the error via the diagnostic system for
+ // users to see it.
+ if (error.Fail()) {
+ Debugger::ReportError(
+ llvm::formatv("Failed to create ScriptedProcess: {0}",
+ error.AsCString())
+ .str(),
+ target_sp->GetDebugger().GetID());
+ }
LLDB_LOGF(GetLog(LLDBLog::Process), "%s", error.AsCString());
return nullptr;
}
@@ -112,8 +132,10 @@ ScriptedProcess::ScriptedProcess(lldb::TargetSP target_sp,
GetInterface().CreatePluginObject(m_scripted_metadata, exe_ctx);
if (!obj_or_err) {
- llvm::consumeError(obj_or_err.takeError());
- error = Status::FromErrorString("Failed to create script object.");
+ // Extract the detailed error message including the Python backtrace.
+ std::string error_msg = llvm::toString(obj_or_err.takeError());
+ error = Status::FromErrorStringWithFormatv(
+ "Failed to create script object: {0}", error_msg);
return;
}
@@ -189,10 +211,13 @@ Status ScriptedProcess::DoResume(RunDirection direction) {
Status ScriptedProcess::DoAttach(const ProcessAttachInfo &attach_info) {
Status error = GetInterface().Attach(attach_info);
+ if (error.Fail()) {
+ error = Status::FromErrorStringWithFormatv(
+ "Failed to attach to scripted process: {0}", error.AsCString());
+ return error;
+ }
SetPrivateState(eStateRunning);
SetPrivateState(eStateStopped);
- if (error.Fail())
- return error;
// NOTE: We need to set the PID before finishing to attach otherwise we will
// hit an assert when calling the attach completion handler.
DidLaunch();
@@ -224,8 +249,14 @@ size_t ScriptedProcess::DoReadMemory(lldb::addr_t addr, void *buf, size_t size,
lldb::DataExtractorSP data_extractor_sp =
GetInterface().ReadMemoryAtAddress(addr, size, error);
- if (!data_extractor_sp || !data_extractor_sp->HasData() || error.Fail())
+ if (!data_extractor_sp || !data_extractor_sp->HasData() || error.Fail()) {
+ if (error.Fail()) {
+ error = Status::FromErrorStringWithFormatv(
+ "Failed to read memory from scripted process at 0x{0:x}: {1}", addr,
+ error.AsCString());
+ }
return 0;
+ }
offset_t bytes_copied = data_extractor_sp->CopyByteOrderedData(
0, data_extractor_sp->GetByteSize(), buf, size, GetByteOrder());
@@ -251,9 +282,15 @@ size_t ScriptedProcess::DoWriteMemory(lldb::addr_t vm_addr, const void *buf,
lldb::offset_t bytes_written =
GetInterface().WriteMemoryAtAddress(vm_addr, data_extractor_sp, error);
- if (!bytes_written || bytes_written == LLDB_INVALID_OFFSET)
+ if (!bytes_written || bytes_written == LLDB_INVALID_OFFSET) {
+ if (error.Fail()) {
+ error = Status::FromErrorStringWithFormatv(
+ "Failed to write memory to scripted process at 0x{0:x}: {1}", vm_addr,
+ error.AsCString());
+ }
return ScriptedInterface::ErrorWithMessage<size_t>(
LLVM_PRETTY_FUNCTION, "Failed to copy write buffer to memory.", error);
+ }
// FIXME: We should use the diagnostic system to report a warning if the
// `bytes_written` is different from `size`.
@@ -565,3 +602,14 @@ void *ScriptedProcess::GetImplementation() {
return object_instance_sp->GetAsGeneric()->GetValue();
return nullptr;
}
+
+void ScriptedProcess::SetScriptedInterfaceErrorCallback(
+ std::function<void(const Status &)> callback) {
+ if (m_interface_up)
+ m_interface_up->SetErrorCallback(std::move(callback));
+}
+
+void ScriptedProcess::ClearScriptedInterfaceErrorCallback() {
+ if (m_interface_up)
+ m_interface_up->ClearErrorCallback();
+}
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.h b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h
index 8371180734217..9ce4a3f8d3cc9 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedProcess.h
+++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h
@@ -17,8 +17,6 @@
#include "ScriptedThread.h"
-#include <mutex>
-
namespace lldb_private {
class ScriptedProcess : public Process {
public:
@@ -93,6 +91,18 @@ class ScriptedProcess : public Process {
void *GetImplementation() override;
+ /// Set error callback to surface Python exceptions directly to users.
+ ///
+ /// This allows command handlers to receive Python exception details
+ /// immediately rather than relying on diagnostic broadcasts.
+ ///
+ /// \param callback Function to call with Status containing exception details.
+ void SetScriptedInterfaceErrorCallback(
+ std::function<void(const Status &)> callback);
+
+ /// Clear the error callback.
+ void ClearScriptedInterfaceErrorCallback();
+
void ForceScriptedState(lldb::StateType state) override {
// If we're about to stop, we should fetch the loaded dynamic libraries
// dictionary before emitting the private stop event to avoid having the
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
index f87b37ca67e09..ac5dd00483f3b 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
@@ -69,11 +69,8 @@ ScriptedThread::Create(ScriptedProcess &process,
auto obj_or_err = scripted_thread_interface->CreatePluginObject(
thread_metadata, exe_ctx, script_object);
- if (!obj_or_err) {
- llvm::consumeError(obj_or_err.takeError());
- return llvm::createStringError(llvm::inconvertibleErrorCode(),
- "Failed to create script object.");
- }
+ if (!obj_or_err)
+ return obj_or_err.takeError();
StructuredData::GenericSP owned_script_object_sp = *obj_or_err;
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index 28ed5745ca55f..b11e44005a50a 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -9,12 +9,14 @@
#ifndef LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDPYTHONINTERFACE_H
#define LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDPYTHONINTERFACE_H
+#include <functional>
#include <optional>
#include <sstream>
#include <tuple>
#include <type_traits>
#include <utility>
+#include "lldb/Core/Debugger.h"
#include "lldb/Interpreter/Interfaces/ScriptedInterface.h"
#include "lldb/Utility/DataBufferHeap.h"
@@ -29,6 +31,22 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
ScriptedPythonInterface(ScriptInterpreterPythonImpl &interpreter);
~ScriptedPythonInterface() override = default;
+ /// Set callback to surface Python exceptions to CommandReturnObject.
+ ///
+ /// When set, this callback will be invoked whenever a Python exception occurs
+ /// in scripting affordance methods, allowing errors to be surfaced directly
+ /// to the user via CommandReturnObject::AppendError().
+ ///
+ /// If no callback is registered, errors will be reported via
+ /// Debugger::ReportError() instead.
+ ///
+ /// \param callback Function to call with Status containing exception details.
+ using ErrorCallback = std::function<void(const Status &)>;
+ void SetErrorCallback(ErrorCallback callback) override {
+ m_error_callback = std::move(callback);
+ }
+ void ClearErrorCallback() override { m_error_callback = nullptr; }
+
enum class AbstractMethodCheckerCases {
eNotImplemented,
eNotAllocated,
@@ -130,7 +148,15 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
llvm::Expected<PythonObject> callable_or_err =
class_dict.GetItem(method_name);
if (!callable_or_err) {
- llvm::consumeError(callable_or_err.takeError());
+ Log *log = GetLog(LLDBLog::Script);
+ if (log) {
+ std::string error_msg =
+ ExtractPythonError(callable_or_err.takeError());
+ LLDB_LOGF(log, "Failed to get method '%s': %s", method_name.data(),
+ error_msg.c_str());
+ } else {
+ llvm::consumeError(callable_or_err.takeError());
+ }
SET_CASE_AND_CONTINUE(method_name,
AbstractMethodCheckerCases::eNotAllocated)
}
@@ -145,7 +171,15 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
auto arg_info_or_err = callable.GetArgInfo();
if (!arg_info_or_err) {
- llvm::consumeError(arg_info_or_err.takeError());
+ Log *log = GetLog(LLDBLog::Script);
+ if (log) {
+ std::string error_msg =
+ ExtractPythonError(arg_info_or_err.takeError());
+ LLDB_LOGF(log, "Failed to get arg info for method '%s': %s",
+ method_name.data(), error_msg.c_str());
+ } else {
+ llvm::consumeError(arg_info_or_err.takeError());
+ }
SET_CASE_AND_CONTINUE(method_name,
AbstractMethodCheckerCases::eUnknownArgumentCount)
}
@@ -258,14 +292,18 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
std::apply(
[&init, &expected_return_object](auto &&...args) {
- llvm::consumeError(expected_return_object.takeError());
+ // Consume placeholder error (expected initial state).
+ if (!expected_return_object)
+ llvm::consumeError(expected_return_object.takeError());
expected_return_object = init(args...);
},
std::tuple_cat(transformed_args, std::make_tuple(dict)));
} else {
std::apply(
[&init, &expected_return_object](auto &&...args) {
- llvm::consumeError(expected_return_object.takeError());
+ // Consume placeholder error (expected initial state).
+ if (!expected_return_object)
+ llvm::consumeError(expected_return_object.takeError());
expected_return_object = init(args...);
},
transformed_args);
@@ -467,13 +505,41 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
llvm::createStringError("not initialized");
std::apply(
[&method, &expected_return_object](auto &&...args) {
- llvm::consumeError(expected_return_object.takeError());
+ // Consume placeholder error (expected initial state).
+ if (!expected_return_object)
+ llvm::consumeError(expected_return_object.takeError());
expected_return_object = method(args...);
},
transformed_args);
if (llvm::Error e = expected_return_object.takeError()) {
- error = Status::FromError(std::move(e));
+ // Extract Python backtrace and log it.
+ std::string detailed_error = ExtractPythonError(std::move(e));
+
+ Log *log = GetLog(LLDBLog::Script);
+ if (log) {
+ LLDB_LOGF(log, "%s: Python exception in static method %s:\n%s",
+ caller_signature.c_str(), method_name.data(),
+ detailed_error.c_str());
+ }
+
+ // Create Status with full context including the interface type.
+ // TODO: Stringify `args` and include them in the message so users
+ // can see what was passed to the failing call (e.g.
+ // `read_memory_at_address(0x500000000, 4)`). Requires a SFINAE
+ // helper that falls back to a placeholder for types without a
+ // format_provider / operator<<.
+ error = Status::FromErrorStringWithFormatv(
+ "Python exception in {0} method '{1}':\n{2}",
+ GetScriptedMetadata() ? GetScriptedMetadata()->GetClassName()
+ : "<unknown>",
+ method_name, detailed_error);
+
+ // Surface error to user: use callback if available.
+ if (m_error_callback) {
+ m_error_callback(error);
+ }
+
return ErrorWithMessage<T>(
caller_signature, "python static method could not be called", error);
}
@@ -492,6 +558,23 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
}
protected:
+ /// Extract detailed error message including Python backtrace if available.
+ ///
+ /// This helper processes llvm::Error objects that may contain PythonException
+ /// instances, extracting full Python backtraces when available.
+ ///
+ /// \param error The llvm::Error to extract information from.
+ /// \return A string containing the error message, including full Python
+ /// backtrace if the error was a PythonException.
+ static std::string ExtractPythonError(llvm::Error error) {
+ std::string error_msg;
+ llvm::handleAllErrors(
+ std::move(error),
+ [&](python::PythonException &E) { error_msg = E.ReadBacktrace(); },
+ [&](const llvm::ErrorInfoBase &E) { error_msg = E.message(); });
+ return error_msg;
+ }
+
template <typename T = StructuredData::ObjectSP>
T ExtractValueFromPythonObject(python::PythonObject &p, Status &error) {
return p.CreateStructuredObject();
@@ -530,14 +613,42 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
llvm::createStringError("not initialized");
std::apply(
[&implementor, &method_name, &expected_return_object](auto &&...args) {
- llvm::consumeError(expected_return_object.takeError());
+ // Consume placeholder error (expected initial state).
+ if (!expected_return_object)
+ llvm::consumeError(expected_return_object.takeError());
expected_return_object =
implementor.CallMethod(method_name.data(), args...);
},
transformed_args);
if (llvm::Error e = expected_return_object.takeError()) {
- error = Status::FromError(std::move(e));
+ // Extract Python backtrace and log it.
+ std::string detailed_error = ExtractPythonError(std::move(e));
+
+ Log *log = GetLog(LLDBLog::Script);
+ if (log) {
+ LLDB_LOGF(log, "%s: Python exception in %s:\n%s",
+ caller_signature.c_str(), method_name.data(),
+ detailed_error.c_str());
+ }
+
+ // Create Status with full context including the interface type.
+ // TODO: Stringify `args` and include them in the message so users
+ // can see what was passed to the failing call (e.g.
+ // `read_memory_at_address(0x500000000, 4)`). Requires a SFINAE
+ // helper that falls back to a placeholder for types without a
+ // format_provider / operator<<.
+ error = Status::FromErrorStringWithFormatv(
+ "Python exception in {0} method '{1}':\n{2}",
+ GetScriptedMetadata() ? GetScriptedMetadata()->GetClassName()
+ : "<unknown>",
+ method_name, detailed_error);
+
+ // Surface error to user: use callback if available.
+ if (m_error_callback) {
+ m_error_callback(error);
+ }
+
return ErrorWithMessage<T>(caller_signature,
"python method could not be called", error);
}
@@ -738,6 +849,11 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
// The lifetime is managed by the ScriptInterpreter
ScriptInterpreterPythonImpl &m_interpreter;
+
+ // Optional callback for surfacing errors to users (e.g.,
+ // CommandReturnObject). If not set, errors are reported via
+ // Debugger::ReportError().
+ ErrorCallback m_error_callback;
};
template <>
diff --git a/lldb/source/Target/ScriptedThreadPlan.cpp b/lldb/source/Target/ScriptedThreadPlan.cpp
index 7b0a6bd3a7279..ffdd3cc018acb 100644
--- a/lldb/source/Target/ScriptedThreadPlan.cpp
+++ b/lldb/source/Target/ScriptedThreadPlan.cpp
@@ -20,6 +20,7 @@
#include "lldb/Utility/LLDBLog.h"
#include "lldb/Utility/Log.h"
#include "lldb/Utility/State.h"
+#include "llvm/Support/FormatVariadic.h"
using namespace lldb;
using namespace lldb_private;
@@ -89,6 +90,10 @@ void ScriptedThreadPlan::DidPush() {
this->shared_from_this());
if (!obj_or_err) {
m_error_str = llvm::toString(obj_or_err.takeError());
+ Debugger::ReportError(
+ llvm::formatv("Failed to create ScriptedThreadPlan: {0}", m_error_str)
+ .str(),
+ m_process.GetTarget().GetDebugger().GetID());
SetPlanComplete(false);
} else
m_implementation_sp = *obj_or_err;
diff --git a/lldb/test/API/functionalities/scripted_extensions/Makefile b/lldb/test/API/functionalities/scripted_extensions/Makefile
new file mode 100644
index 0000000000000..10495940055b6
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py b/lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py
new file mode 100644
index 0000000000000..5ffe7c316546f
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py
@@ -0,0 +1,124 @@
+"""
+Verify that exceptions raised inside scripted extension affordance methods
+(or missing required abstract methods) are surfaced to the user.
+
+For entry points with no return-channel for errors
+(`ScriptedProcess::CreateInstance`, `OperatingSystemPython` ctor,
+`ScriptedThreadPlan::DidPush`, `BreakpointResolverScripted` ctor) the
+diagnostic is broadcast via `Debugger::ReportError` and asserted on a
+listener.
+
+Entry points that already return an `llvm::Expected` / `Status`
+(`ScriptedThread::Create`, `ScriptedFrameProvider::CreateInstance`,
+`StopHookScripted::SetScriptCallback`) propagate the detailed error
+through their return type; tests for those are tracked as follow-up.
+"""
+
+import os
+
+import lldb
+import lldbsuite.test.lldbutil as lldbutil
+from lldbsuite.test.decorators import expectedFailureAll
+from lldbsuite.test.lldbtest import TestBase
+
+
+class TestScriptedExtensionsDiagnostics(TestBase):
+ NO_DEBUG_INFO_TESTCASE = True
+
+ def setUp(self):
+ TestBase.setUp(self)
+ self.broadcaster = self.dbg.GetBroadcaster()
+ self.listener = lldbutil.start_listening_from(
+ self.broadcaster,
+ lldb.SBDebugger.eBroadcastBitWarning | lldb.SBDebugger.eBroadcastBitError,
+ )
+ script_path = os.path.join(
+ self.getSourceDir(), "malformed_scripted_extensions.py"
+ )
+ self.runCmd("command script import " + script_path)
+
+ def assert_diagnostic(self, expected_substring):
+ event = lldbutil.fetch_next_event(self, self.listener, self.broadcaster)
+ data = lldb.SBDebugger.GetDiagnosticFromEvent(event)
+ self.assertTrue(data.IsValid(), "event has diagnostic data")
+ message = data.GetValueForKey("message").GetStringValue(4096)
+ self.assertIn(expected_substring, message)
+
+ def create_target(self):
+ self.build()
+ target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+ self.assertTrue(target, "valid target")
+ return target
+
+ # ------------------------------------------------------------------
+ # ScriptedProcess - reports via ScriptedProcess::CreateInstance
+ # ------------------------------------------------------------------
+
+ def test_scripted_process_missing_methods(self):
+ """A ScriptedProcess missing abstract methods should emit a
+ diagnostic naming the missing method."""
+ target = self.create_target()
+ launch_info = lldb.SBLaunchInfo(None)
+ launch_info.SetProcessPluginName("ScriptedProcess")
+ launch_info.SetScriptedProcessClassName(
+ "malformed_scripted_extensions.MissingMethodsScriptedProcess"
+ )
+ error = lldb.SBError()
+ target.Launch(launch_info, error)
+ self.assertTrue(error.Fail(), "launch should fail")
+ self.assert_diagnostic("read_memory_at_address")
+
+ # ------------------------------------------------------------------
+ # BreakpointResolverScripted - reports via
+ # BreakpointResolverScripted::CreateImplementationIfNeeded.
+ # `m_error` is set but never surfaced to the user, so ReportError is
+ # the only user-visible channel.
+ # ------------------------------------------------------------------
+
+ # TODO: malformed_scripted_extensions needs a class whose __init__
+ # raises (the current ExceptionScriptedBreakpointResolver only raises
+ # from __callback__, which goes through Dispatch/Status). Tracked as
+ # follow-up.
+ @expectedFailureAll(bugnumber="needs an init-raising malformed class")
+ def test_scripted_breakpoint_resolver_init_failure(self):
+ target = self.create_target()
+ target.BreakpointCreateFromScript(
+ "malformed_scripted_extensions.ExceptionScriptedBreakpointResolver",
+ lldb.SBStructuredData(),
+ lldb.SBFileSpecList(),
+ lldb.SBFileSpecList(),
+ )
+ self.assert_diagnostic("ExceptionScriptedBreakpointResolver")
+
+ # ------------------------------------------------------------------
+ # ScriptedThreadPlan - reports via ScriptedThreadPlan::DidPush. The
+ # plan stores the error in m_error_str but never surfaces it; the
+ # diagnostic is the user-visible channel.
+ # ------------------------------------------------------------------
+
+ @expectedFailureAll(bugnumber="needs an init-raising malformed class")
+ def test_scripted_thread_plan_init_failure(self):
+ target = self.create_target()
+ process = target.LaunchSimple(None, None, self.get_process_working_directory())
+ self.assertTrue(process, "valid process")
+ thread = process.GetSelectedThread()
+ self.assertTrue(thread, "valid thread")
+ thread.StepUsingScriptedThreadPlan(
+ "malformed_scripted_extensions.ExceptionScriptedThreadPlan"
+ )
+ self.assert_diagnostic("ExceptionScriptedThreadPlan")
+
+ # ------------------------------------------------------------------
+ # The remaining entry points need a live inferior process to trigger
+ # their creation paths, or have no plugin implementation yet.
+ # ------------------------------------------------------------------
+
+ @expectedFailureAll(
+ bugnumber="OperatingSystemPython needs a live process to load the OS plugin"
+ )
+ def test_operating_system_missing_methods(self):
+ self.assert_diagnostic("get_thread_info")
+
+ @expectedFailureAll(bugnumber="ScriptedPlatform has no plugin implementation yet")
+ def test_scripted_platform_missing_methods(self):
+ self.assert_diagnostic("list_processes")
diff --git a/lldb/test/API/functionalities/scripted_extensions/main.c b/lldb/test/API/functionalities/scripted_extensions/main.c
new file mode 100644
index 0000000000000..237c8ce181774
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/main.c
@@ -0,0 +1 @@
+int main() {}
diff --git a/lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py b/lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py
new file mode 100644
index 0000000000000..e9ff0fd17a57c
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py
@@ -0,0 +1,195 @@
+"""
+Intentionally malformed scripted extensions used by
+TestScriptedExtensionsDiagnostics.
+
+Each class either omits a required abstract method or raises a Python
+exception from one of its affordance methods. The corresponding test asserts
+that LLDB surfaces those errors to the user instead of silently swallowing
+them.
+"""
+
+# ---------------------------------------------------------------------------
+# Scripted Process
+# ---------------------------------------------------------------------------
+
+
+class MissingMethodsScriptedProcess:
+ """Missing required abstract method `read_memory_at_address`."""
+
+ def __init__(self, exe_ctx, args):
+ self.exe_ctx = exe_ctx
+ self.args = args
+
+ def get_scripted_thread_plugin(self):
+ return None
+
+ def is_alive(self):
+ return True
+
+
+class ExceptionScriptedProcess:
+ """All abstract methods present, but `launch` raises."""
+
+ def __init__(self, exe_ctx, args):
+ self.exe_ctx = exe_ctx
+ self.args = args
+
+ def get_scripted_thread_plugin(self):
+ return None
+
+ def is_alive(self):
+ return True
+
+ def read_memory_at_address(self, addr, size, error):
+ return None
+
+ def launch(self):
+ raise RuntimeError("intentional exception from launch()")
+
+
+# ---------------------------------------------------------------------------
+# Scripted Thread
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedThread:
+ def __init__(self, process, args):
+ self.process = process
+ self.args = args
+
+ def get_thread_id(self):
+ raise ValueError("intentional exception from get_thread_id()")
+
+ def get_register_context(self):
+ return ""
+
+ def get_name(self):
+ return "ExceptionScriptedThread"
+
+ def get_state(self):
+ return 0
+
+
+# ---------------------------------------------------------------------------
+# Scripted Platform
+# ---------------------------------------------------------------------------
+
+
+class MissingMethodsScriptedPlatform:
+ """Missing required abstract method `list_processes`."""
+
+ def __init__(self, exe_ctx, args):
+ self.exe_ctx = exe_ctx
+ self.args = args
+
+
+class ExceptionScriptedPlatform:
+ def __init__(self, exe_ctx, args):
+ self.exe_ctx = exe_ctx
+ self.args = args
+
+ def list_processes(self):
+ raise RuntimeError("intentional exception from list_processes()")
+
+ def get_process_info(self, pid):
+ return None
+
+ def launch_process(self, launch_info):
+ return None
+
+ def kill_process(self, pid):
+ return None
+
+
+# ---------------------------------------------------------------------------
+# Scripted Frame Provider
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedFrameProvider:
+ def __init__(self, frames, args):
+ self.frames = frames
+ self.args = args
+
+ def get_num_frames(self):
+ raise RuntimeError("intentional exception from get_num_frames()")
+
+ def get_frame_at_index(self, idx):
+ return None
+
+
+# ---------------------------------------------------------------------------
+# Scripted Thread Plan
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedThreadPlan:
+ def __init__(self, thread_plan, args):
+ self.thread_plan = thread_plan
+ self.args = args
+
+ def explains_stop(self, event):
+ raise RuntimeError("intentional exception from explains_stop()")
+
+ def should_stop(self, event):
+ return True
+
+ def is_stale(self):
+ return False
+
+
+# ---------------------------------------------------------------------------
+# Scripted Breakpoint Resolver
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedBreakpointResolver:
+ def __init__(self, bkpt, args):
+ self.bkpt = bkpt
+ self.args = args
+
+ def __callback__(self, sym_ctx):
+ raise RuntimeError("intentional exception from __callback__()")
+
+ def get_short_help(self):
+ return "Exception breakpoint resolver"
+
+
+# ---------------------------------------------------------------------------
+# Scripted Stop Hook
+# ---------------------------------------------------------------------------
+
+
+class ExceptionScriptedStopHook:
+ def __init__(self, target, args):
+ self.target = target
+ self.args = args
+
+ def handle_stop(self, exe_ctx, stream):
+ raise RuntimeError("intentional exception from handle_stop()")
+
+
+# ---------------------------------------------------------------------------
+# Operating System
+# ---------------------------------------------------------------------------
+
+
+class MissingMethodsOperatingSystem:
+ """Missing required abstract method `get_thread_info`."""
+
+ def __init__(self, process):
+ self.process = process
+
+
+class ExceptionOperatingSystem:
+ def __init__(self, process):
+ self.process = process
+
+ def get_thread_info(self):
+ raise RuntimeError("intentional exception from get_thread_info()")
+
+ def get_register_info(self):
+ return {}
+
+ def get_register_data(self, tid):
+ return b""
More information about the lldb-commits
mailing list