[Lldb-commits] [lldb] [lldb-dap] Refactoring IO handling for the SBDebugger. (PR #167067)
John Harrison via lldb-commits
lldb-commits at lists.llvm.org
Fri Nov 7 16:59:02 PST 2025
https://github.com/ashgti created https://github.com/llvm/llvm-project/pull/167067
This refactors the IO handling of lldb-dap and its SBDebugger. This adjusts the SBDebugger instance to have a pty associated with in/out/err. Using a pty allows us to handle commands with raw input modes, such as `script` or `breakpoint command add`.
Additionally, to better handle output produced by the debugger and evaluate command I added a print helper. This new print helper will inspect the SBCommandReturnObject and store any associated variables in the variable store. This lets users more easily inspect variables directly in the Debug Console in VSCode.
See the associated screenshot for more info comparing `v <var>`, `p <var>` and `e <var>` as an example.
<img width="1825" height="1090" alt="Screenshot 2025-11-07 at 4 57 07 PM" src="https://github.com/user-attachments/assets/83c35131-2549-4569-bab8-b0388982fa28" />
>From 4930c76f1fbea74e7c1c4e040ab786489ee60e93 Mon Sep 17 00:00:00 2001
From: John Harrison <harjohn at google.com>
Date: Fri, 7 Nov 2025 16:52:16 -0800
Subject: [PATCH] [lldb-dap] Refactoring IO handling for the SBDebugger.
This refactors the IO handling of lldb-dap and its SBDebugger. This adjusts the SBDebugger instance to have a pty associated with in/out/err. Using a pty allows us to handle commands with raw input modes, such as `script` or `breakpoint command add`.
Additionally, to better handle output produced by the debugger and evaluate command I added a print helper. This new print helper will inspect the SBCommandReturnObject and store any associated variables in the variable store. This lets users more easily inspect variables directly in the Debug Console in VSCode.
---
lldb/tools/lldb-dap/DAP.cpp | 53 ++++++++----
lldb/tools/lldb-dap/DAP.h | 22 +++--
lldb/tools/lldb-dap/EventHelper.cpp | 2 +-
.../lldb-dap/Handler/CompletionsHandler.cpp | 3 +-
.../Handler/EvaluateRequestHandler.cpp | 41 ++++++++--
.../Handler/InitializeRequestHandler.cpp | 80 ++++++++++++++-----
.../tools/lldb-dap/Handler/RequestHandler.cpp | 7 ++
lldb/tools/lldb-dap/Variables.cpp | 19 +++++
lldb/tools/lldb-dap/Variables.h | 3 +
9 files changed, 181 insertions(+), 49 deletions(-)
diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp
index f009a902f79e7..235d76c64abf3 100644
--- a/lldb/tools/lldb-dap/DAP.cpp
+++ b/lldb/tools/lldb-dap/DAP.cpp
@@ -20,7 +20,6 @@
#include "Protocol/ProtocolRequests.h"
#include "Protocol/ProtocolTypes.h"
#include "ProtocolUtils.h"
-#include "Transport.h"
#include "lldb/API/SBBreakpoint.h"
#include "lldb/API/SBCommandInterpreter.h"
#include "lldb/API/SBEvent.h"
@@ -29,12 +28,13 @@
#include "lldb/API/SBMutex.h"
#include "lldb/API/SBProcess.h"
#include "lldb/API/SBStream.h"
-#include "lldb/Host/JSONTransport.h"
+#include "lldb/Host/File.h"
#include "lldb/Host/MainLoop.h"
#include "lldb/Host/MainLoopBase.h"
#include "lldb/Utility/Status.h"
#include "lldb/lldb-defines.h"
#include "lldb/lldb-enumerations.h"
+#include "lldb/lldb-forward.h"
#include "lldb/lldb-types.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/STLExtras.h"
@@ -55,7 +55,6 @@
#include <cstdint>
#include <cstdio>
#include <functional>
-#include <future>
#include <memory>
#include <mutex>
#include <optional>
@@ -77,14 +76,6 @@ using namespace lldb_dap;
using namespace lldb_dap::protocol;
using namespace lldb_private;
-namespace {
-#ifdef _WIN32
-const char DEV_NULL[] = "nul";
-#else
-const char DEV_NULL[] = "/dev/null";
-#endif
-} // namespace
-
namespace lldb_dap {
static std::string GetStringFromStructuredData(lldb::SBStructuredData &data,
@@ -226,8 +217,6 @@ ExceptionBreakpoint *DAP::GetExceptionBreakpoint(const lldb::break_id_t bp_id) {
}
llvm::Error DAP::ConfigureIO(std::FILE *overrideOut, std::FILE *overrideErr) {
- in = lldb::SBFile(std::fopen(DEV_NULL, "r"), /*transfer_ownership=*/true);
-
if (auto Error = out.RedirectTo(overrideOut, [this](llvm::StringRef output) {
SendOutput(OutputType::Console, output);
}))
@@ -375,7 +364,8 @@ Id DAP::Send(const Message &message) {
// "required": [ "event", "body" ]
// }]
// }
-void DAP::SendOutput(OutputType o, const llvm::StringRef output) {
+void DAP::SendOutput(OutputType o, const llvm::StringRef output,
+ int64_t varref) {
if (output.empty())
return;
@@ -409,6 +399,10 @@ void DAP::SendOutput(OutputType o, const llvm::StringRef output) {
llvm::json::Object body;
body.try_emplace("category", category);
EmplaceSafeString(body, "output", output.slice(idx, end + 1).str());
+ if (varref != 0) {
+ body["variablesReference"] = varref;
+ varref = 0; // In case there are multiple lines.
+ }
event.try_emplace("body", std::move(body));
SendJSON(llvm::json::Value(std::move(event)));
idx = end + 1;
@@ -1075,9 +1069,7 @@ llvm::Error DAP::Loop() {
auto thread = std::thread(std::bind(&DAP::TransportHandler, this));
auto cleanup = llvm::make_scope_exit([this]() {
- // FIXME: Merge these into the MainLoop handler.
- out.Stop();
- err.Stop();
+ pty.ClosePrimaryFileDescriptor();
StopEventHandlers();
// Destroy the debugger when the session ends. This will trigger the
@@ -1304,6 +1296,33 @@ void DAP::StartEventThread() {
event_thread = std::thread(&DAP::EventThread, this);
}
+void DAP::StartIOThread() {
+ lldb::FileSP file = std::make_shared<lldb_private::NativeFile>(
+ pty.GetPrimaryFileDescriptor(),
+ lldb_private::NativeFile::eOpenOptionReadWrite |
+ lldb_private::NativeFile::eOpenOptionNonBlocking,
+ false);
+
+ Status error;
+ handle = m_loop.RegisterReadObject(
+ file,
+ [this, file](MainLoopBase &loop) {
+ char buf[4096];
+ size_t len = sizeof(buf);
+ if (auto err = file->Read(buf, len).takeError())
+ DAP_LOG_ERROR(log, std::move(err), "Reading from pty failed {0}");
+ if (len == 0) { // EOF
+ loop.RequestTermination();
+ return;
+ }
+ llvm::StringRef output = {buf, len};
+ output = output.trim();
+ if (!output.empty())
+ SendOutput(OutputType::Console, output);
+ },
+ error);
+}
+
void DAP::StartProgressEventThread() {
progress_event_thread = std::thread(&DAP::ProgressEventThread, this);
}
diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h
index b4f111e4e720c..0e4855451e87a 100644
--- a/lldb/tools/lldb-dap/DAP.h
+++ b/lldb/tools/lldb-dap/DAP.h
@@ -22,21 +22,19 @@
#include "Transport.h"
#include "Variables.h"
#include "lldb/API/SBBroadcaster.h"
-#include "lldb/API/SBCommandInterpreter.h"
+#include "lldb/API/SBCommandReturnObject.h"
#include "lldb/API/SBDebugger.h"
#include "lldb/API/SBError.h"
-#include "lldb/API/SBFile.h"
#include "lldb/API/SBFormat.h"
#include "lldb/API/SBFrame.h"
#include "lldb/API/SBMutex.h"
#include "lldb/API/SBTarget.h"
#include "lldb/API/SBThread.h"
#include "lldb/Host/MainLoop.h"
-#include "lldb/Utility/Status.h"
+#include "lldb/Host/PseudoTerminal.h"
#include "lldb/lldb-types.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/DenseSet.h"
-#include "llvm/ADT/FunctionExtras.h"
#include "llvm/ADT/SmallSet.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
@@ -47,6 +45,7 @@
#include <condition_variable>
#include <cstdint>
#include <deque>
+#include <future>
#include <memory>
#include <mutex>
#include <optional>
@@ -80,13 +79,18 @@ enum class ReplMode { Variable = 0, Command, Auto };
using DAPTransport = lldb_private::transport::JSONTransport<ProtocolDescriptor>;
+struct EvaluateContext {
+ lldb::SBCommandReturnObject result;
+ std::promise<void> done;
+};
+
struct DAP final : public DAPTransport::MessageHandler {
/// Path to the lldb-dap binary itself.
static llvm::StringRef debug_adapter_path;
Log *log;
DAPTransport &transport;
- lldb::SBFile in;
+ lldb_private::PseudoTerminal pty;
OutputRedirector out;
OutputRedirector err;
@@ -99,6 +103,8 @@ struct DAP final : public DAPTransport::MessageHandler {
/// The target instance for this DAP session.
lldb::SBTarget target;
+ std::optional<EvaluateContext> context;
+
Variables variables;
lldb::SBBroadcaster broadcaster;
FunctionBreakpointMap function_breakpoints;
@@ -223,7 +229,8 @@ struct DAP final : public DAPTransport::MessageHandler {
/// Send the given message to the client.
protocol::Id Send(const protocol::Message &message);
- void SendOutput(OutputType o, const llvm::StringRef output);
+ void SendOutput(OutputType o, const llvm::StringRef output,
+ int64_t varref = 0);
void SendProgressEvent(uint64_t progress_id, const char *message,
uint64_t completed, uint64_t total);
@@ -409,6 +416,7 @@ struct DAP final : public DAPTransport::MessageHandler {
lldb::SBMutex GetAPIMutex() const { return target.GetAPIMutex(); }
void StartEventThread();
+ void StartIOThread();
void StartProgressEventThread();
/// Sets the given protocol `breakpoints` in the given `source`, while
@@ -477,6 +485,8 @@ struct DAP final : public DAPTransport::MessageHandler {
// Loop for managing reading from the client.
lldb_private::MainLoop &m_loop;
+ lldb_private::MainLoop::ReadHandleUP handle;
+
std::mutex m_cancelled_requests_mutex;
llvm::SmallSet<int64_t, 4> m_cancelled_requests;
diff --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp
index 12d9e21c52ab3..723976da09c09 100644
--- a/lldb/tools/lldb-dap/EventHelper.cpp
+++ b/lldb/tools/lldb-dap/EventHelper.cpp
@@ -243,7 +243,7 @@ void SendTerminatedEvent(DAP &dap) { dap.SendTerminatedEvent(); }
// Grab any STDOUT and STDERR from the process and send it up to VS Code
// via an "output" event to the "stdout" and "stderr" categories.
void SendStdOutStdErr(DAP &dap, lldb::SBProcess &process) {
- char buffer[OutputBufferSize];
+ char buffer[OutputBufferSize] = {0};
size_t count;
while ((count = process.GetSTDOUT(buffer, sizeof(buffer))) > 0)
dap.SendOutput(OutputType::Stdout, llvm::StringRef(buffer, count));
diff --git a/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp b/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp
index de9a15dcb73f4..c8a221b4284de 100644
--- a/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/CompletionsHandler.cpp
@@ -7,10 +7,11 @@
//===----------------------------------------------------------------------===//
#include "DAP.h"
-#include "JSONUtils.h"
#include "Protocol/ProtocolRequests.h"
#include "Protocol/ProtocolTypes.h"
#include "RequestHandler.h"
+#include "lldb/API/SBCommandInterpreter.h"
+#include "lldb/API/SBDebugger.h"
#include "lldb/API/SBStringList.h"
using namespace llvm;
diff --git a/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp
index e1556846dff19..64131d6e2b67c 100644
--- a/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp
@@ -9,8 +9,14 @@
#include "DAP.h"
#include "EventHelper.h"
#include "JSONUtils.h"
-#include "LLDBUtils.h"
#include "RequestHandler.h"
+#include "lldb/API/SBCommandInterpreter.h"
+#include "lldb/API/SBCommandReturnObject.h"
+#include "lldb/lldb-enumerations.h"
+#include "llvm/ADT/StringRef.h"
+#include <future>
+#include <optional>
+#include <unistd.h>
namespace lldb_dap {
@@ -164,13 +170,36 @@ void EvaluateRequestHandler::operator()(
dap.focus_tid = frame.GetThread().GetThreadID();
}
- bool required_command_failed = false;
- std::string result = RunLLDBCommands(
- dap.debugger, llvm::StringRef(), {expression}, required_command_failed,
- /*parse_command_directives=*/false, /*echo_commands=*/false);
+ std::string result = "";
+ int64_t varref = 0;
+ int fd = dap.pty.GetPrimaryFileDescriptor();
+ for (auto expr : llvm::split(expression, "\n")) {
+ if (!dap.debugger.GetCommandInterpreter().IsActive()) {
+ ::write(fd, expr.data(), expr.size());
+ std::string rcnl = "\r\n";
+ ::write(fd, rcnl.data(), rcnl.size());
+ continue;
+ }
+
+ dap.context = EvaluateContext{};
+ std::future<void> done = dap.context->done.get_future();
+ ::write(fd, expr.data(), expr.size());
+ std::string rcnl = "\r\n";
+ ::write(fd, rcnl.data(), rcnl.size());
+ dap.GetAPIMutex().unlock();
+ done.wait();
+ dap.GetAPIMutex().lock();
+ lldb::SBCommandReturnObject ret = dap.context->result;
+ dap.context = std::nullopt;
+ if (ret)
+ result += ret.GetOutput();
+ auto variables = ret.GetValues(lldb::eNoDynamicValues);
+ if (variables)
+ varref = dap.variables.InsertVariables(variables, true);
+ }
EmplaceSafeString(body, "result", result);
- body.try_emplace("variablesReference", (int64_t)0);
+ body.try_emplace("variablesReference", (int64_t)varref);
} else {
if (context == "repl") {
// If the expression is empty and the last expression was for a
diff --git a/lldb/tools/lldb-dap/Handler/InitializeRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/InitializeRequestHandler.cpp
index 9069de4a3a690..9383614fe1f64 100644
--- a/lldb/tools/lldb-dap/Handler/InitializeRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/InitializeRequestHandler.cpp
@@ -9,11 +9,12 @@
#include "CommandPlugins.h"
#include "DAP.h"
#include "EventHelper.h"
-#include "JSONUtils.h"
-#include "LLDBUtils.h"
#include "Protocol/ProtocolRequests.h"
#include "RequestHandler.h"
+#include "lldb/API/SBCommandReturnObject.h"
#include "lldb/API/SBTarget.h"
+#include "lldb/Host/File.h"
+#include "lldb/lldb-enumerations.h"
using namespace lldb_dap;
using namespace lldb_dap::protocol;
@@ -25,21 +26,68 @@ llvm::Expected<InitializeResponse> InitializeRequestHandler::Run(
// Do not source init files until in/out/err are configured.
dap.debugger = lldb::SBDebugger::Create(false);
- dap.debugger.SetInputFile(dap.in);
+
+ if (auto err =
+ dap.pty.OpenFirstAvailablePrimary(O_RDWR | O_NOCTTY | O_NONBLOCK))
+ return err;
+
+ if (auto err = dap.pty.OpenSecondary(O_RDWR | O_NOCTTY))
+ return err;
+
+ lldb::FileSP io = std::make_shared<lldb_private::NativeFile>(
+ dap.pty.ReleaseSecondaryFileDescriptor(),
+ lldb_private::NativeFile::eOpenOptionReadWrite, true);
+ dap.debugger.SetInputFile(io);
+ dap.debugger.SetOutputFile(io);
+ dap.debugger.SetErrorFile(io);
+ dap.debugger.SetPrompt(
+ ""); // disable the prompt to minimize noise in the Debug Console.
+
+ lldb_private::Terminal terminal(dap.pty.GetPrimaryFileDescriptor());
+ llvm::consumeError(terminal.SetRaw());
+
dap.target = dap.debugger.GetDummyTarget();
- llvm::Expected<int> out_fd = dap.out.GetWriteFileDescriptor();
- if (!out_fd)
- return out_fd.takeError();
- dap.debugger.SetOutputFile(lldb::SBFile(*out_fd, "w", false));
+ if (arguments.supportedFeatures.contains(eClientFeatureProgressReporting))
+ dap.StartProgressEventThread();
- llvm::Expected<int> err_fd = dap.err.GetWriteFileDescriptor();
- if (!err_fd)
- return err_fd.takeError();
- dap.debugger.SetErrorFile(lldb::SBFile(*err_fd, "w", false));
+ // Start our event thread so we can receive events from the debugger, target,
+ // process and more.
+ dap.StartEventThread();
+ dap.StartIOThread();
auto interp = dap.debugger.GetCommandInterpreter();
+ interp.SetPrintCallback(
+ [](lldb::SBCommandReturnObject &result, void *baton) {
+ if (!result.GetCommand()) // skip internal commands.
+ return lldb::eCommandReturnObjectPrintCallbackSkipped;
+
+ DAP *dap = static_cast<DAP *>(baton);
+
+ if (dap->context) {
+ dap->context->result = result;
+ dap->context->done.set_value();
+ return lldb::eCommandReturnObjectPrintCallbackHandled;
+ }
+
+ std::string output;
+ if (result.GetOutputSize())
+ output += result.GetOutput();
+ if (result.GetErrorSize())
+ output += result.GetError();
+
+ uint64_t varref = 0;
+ auto variables = result.GetValues(lldb::eNoDynamicValues);
+ if (variables)
+ varref = dap->variables.InsertVariables(variables, false);
+
+ dap->SendOutput(OutputType::Console, output, varref);
+
+ return lldb::eCommandReturnObjectPrintCallbackHandled;
+ },
+ &dap);
+
// The sourceInitFile option is not part of the DAP specification. It is an
// extension used by the test suite to prevent sourcing `.lldbinit` and
// changing its behavior. The CLI flag --no-lldbinit takes precedence over
@@ -54,6 +102,9 @@ llvm::Expected<InitializeResponse> InitializeRequestHandler::Run(
interp.SourceInitFileInHomeDirectory(init);
}
+ // Spawn the IOHandler thread.
+ dap.debugger.RunCommandInterpreter(false, true);
+
if (llvm::Error err = dap.RunPreInitCommands())
return err;
@@ -72,12 +123,5 @@ llvm::Expected<InitializeResponse> InitializeRequestHandler::Run(
cmd.AddCommand("send-event", new SendEventCommand(dap),
"Sends an DAP event to the client.");
- if (arguments.supportedFeatures.contains(eClientFeatureProgressReporting))
- dap.StartProgressEventThread();
-
- // Start our event thread so we can receive events from the debugger, target,
- // process and more.
- dap.StartEventThread();
-
return dap.GetCapabilities();
}
diff --git a/lldb/tools/lldb-dap/Handler/RequestHandler.cpp b/lldb/tools/lldb-dap/Handler/RequestHandler.cpp
index d67437ad5b3ae..ce695a6565d3b 100644
--- a/lldb/tools/lldb-dap/Handler/RequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/RequestHandler.cpp
@@ -17,6 +17,7 @@
#include "RunInTerminal.h"
#include "lldb/API/SBDefines.h"
#include "lldb/API/SBEnvironment.h"
+#include "lldb/lldb-types.h"
#include "llvm/Support/Error.h"
#include <mutex>
@@ -258,6 +259,12 @@ llvm::Error BaseRequestHandler::LaunchProcess(
void BaseRequestHandler::PrintWelcomeMessage() const {
#ifdef LLDB_DAP_WELCOME_MESSAGE
dap.SendOutput(OutputType::Console, LLDB_DAP_WELCOME_MESSAGE);
+#else
+ dap.SendOutput(
+ OutputType::Console,
+ "lldb-dap launched, type \"help\" for more information or visit "
+ "https://github.com/llvm/llvm-project/blob/main/lldb/tools/"
+ "lldb-dap/README.md");
#endif
}
diff --git a/lldb/tools/lldb-dap/Variables.cpp b/lldb/tools/lldb-dap/Variables.cpp
index 777e3183d8c0d..d0cfc00a46eb5 100644
--- a/lldb/tools/lldb-dap/Variables.cpp
+++ b/lldb/tools/lldb-dap/Variables.cpp
@@ -8,6 +8,7 @@
#include "Variables.h"
#include "JSONUtils.h"
+#include "lldb/API/SBValueList.h"
using namespace lldb_dap;
@@ -20,6 +21,9 @@ lldb::SBValueList *Variables::GetTopLevelScope(int64_t variablesReference) {
case VARREF_REGS:
return ®isters;
default:
+ if (m_referencedlists.contains(variablesReference) &&
+ m_referencedlists[variablesReference].IsValid())
+ return &m_referencedlists[variablesReference];
return nullptr;
}
}
@@ -63,6 +67,21 @@ int64_t Variables::InsertVariable(lldb::SBValue variable, bool is_permanent) {
return var_ref;
}
+int64_t Variables::InsertVariables(lldb::SBValueList variables,
+ bool is_permanent) {
+ int64_t var_ref = GetNewVariableReference(is_permanent);
+ if (is_permanent) {
+ lldb::SBValueList permvalues;
+ for (uint32_t i = 0; i < variables.GetSize(); i++) {
+ permvalues.Append(variables.GetValueAtIndex(i).Persist());
+ }
+ m_referencedlists[var_ref] = permvalues;
+ } else {
+ m_referencedlists[var_ref] = variables;
+ }
+ return var_ref;
+}
+
lldb::SBValue Variables::FindVariable(uint64_t variablesReference,
llvm::StringRef name) {
lldb::SBValue variable;
diff --git a/lldb/tools/lldb-dap/Variables.h b/lldb/tools/lldb-dap/Variables.h
index 0ed84b36aef99..c59a631aa5328 100644
--- a/lldb/tools/lldb-dap/Variables.h
+++ b/lldb/tools/lldb-dap/Variables.h
@@ -42,6 +42,7 @@ struct Variables {
/// Insert a new \p variable.
/// \return variableReference assigned to this expandable variable.
int64_t InsertVariable(lldb::SBValue variable, bool is_permanent);
+ int64_t InsertVariables(lldb::SBValueList variables, bool is_permanent);
lldb::SBValueList *GetTopLevelScope(int64_t variablesReference);
@@ -62,6 +63,8 @@ struct Variables {
/// These are the variables evaluated from debug console REPL.
llvm::DenseMap<int64_t, lldb::SBValue> m_referencedpermanent_variables;
+ llvm::DenseMap<int64_t, lldb::SBValueList> m_referencedlists;
+
int64_t m_next_temporary_var_ref{VARREF_FIRST_VAR_IDX};
int64_t m_next_permanent_var_ref{PermanentVariableStartIndex};
};
More information about the lldb-commits
mailing list