Skip to content

[lldb] Support CommandInterpreter print callbacks #125006

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 4, 2025

Conversation

JDevlieghere
Copy link
Member

Xcode uses a pseudoterminal for the debugger console.

  • The upside of this apporach is that it means that it can rely on LLDB's IOHandlers for multiline and script input.
  • The downside of this approach is that the command output is printed to the PTY and you don't get a SBCommandReturnObject. Adrian added support for inline diagnostics (Support inline diagnostics in CommandReturnObject #110901) and we'd like to access those from the IDE.

This patch adds support for registering a callback in the command interpreter that gives access to the (SB)CommandReturnObject right before it will be printed. The callback implementation can choose whether it likes to handle printing the result or defer to lldb. If the callback indicated it handled the result, the command interpreter will skip printing the result.

We considered a few other alternatives to solve this problem:

  • The most obvious one is using HandleCommand, which returns a SBCommandReturnObject. The problem with this approach is the multiline input mentioned above. We would need a way to tell the IDE that it should expect multiline input, which isn't known until LLDB starts handling the command.
  • To address the multiline issue,we considered exposing (some of the) IOHandler machinery through the SB API. To solve this particular issue, that would require reimplementing a ton of logic that already exists today in the CommandInterpeter. Furthermore that seems like overkill compared to the proposed solution.

rdar://141254310

@llvmbot
Copy link
Member

llvmbot commented Jan 30, 2025

@llvm/pr-subscribers-lldb

Author: Jonas Devlieghere (JDevlieghere)

Changes

Xcode uses a pseudoterminal for the debugger console.

  • The upside of this apporach is that it means that it can rely on LLDB's IOHandlers for multiline and script input.
  • The downside of this approach is that the command output is printed to the PTY and you don't get a SBCommandReturnObject. Adrian added support for inline diagnostics (#110901) and we'd like to access those from the IDE.

This patch adds support for registering a callback in the command interpreter that gives access to the (SB)CommandReturnObject right before it will be printed. The callback implementation can choose whether it likes to handle printing the result or defer to lldb. If the callback indicated it handled the result, the command interpreter will skip printing the result.

We considered a few other alternatives to solve this problem:

  • The most obvious one is using HandleCommand, which returns a SBCommandReturnObject. The problem with this approach is the multiline input mentioned above. We would need a way to tell the IDE that it should expect multiline input, which isn't known until LLDB starts handling the command.
  • To address the multiline issue,we considered exposing (some of the) IOHandler machinery through the SB API. To solve this particular issue, that would require reimplementing a ton of logic that already exists today in the CommandInterpeter. Furthermore that seems like overkill compared to the proposed solution.

rdar://141254310


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

17 Files Affected:

  • (modified) lldb/bindings/python/python-swigsafecast.swig (+4)
  • (modified) lldb/bindings/python/python-typemaps.swig (+19)
  • (modified) lldb/bindings/python/python-wrapper.swig (+22-2)
  • (modified) lldb/include/lldb/API/SBCommandInterpreter.h (+5-3)
  • (modified) lldb/include/lldb/API/SBCommandReturnObject.h (+2)
  • (modified) lldb/include/lldb/API/SBDefines.h (+3)
  • (modified) lldb/include/lldb/Interpreter/CommandInterpreter.h (+20-9)
  • (modified) lldb/include/lldb/Interpreter/CommandReturnObject.h (+5-5)
  • (modified) lldb/include/lldb/Utility/StreamTee.h (+1-1)
  • (modified) lldb/include/lldb/lldb-enumerations.h (+9)
  • (modified) lldb/source/API/SBCommandInterpreter.cpp (+39-1)
  • (modified) lldb/source/Interpreter/CommandInterpreter.cpp (+40-20)
  • (modified) lldb/source/Interpreter/CommandReturnObject.cpp (+3-2)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h (+7-6)
  • (added) lldb/test/API/python_api/interpreter_callback/Makefile (+3)
  • (added) lldb/test/API/python_api/interpreter_callback/TestCommandInterepterPrintCallback.py (+66)
  • (added) lldb/test/API/python_api/interpreter_callback/main.c (+6)
diff --git a/lldb/bindings/python/python-swigsafecast.swig b/lldb/bindings/python/python-swigsafecast.swig
index 429baad158ca5d..4721dfdc17e6a0 100644
--- a/lldb/bindings/python/python-swigsafecast.swig
+++ b/lldb/bindings/python/python-swigsafecast.swig
@@ -9,6 +9,10 @@ PythonObject SWIGBridge::ToSWIGWrapper(std::unique_ptr<lldb::SBValue> value_sb)
   return ToSWIGHelper(value_sb.release(), SWIGTYPE_p_lldb__SBValue);
 }
 
+PythonObject SWIGBridge::ToSWIGWrapper(std::unique_ptr<lldb::SBCommandReturnObject> result_up) {
+  return ToSWIGHelper(result_up.release(), SWIGTYPE_p_lldb__SBCommandReturnObject);
+}
+
 PythonObject SWIGBridge::ToSWIGWrapper(lldb::ValueObjectSP value_sp) {
   return ToSWIGWrapper(std::unique_ptr<lldb::SBValue>(new lldb::SBValue(value_sp)));
 }
diff --git a/lldb/bindings/python/python-typemaps.swig b/lldb/bindings/python/python-typemaps.swig
index f8c33e15c03e66..88b6cd9ef6b6e7 100644
--- a/lldb/bindings/python/python-typemaps.swig
+++ b/lldb/bindings/python/python-typemaps.swig
@@ -476,6 +476,25 @@ template <> bool SetNumberFromPyObject<double>(double &number, PyObject *obj) {
   $1 = $1 || PyCallable_Check(reinterpret_cast<PyObject *>($input));
 }
 
+// For lldb::SBCommandPrintCallback
+%typemap(in) (lldb::SBCommandPrintCallback callback, void *baton) {
+  if (!($input == Py_None ||
+        PyCallable_Check(reinterpret_cast<PyObject *>($input)))) {
+    PyErr_SetString(PyExc_TypeError, "Need a callable object or None!");
+    SWIG_fail;
+  }
+
+  // Don't lose the callback reference.
+  Py_INCREF($input);
+  $1 = LLDBSwigPythonCallPythonCommandPrintCallback;
+  $2 = $input;
+}
+
+%typemap(typecheck) (lldb::SBCommandPrintCallback callback, void *baton) {
+  $1 = $input == Py_None;
+  $1 = $1 || PyCallable_Check(reinterpret_cast<PyObject *>($input));
+}
+
 %typemap(in) (lldb::CommandOverrideCallback callback, void *baton) {
   if (!($input == Py_None ||
         PyCallable_Check(reinterpret_cast<PyObject *>($input)))) {
diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig
index b72a462d04643b..fcabf60008b17d 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -727,7 +727,7 @@ lldb_private::python::SWIGBridge::LLDBSwigPythonHandleOptionArgumentCompletionFo
     dict_sp->AddBooleanItem("no-completion", true);
     return dict_sp;
   }
-    
+
 
   // Convert the return dictionary to a DictionarySP.
   StructuredData::ObjectSP result_obj_sp = result.CreateStructuredObject();
@@ -753,7 +753,7 @@ bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallParsedCommandObject(
   auto pfunc = self.ResolveName<PythonCallable>("__call__");
 
   if (!pfunc.IsAllocated()) {
-    cmd_retobj.AppendError("Could not find '__call__' method in implementation class"); 
+    cmd_retobj.AppendError("Could not find '__call__' method in implementation class");
     return false;
   }
 
@@ -1012,6 +1012,26 @@ static void LLDBSwigPythonCallPythonLogOutputCallback(const char *str,
   }
 }
 
+// For DebuggerTerminateCallback functions
+static CommandReturnObjectCallbackResult LLDBSwigPythonCallPythonCommandPrintCallback(SBCommandReturnObject& result, void *callback_baton) {
+  SWIG_Python_Thread_Block swig_thread_block;
+
+  PyErr_Cleaner py_err_cleaner(true);
+
+  PythonObject result_arg = SWIGBridge::ToSWIGWrapper(
+      std::make_unique<SBCommandReturnObject>(result));
+  PythonCallable callable =
+      Retain<PythonCallable>(reinterpret_cast<PyObject *>(callback_baton));
+
+  if (!callable.IsValid())
+    return eCommandReturnObjectPrintCallbackSkipped;
+
+  PythonObject callback_result = callable(result_arg);
+
+  long long ret_val = unwrapOrSetPythonException(As<long long>(callback_result));
+  return (CommandReturnObjectCallbackResult)ret_val;
+}
+
 // For DebuggerTerminateCallback functions
 static void LLDBSwigPythonCallPythonSBDebuggerTerminateCallback(lldb::user_id_t debugger_id,
                                                       void *baton) {
diff --git a/lldb/include/lldb/API/SBCommandInterpreter.h b/lldb/include/lldb/API/SBCommandInterpreter.h
index b7e39b78586168..dd475ddf6c1702 100644
--- a/lldb/include/lldb/API/SBCommandInterpreter.h
+++ b/lldb/include/lldb/API/SBCommandInterpreter.h
@@ -247,13 +247,13 @@ class SBCommandInterpreter {
                                        lldb::SBStringList &matches,
                                        lldb::SBStringList &descriptions);
 
-  /// Returns whether an interrupt flag was raised either by the SBDebugger - 
+  /// Returns whether an interrupt flag was raised either by the SBDebugger -
   /// when the function is not running on the RunCommandInterpreter thread, or
   /// by SBCommandInterpreter::InterruptCommand if it is.  If your code is doing
-  /// interruptible work, check this API periodically, and interrupt if it 
+  /// interruptible work, check this API periodically, and interrupt if it
   /// returns true.
   bool WasInterrupted() const;
-  
+
   /// Interrupts the command currently executing in the RunCommandInterpreter
   /// thread.
   ///
@@ -331,6 +331,8 @@ class SBCommandInterpreter {
   /// this list. Otherwise this list is empty.
   SBStructuredData GetTranscript();
 
+  void SetPrintCallback(lldb::SBCommandPrintCallback callback, void *baton);
+
 protected:
   friend class lldb_private::CommandPluginInterfaceImplementation;
 
diff --git a/lldb/include/lldb/API/SBCommandReturnObject.h b/lldb/include/lldb/API/SBCommandReturnObject.h
index e8e20a3f3016b8..377e4714f6d26b 100644
--- a/lldb/include/lldb/API/SBCommandReturnObject.h
+++ b/lldb/include/lldb/API/SBCommandReturnObject.h
@@ -17,6 +17,7 @@
 
 namespace lldb_private {
 class CommandPluginInterfaceImplementation;
+class CommandPrintCallbackBaton;
 class SBCommandReturnObjectImpl;
 namespace python {
 class SWIGBridge;
@@ -138,6 +139,7 @@ class LLDB_API SBCommandReturnObject {
 
   friend class lldb_private::CommandPluginInterfaceImplementation;
   friend class lldb_private::python::SWIGBridge;
+  friend class lldb_private::CommandPrintCallbackBaton;
 
   SBCommandReturnObject(lldb_private::CommandReturnObject &ref);
 
diff --git a/lldb/include/lldb/API/SBDefines.h b/lldb/include/lldb/API/SBDefines.h
index 31e8c9279f8b8b..b7b5cc06546f86 100644
--- a/lldb/include/lldb/API/SBDefines.h
+++ b/lldb/include/lldb/API/SBDefines.h
@@ -144,6 +144,9 @@ typedef bool (*SBBreakpointHitCallback)(void *baton, lldb::SBProcess &process,
 typedef void (*SBDebuggerDestroyCallback)(lldb::user_id_t debugger_id,
                                           void *baton);
 
+typedef CommandReturnObjectCallbackResult (*SBCommandPrintCallback)(
+    lldb::SBCommandReturnObject &result, void *baton);
+
 typedef lldb::SBError (*SBPlatformLocateModuleCallback)(
     void *baton, const lldb::SBModuleSpec &module_spec,
     lldb::SBFileSpec &module_file_spec, lldb::SBFileSpec &symbol_file_spec);
diff --git a/lldb/include/lldb/Interpreter/CommandInterpreter.h b/lldb/include/lldb/Interpreter/CommandInterpreter.h
index 2bafc30cc8e23a..b0a2d7ed3ace05 100644
--- a/lldb/include/lldb/Interpreter/CommandInterpreter.h
+++ b/lldb/include/lldb/Interpreter/CommandInterpreter.h
@@ -16,6 +16,7 @@
 #include "lldb/Interpreter/CommandObject.h"
 #include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Utility/Args.h"
+#include "lldb/Utility/Baton.h"
 #include "lldb/Utility/Broadcaster.h"
 #include "lldb/Utility/CompletionRequest.h"
 #include "lldb/Utility/Event.h"
@@ -100,8 +101,7 @@ class CommandInterpreterRunOptions {
                                LazyBool stop_on_error, LazyBool stop_on_crash,
                                LazyBool echo_commands, LazyBool echo_comments,
                                LazyBool print_results, LazyBool print_errors,
-                               LazyBool add_to_history,
-                               LazyBool handle_repeats)
+                               LazyBool add_to_history, LazyBool handle_repeats)
       : m_stop_on_continue(stop_on_continue), m_stop_on_error(stop_on_error),
         m_stop_on_crash(stop_on_crash), m_echo_commands(echo_commands),
         m_echo_comment_commands(echo_comments), m_print_results(print_results),
@@ -248,13 +248,16 @@ class CommandInterpreter : public Broadcaster,
   enum CommandTypes {
     eCommandTypesBuiltin = 0x0001, //< native commands such as "frame"
     eCommandTypesUserDef = 0x0002, //< scripted commands
-    eCommandTypesUserMW  = 0x0004, //< multiword commands (command containers)
+    eCommandTypesUserMW = 0x0004,  //< multiword commands (command containers)
     eCommandTypesAliases = 0x0008, //< aliases such as "po"
-    eCommandTypesHidden  = 0x0010, //< commands prefixed with an underscore
+    eCommandTypesHidden = 0x0010,  //< commands prefixed with an underscore
     eCommandTypesAllThem = 0xFFFF  //< all commands
   };
 
-  // The CommandAlias and CommandInterpreter both have a hand in 
+  typedef lldb::CommandReturnObjectCallbackResult (
+      *CommandReturnObjectCallback)(CommandReturnObject &, void *);
+
+  // The CommandAlias and CommandInterpreter both have a hand in
   // substituting for alias commands.  They work by writing special tokens
   // in the template form of the Alias command, and then detecting them when the
   // command is executed.  These are the special tokens:
@@ -334,9 +337,8 @@ class CommandInterpreter : public Broadcaster,
   ///         dummy "contains everything MWC, so we return null here, but
   ///         in this case error.Success is true.
 
-  CommandObjectMultiword *VerifyUserMultiwordCmdPath(Args &path,
-                                                     bool leaf_is_command,
-                                                     Status &result);
+  CommandObjectMultiword *
+  VerifyUserMultiwordCmdPath(Args &path, bool leaf_is_command, Status &result);
 
   CommandAlias *AddAlias(llvm::StringRef alias_name,
                          lldb::CommandObjectSP &command_obj_sp,
@@ -596,7 +598,7 @@ class CommandInterpreter : public Broadcaster,
   void SetEchoCommentCommands(bool enable);
 
   bool GetRepeatPreviousCommand() const;
-  
+
   bool GetRequireCommandOverwrite() const;
 
   const CommandObject::CommandMap &GetUserCommands() const {
@@ -666,6 +668,9 @@ class CommandInterpreter : public Broadcaster,
     ++m_command_usages[cmd_obj.GetCommandName()];
   }
 
+  void SetPrintCallback(CommandReturnObjectCallback callback,
+                        lldb::BatonSP baton_sp);
+
   llvm::json::Value GetStatistics();
   const StructuredData::Array &GetTranscript() const;
 
@@ -776,6 +781,12 @@ class CommandInterpreter : public Broadcaster,
   std::vector<uint32_t> m_command_source_flags;
   CommandInterpreterRunResult m_result;
 
+  /// An optional callback to handle printing the CommandReturnObject.
+  /// @{
+  CommandReturnObjectCallback m_print_callback = nullptr;
+  lldb::BatonSP m_print_callback_baton_sp;
+  /// @}
+
   // The exit code the user has requested when calling the 'quit' command.
   // No value means the user hasn't set a custom exit code so far.
   std::optional<int> m_quit_exit_code;
diff --git a/lldb/include/lldb/Interpreter/CommandReturnObject.h b/lldb/include/lldb/Interpreter/CommandReturnObject.h
index 9fef59337016df..f96da34889a324 100644
--- a/lldb/include/lldb/Interpreter/CommandReturnObject.h
+++ b/lldb/include/lldb/Interpreter/CommandReturnObject.h
@@ -32,9 +32,9 @@ class CommandReturnObject {
   ~CommandReturnObject() = default;
 
   /// Format any inline diagnostics with an indentation of \c indent.
-  std::string GetInlineDiagnosticString(unsigned indent);
+  std::string GetInlineDiagnosticString(unsigned indent) const;
 
-  llvm::StringRef GetOutputString() {
+  llvm::StringRef GetOutputString() const {
     lldb::StreamSP stream_sp(m_out_stream.GetStreamAtIndex(eStreamStringIndex));
     if (stream_sp)
       return std::static_pointer_cast<StreamString>(stream_sp)->GetString();
@@ -46,7 +46,7 @@ class CommandReturnObject {
   /// If \c with_diagnostics is true, all diagnostics are also
   /// rendered into the string. Otherwise the expectation is that they
   /// are fetched with \ref GetInlineDiagnosticString().
-  std::string GetErrorString(bool with_diagnostics = true);
+  std::string GetErrorString(bool with_diagnostics = true) const;
   StructuredData::ObjectSP GetErrorData();
 
   Stream &GetOutputStream() {
@@ -95,11 +95,11 @@ class CommandReturnObject {
     m_err_stream.SetStreamAtIndex(eImmediateStreamIndex, stream_sp);
   }
 
-  lldb::StreamSP GetImmediateOutputStream() {
+  lldb::StreamSP GetImmediateOutputStream() const {
     return m_out_stream.GetStreamAtIndex(eImmediateStreamIndex);
   }
 
-  lldb::StreamSP GetImmediateErrorStream() {
+  lldb::StreamSP GetImmediateErrorStream() const {
     return m_err_stream.GetStreamAtIndex(eImmediateStreamIndex);
   }
 
diff --git a/lldb/include/lldb/Utility/StreamTee.h b/lldb/include/lldb/Utility/StreamTee.h
index 5695586171f358..571548e2e23fe6 100644
--- a/lldb/include/lldb/Utility/StreamTee.h
+++ b/lldb/include/lldb/Utility/StreamTee.h
@@ -85,7 +85,7 @@ class StreamTee : public Stream {
     return result;
   }
 
-  lldb::StreamSP GetStreamAtIndex(uint32_t idx) {
+  lldb::StreamSP GetStreamAtIndex(uint32_t idx) const {
     lldb::StreamSP stream_sp;
     std::lock_guard<std::recursive_mutex> guard(m_streams_mutex);
     if (idx < m_streams.size())
diff --git a/lldb/include/lldb/lldb-enumerations.h b/lldb/include/lldb/lldb-enumerations.h
index 50d2233509de6f..fecf9cbb765f71 100644
--- a/lldb/include/lldb/lldb-enumerations.h
+++ b/lldb/include/lldb/lldb-enumerations.h
@@ -1368,6 +1368,15 @@ enum Severity {
   eSeverityInfo, // Equivalent to Remark used in clang.
 };
 
+/// Callback return value, indicating whether it handled printing the
+/// CommandReturnObject or deferred doing so to the CommandInterpreter.
+enum CommandReturnObjectCallbackResult {
+  /// The callback deferred printing the command return object.
+  eCommandReturnObjectPrintCallbackSkipped = 0,
+  /// The callback handled printing the command return object.
+  eCommandReturnObjectPrintCallbackHandled = 1,
+};
+
 } // namespace lldb
 
 #endif // LLDB_LLDB_ENUMERATIONS_H
diff --git a/lldb/source/API/SBCommandInterpreter.cpp b/lldb/source/API/SBCommandInterpreter.cpp
index 7a35473283684c..08bac1afc64e87 100644
--- a/lldb/source/API/SBCommandInterpreter.cpp
+++ b/lldb/source/API/SBCommandInterpreter.cpp
@@ -151,7 +151,7 @@ bool SBCommandInterpreter::WasInterrupted() const {
 
 bool SBCommandInterpreter::InterruptCommand() {
   LLDB_INSTRUMENT_VA(this);
-  
+
   return (IsValid() ? m_opaque_ptr->InterruptCommand() : false);
 }
 
@@ -743,3 +743,41 @@ void SBCommand::SetFlags(uint32_t flags) {
   if (IsValid())
     m_opaque_sp->GetFlags().Set(flags);
 }
+
+namespace lldb_private {
+struct CommandCallbackData {
+  SBCommandPrintCallback callback;
+  void *callback_baton;
+};
+
+class CommandPrintCallbackBaton
+    : public lldb_private::TypedBaton<CommandCallbackData> {
+public:
+  CommandPrintCallbackBaton(SBCommandPrintCallback callback, void *baton)
+      : TypedBaton(std::make_unique<CommandCallbackData>()) {
+    getItem()->callback = callback;
+    getItem()->callback_baton = baton;
+  }
+
+  static lldb::CommandReturnObjectCallbackResult
+  PrivateCallback(lldb_private::CommandReturnObject &result, void *baton) {
+    if (baton) {
+      CommandCallbackData *data = (CommandCallbackData *)baton;
+      SBCommandReturnObject sb_result(result);
+      return data->callback(sb_result, data->callback_baton);
+    }
+    return eCommandReturnObjectPrintCallbackSkipped;
+  }
+};
+} // namespace lldb_private
+
+void SBCommandInterpreter::SetPrintCallback(
+    lldb::SBCommandPrintCallback callback, void *baton) {
+  LLDB_INSTRUMENT_VA(this, callback, baton);
+
+  BatonSP baton_sp =
+      std::make_shared<CommandPrintCallbackBaton>(callback, baton);
+  if (m_opaque_ptr)
+    return m_opaque_ptr->SetPrintCallback(
+        &CommandPrintCallbackBaton::PrivateCallback, baton_sp);
+}
diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp
index 764dcfd1903b19..cc74f17b287cf2 100644
--- a/lldb/source/Interpreter/CommandInterpreter.cpp
+++ b/lldb/source/Interpreter/CommandInterpreter.cpp
@@ -3182,30 +3182,44 @@ void CommandInterpreter::IOHandlerInputComplete(IOHandler &io_handler,
   if ((result.Succeeded() &&
        io_handler.GetFlags().Test(eHandleCommandFlagPrintResult)) ||
       io_handler.GetFlags().Test(eHandleCommandFlagPrintErrors)) {
-    // Display any inline diagnostics first.
-    const bool inline_diagnostics = !result.GetImmediateErrorStream() &&
-                                    GetDebugger().GetShowInlineDiagnostics();
-    if (inline_diagnostics) {
-      unsigned prompt_len = m_debugger.GetPrompt().size();
-      if (auto indent = result.GetDiagnosticIndent()) {
-        std::string diags =
-            result.GetInlineDiagnosticString(prompt_len + *indent);
-        PrintCommandOutput(io_handler, diags, true);
+    auto DefaultPrintCallback = [&](const CommandReturnObject &result) {
+      // Display any inline diagnostics first.
+      const bool inline_diagnostics = !result.GetImmediateErrorStream() &&
+                                      GetDebugger().GetShowInlineDiagnostics();
+      if (inline_diagnostics) {
+        unsigned prompt_len = m_debugger.GetPrompt().size();
+        if (auto indent = result.GetDiagnosticIndent()) {
+          std::string diags =
+              result.GetInlineDiagnosticString(prompt_len + *indent);
+          PrintCommandOutput(io_handler, diags, true);
+        }
       }
-    }
 
-    // Display any STDOUT/STDERR _prior_ to emitting the command result text.
-    GetProcessOutput();
+      // Display any STDOUT/STDERR _prior_ to emitting the command result text.
+      GetProcessOutput();
 
-    if (!result.GetImmediateOutputStream()) {
-      llvm::StringRef output = result.GetOutputString();
-      PrintCommandOutput(io_handler, output, true);
-    }
+      if (!result.GetImmediateOutputStream()) {
+        llvm::StringRef output = result.GetOutputString();
+        PrintCommandOutput(io_handler, output, true);
+      }
 
-    // Now emit the command error text from the command we just executed.
-    if (!result.GetImmediateErrorStream()) {
-      std::string error = result.GetErrorString(!inline_diagnostics);
-      PrintCommandOutput(io_handler, error, false);
+      // Now emit the command error text from the command we just executed.
+      if (!result.GetImmediateErrorStream()) {
+        std::string error = result.GetErrorString(!inline_diagnostics);
+        PrintCommandOutput(io_handler, error, false);
+      }
+    };
+
+    if (m_print_callback) {
+      void *baton = m_print_callback_baton_sp
+                        ? m_print_callback_baton_sp->data()
+                        : nullptr;
+      lldb::CommandReturnObjectCallbackResult callback_result =
+          m_print_callback(result, baton);
+      if (callback_result == eCommandReturnObjectPrintCallbackSkipped)
+        DefaultPrintCallback(result);
+    } else {
+      DefaultPrintCallback(result);
     }
   }
 
@@ -3656,3 +3670,9 @@ llvm::json::Value CommandInterpreter::GetStatistics() {
 const StructuredData::Array &CommandInterpreter::GetTranscript() const {
   return m_transcript;
 }
+
+void CommandInterpreter::SetPrintCallback(CommandReturnObjectCallback callback,
+                                          lldb::BatonSP baton_sp) {
+  m_print_callback = callback;
+  m_print_callback_baton_sp = baton_sp;
+}
diff --git a/lldb/source/Interpreter/CommandReturnObject.cpp b/lldb/source/Interpreter/CommandReturnObject.cpp
index b99b2bc7b36ce4..0a2948e8e6ca44 100644
--- a/lldb/source/Interpreter/CommandReturnObject.cpp
+++ b/lldb/source/Interpreter/CommandReturnObject.cpp
@@ -147,7 +147,8 @@ void CommandReturnObject::SetError(llvm::Error error) {
   }
 }
 
-std::string CommandReturnObject::GetInlineDiagnosticString(unsigned indent) {
+std::string
+CommandReturnObject::GetInlineDiagnosticString(unsigned indent) const {
   StreamString diag_stream(m_colors);
   RenderDiagnosticDetails(diag_stream, indent, true, m_diagnostics);
   // Duplex the diagnostics to the secondary stream (but not inlined).
@@ -157,7 +158,7 @@ std::string CommandReturnObject::GetInlineDiagnosticString(unsigned indent) {
   return diag_stream.GetString().str();
 }
 
-std::strin...
[truncated]

@JDevlieghere
Copy link
Member Author

I recommend reviewing the last commit and ignoring the two NFC commits, which I will land separately anyway.

@jimingham
Copy link
Collaborator

Man there's a lot of boiler plate in hooking up to SWIG... But this all looks right to me.

I wondered whether the callback would benefit from knowing whether the result had been dumped immediately or not, but I can't think of a good reason for that.

If this result printing callback was also passed the command string (which it has from just above where the callback is called) then this would also be an easy way to do logging of command execution. That's not needed for your current purpose, but would add another strong motivation for this change...

@JDevlieghere
Copy link
Member Author

If this result printing callback was also passed the command string (which it has from just above where the callback is called) then this would also be an easy way to do logging of command execution. That's not needed for your current purpose, but would add another strong motivation for this change...

Adrian made a similar observation offline. What do you think about putting that in the CommandReturnObject? This seems generally useful and that way it's not tied to the callback.

Copy link
Collaborator

@labath labath left a comment

Choose a reason for hiding this comment

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

It's not clear from the patch description whether you actually need the ability to suppress the command output, or if you just want the ability to "access" the diagnostics. Because, if its the latter, then perhaps an (async) event would be a slightly more lightweight alternative?

Comment on lines 778 to 779
BatonSP baton_sp =
std::make_shared<CommandPrintCallbackBaton>(callback, baton);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I know this is here as a form of dependency inversion, and that we have this Baton thingy already, but it all fields unnecessarily elaborate. Couldn't the private callback type be a std::function<CommandReturnObjectCallbackResult(CommandReturnObject&)>, and just wrap things in a lambda?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, I was mimicking what we do for breakpoint callbacks. I suspect we could simplify that with the same trick. Updated the PR.

# Test registering a callback that handles the printing. Make sure the
# result is passed to the callback and that we don't print the result.
def handling_callback(return_object):
nonlocal called
Copy link
Collaborator

Choose a reason for hiding this comment

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

TIL

@JDevlieghere
Copy link
Member Author

It's not clear from the patch description whether you actually need the ability to suppress the command output, or if you just want the ability to "access" the diagnostics. Because, if its the latter, then perhaps an (async) event would be a slightly more lightweight alternative?

Yes, part of the requirement is that the output isn't printed by lldb itself, although the implementer is still in control with the return value.

@jimingham
Copy link
Collaborator

If this result printing callback was also passed the command string (which it has from just above where the callback is called) then this would also be an easy way to do logging of command execution. That's not needed for your current purpose, but would add another strong motivation for this change...

Adrian made a similar observation offline. What do you think about putting that in the CommandReturnObject? This seems generally useful and that way it's not tied to the callback.

That's a good idea.

@JDevlieghere
Copy link
Member Author

Alright, I have two follow-ups/action items:

  • Check if we can improve the breakpoint callback with a lambda.
  • Add the command string to CommandReturnObject.

JDevlieghere added a commit that referenced this pull request Jan 30, 2025
Fix CommandInterpreter.{h,cpp} formatting in preparation for #125006.
JDevlieghere added a commit that referenced this pull request Jan 30, 2025
There's no reason these methods cannot be `const`. Currently this
prevents us from passing around a const ref. This patch is in
preparation for #125006.
Xcode uses a pseudoterminal for the debugger console.

 - The upside of this apporach is that it means that it can rely on
   LLDB's IOHandlers for multiline and script input.

 - The downside of this approach is that the command output is printed
   to the PTY and you don't get a SBCommandReturnObject. Adrian added
   support for inline diagnostics in llvm#110901 and we'd like to access
   those from the IDE.

This patch adds support for registering a callback in the command
interpreter that gives access to the (SB)CommandReturnObject right
before it will be printed. The callback implementation can choose
whether it likes to handle printing the result or defer to lldb. If the
callback indicated it handled the result, the command interpreter will
skip printing the result.

We considered a few other alternatives to solve this problem:

 - The most obvious one is using `HandleCommand`, which returns a
   `SBCommandReturnObject`. The problem with this approach is the
   multiline input mentioned above. We would need a way to tell the IDE
   that it should expect multiline input, which isn't known until LLDB
   starts handling the command.

 - To address the multiline issue,we considered exposing (some of the)
   IOHandler machinery through the SB API. To solve this particular
   issue, that would require reimplementing a ton of logic that already
   exists today in the CommandInterpeter. Furthermore that seems like
   overkill compared to the proposed solution.

rdar://141254310
@JDevlieghere JDevlieghere force-pushed the command-interpreter-callback branch from 869a1c2 to e6d04c1 Compare January 30, 2025 18:49
@JDevlieghere
Copy link
Member Author

JDevlieghere commented Jan 30, 2025

Landed the NFC changes and rebased the PR.

Copy link
Collaborator

@labath labath left a comment

Choose a reason for hiding this comment

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

Yes, part of the requirement is that the output isn't printed by lldb itself, although the implementer is still in control with the return value.

Okay. I was afraid of that. :)

The code looks good, though I must admit that the functionality seems a bit.. random. Like, you're putting lldb in a terminal, but then you don't want it to behave like lldb in a terminal. I don't suppose there's anything you can say about what you intend to do with it?

@jimingham
Copy link
Collaborator

jimingham commented Jan 31, 2025

Yes, part of the requirement is that the output isn't printed by lldb itself, although the implementer is still in control with the return value.

Okay. I was afraid of that. :)

The code looks good, though I must admit that the functionality seems a bit.. random. Like, you're putting lldb in a terminal, but then you don't want it to behave like lldb in a terminal. I don't suppose there's anything you can say about what you intend to do with it?

One use case is that a UI might want to make a nicer presentation for some class of errors - for in particular the expression syntax errors that have more rich structure - making them disclosable or whatnot. Since those can be huge - particularly compile errors from expressions - you really don't want to duplicate the printing in that case.

The way Xcode presents lldb is that it intermixes the tty output from RunCommandInterpreter with process I/O and the output from Darwin's system logging into segmented bins in the console output. They want the input part of the TTY interface to work just like lldb at the command line - particularly they don't want to have to reimplement the modal IOHandlers for script, expr with no arguments, etc. But then since they are already parcelling the output into bins in their segmented presentation of the output, it would be straightforward for them to take a structured error output (of the sort Adrian has been working on) and render it in a more active form in the segment they were going to use for the output text that would have resulted from the command.

@jimingham
Copy link
Collaborator

jimingham commented Jan 31, 2025

There are other ways you could extend this that would be very fun. For instance, we could make any command that conses up its result just by calling the ValueObjectPrinter on some ValueObject to also stuff that ValueObject into the command return. Then we could add SBCommandResultObject::GetReturnValueObject to return that ValueObject. Then the callback could check whether there was a ValueObject in the command result, and if there is it would suppress that textual output and render the ValueObject as a live object in the result slot in the command output it is building up.

Note, when we get to this, we probably want to allow the callback to say "I take over all ValueObject printing" so that internally we wouldn't waste time making up the rendered result.

@JDevlieghere
Copy link
Member Author

Jim & I have been discussing this offline. Thanks for adding the additional context to the PR!

@jimingham
Copy link
Collaborator

jimingham commented Jan 31, 2025

We have a bunch of other structured output that we could start decorating this way. For instance if image list produced a a JSON-ified version of itself (maybe only when there was a callback present) and had a way to "get JSON-ified result" then the UI could render that list inline with active columns and rows that you could rearrange, sort by, etc.

Copy link
Collaborator

@labath labath left a comment

Choose a reason for hiding this comment

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

Thanks for the explanation. I understand what you mean now. The feature sounds cool, and the implementation sort of makes sense (sort of, because the HandleCommand idea is still "sort of" running around my head, but I do see how it could be very difficult to achieve that with it).

I do have my doubts about embedding the command string into the return object, but it sounds like the thought had occurred to at least two of you independently, so if you all think that's the best direction, then I guess I'm outvoted.

@JDevlieghere
Copy link
Member Author

I do have my doubts about embedding the command string into the return object, but it sounds like the thought had occurred to at least two of you independently, so if you all think that's the best direction, then I guess I'm outvoted.

I don't feel strongly about storing the command in the CommandReturnObject. It seemed like the natural place to put it and it wouldn't pollute the callback signature. I have nothing against passing in the command but I'd be worried that there might be other things needed in the future and I really don't want to have two callbacks. We could solve it by passing in an object but that needs to be bridged through the SB API which seems overkill for what I'm trying to do today.

Anyway, let's continue the discussion in #125132. We just branched for the 20.x release so we have plenty of time to refine the callback API if needed. If anyone (else) sees this and thinks about adopting it, this is your warning that it might change :-).

JDevlieghere added a commit to JDevlieghere/llvm-project that referenced this pull request Feb 3, 2025
As suggested in llvm#125006. Depending on which PR lands first, I'll update
TestCommandInterepterPrintCallback.py to check that the
CommandReturnObject passed to the callback has the correct command.
JDevlieghere added a commit that referenced this pull request Feb 4, 2025
As suggested in #125006. Depending on which PR lands first, I'll update
`TestCommandInterepterPrintCallback.py` to check that the
`CommandReturnObject` passed to the callback has the correct command.
@JDevlieghere JDevlieghere merged commit 97f6e53 into llvm:main Feb 4, 2025
7 checks passed
@JDevlieghere JDevlieghere deleted the command-interpreter-callback branch February 4, 2025 17:02
JDevlieghere added a commit to swiftlang/llvm-project that referenced this pull request Feb 5, 2025
There's no reason these methods cannot be `const`. Currently this
prevents us from passing around a const ref. This patch is in
preparation for llvm#125006.

(cherry picked from commit 7469032)
JDevlieghere added a commit to swiftlang/llvm-project that referenced this pull request Feb 5, 2025
As suggested in llvm#125006. Depending on which PR lands first, I'll update
`TestCommandInterepterPrintCallback.py` to check that the
`CommandReturnObject` passed to the callback has the correct command.

(cherry picked from commit 906eeed)
JDevlieghere added a commit to swiftlang/llvm-project that referenced this pull request Feb 5, 2025
Xcode uses a pseudoterminal for the debugger console.

- The upside of this apporach is that it means that it can rely on
LLDB's IOHandlers for multiline and script input.
- The downside of this approach is that the command output is printed to
the PTY and you don't get a SBCommandReturnObject. Adrian added support
for inline diagnostics (llvm#110901) and we'd like to access those from the
IDE.

This patch adds support for registering a callback in the command
interpreter that gives access to the `(SB)CommandReturnObject` right
before it will be printed. The callback implementation can choose
whether it likes to handle printing the result or defer to lldb. If the
callback indicated it handled the result, the command interpreter will
skip printing the result.

We considered a few other alternatives to solve this problem:

- The most obvious one is using `HandleCommand`, which returns a
`SBCommandReturnObject`. The problem with this approach is the multiline
input mentioned above. We would need a way to tell the IDE that it
should expect multiline input, which isn't known until LLDB starts
handling the command.
- To address the multiline issue,we considered exposing (some of the)
IOHandler machinery through the SB API. To solve this particular issue,
that would require reimplementing a ton of logic that already exists
today in the CommandInterpeter. Furthermore that seems like overkill
compared to the proposed solution.

rdar://141254310
(cherry picked from commit 97f6e53)
Icohedron pushed a commit to Icohedron/llvm-project that referenced this pull request Feb 11, 2025
As suggested in llvm#125006. Depending on which PR lands first, I'll update
`TestCommandInterepterPrintCallback.py` to check that the
`CommandReturnObject` passed to the callback has the correct command.
Icohedron pushed a commit to Icohedron/llvm-project that referenced this pull request Feb 11, 2025
Xcode uses a pseudoterminal for the debugger console.

- The upside of this apporach is that it means that it can rely on
LLDB's IOHandlers for multiline and script input.
- The downside of this approach is that the command output is printed to
the PTY and you don't get a SBCommandReturnObject. Adrian added support
for inline diagnostics (llvm#110901) and we'd like to access those from the
IDE.

This patch adds support for registering a callback in the command
interpreter that gives access to the `(SB)CommandReturnObject` right
before it will be printed. The callback implementation can choose
whether it likes to handle printing the result or defer to lldb. If the
callback indicated it handled the result, the command interpreter will
skip printing the result.

We considered a few other alternatives to solve this problem:

- The most obvious one is using `HandleCommand`, which returns a
`SBCommandReturnObject`. The problem with this approach is the multiline
input mentioned above. We would need a way to tell the IDE that it
should expect multiline input, which isn't known until LLDB starts
handling the command.
- To address the multiline issue,we considered exposing (some of the)
IOHandler machinery through the SB API. To solve this particular issue,
that would require reimplementing a ton of logic that already exists
today in the CommandInterpeter. Furthermore that seems like overkill
compared to the proposed solution.

rdar://141254310
JDevlieghere added a commit to JDevlieghere/llvm-project that referenced this pull request Feb 18, 2025
There are a lot of lldb commands whose result is really a ValueObject
that we then print with the ValueObjectPrinter. Now that we have the
ability to access the SBCommandReturnObject through a callback
(llvm#125006), we can store the resultant ValueObject in the return object,
allowing an IDE  to access the SBValue and do its own rich formatting.

rdar://143965453
JDevlieghere added a commit to JDevlieghere/llvm-project that referenced this pull request Feb 18, 2025
There are a lot of lldb commands whose result is really a ValueObject
that we then print with the ValueObjectPrinter. Now that we have the
ability to access the SBCommandReturnObject through a callback
(llvm#125006), we can store the resultant ValueObject in the return object,
allowing an IDE  to access the SBValue and do its own rich formatting.

rdar://143965453
JDevlieghere added a commit that referenced this pull request Feb 19, 2025
There are a lot of lldb commands whose result is really one or more
ValueObjects that we then print with the ValueObjectPrinter. Now that we
have the ability to access the SBCommandReturnObject through a callback
(#125006), we can store the resultant ValueObjects in the return object,
allowing an IDE to access the SBValues and do its own rich formatting.

rdar://143965453
JDevlieghere added a commit to swiftlang/llvm-project that referenced this pull request Feb 19, 2025
…127566)

There are a lot of lldb commands whose result is really one or more
ValueObjects that we then print with the ValueObjectPrinter. Now that we
have the ability to access the SBCommandReturnObject through a callback
(llvm#125006), we can store the resultant ValueObjects in the return object,
allowing an IDE to access the SBValues and do its own rich formatting.

rdar://143965453

 Conflicts:
	lldb/include/lldb/Interpreter/CommandReturnObject.h
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants