Skip to content

Add SBDebugger:: AddNotificationCallback API #111206

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

jeffreytan81
Copy link
Contributor

@jeffreytan81 jeffreytan81 commented Oct 4, 2024

This PR adds new SBDebugger::AddNotificationCallback()\RemoveNotificationCallback() APIs.

This API enables any lldb client to subscribe to interesting events in debugger lifecycle. For this PR, I am supporting two notification types(eDebuggerWillBeCreated and eDebuggerWillBeDestroyed). Future PRs can extend and add more types as desired.

@jeffreytan81 jeffreytan81 marked this pull request as ready for review October 4, 2024 20:34
@llvmbot llvmbot added the lldb label Oct 4, 2024
@llvmbot
Copy link
Member

llvmbot commented Oct 4, 2024

@llvm/pr-subscribers-lldb

Author: None (jeffreytan81)

Changes

This PR adds new SBDebugger::AddCreateCallback()/RemoveCreateCallback() APIs.

This API enables any lldb client to subscribe to debugger creation and perform action on it. For example, we have telemetry code that uses SBDebugger::AddDestroyCallback() to register debugger termination callback to collect statistics dump. We would like to use SBDebugger::AddCreateCallback() so that we can register telemetry reporting for all debugger instances not only the first one.


Full diff: https://github.com/llvm/llvm-project/pull/111206.diff

11 Files Affected:

  • (modified) lldb/bindings/python/python-swigsafecast.swig (+5)
  • (modified) lldb/bindings/python/python-typemaps.swig (+19)
  • (modified) lldb/bindings/python/python-wrapper.swig (+20)
  • (modified) lldb/include/lldb/API/SBDebugger.h (+10)
  • (modified) lldb/include/lldb/API/SBDefines.h (+2)
  • (modified) lldb/include/lldb/Core/Debugger.h (+32-9)
  • (modified) lldb/include/lldb/lldb-private-types.h (+2)
  • (modified) lldb/source/API/SBDebugger.cpp (+20)
  • (modified) lldb/source/Core/Debugger.cpp (+39-1)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h (+3)
  • (modified) lldb/test/API/python_api/debugger/TestDebuggerAPI.py (+22)
diff --git a/lldb/bindings/python/python-swigsafecast.swig b/lldb/bindings/python/python-swigsafecast.swig
index 0127ac6bfa4681..dc2b9c0ca69693 100644
--- a/lldb/bindings/python/python-swigsafecast.swig
+++ b/lldb/bindings/python/python-swigsafecast.swig
@@ -133,5 +133,10 @@ PythonObject SWIGBridge::ToSWIGWrapper(
   return ToSWIGHelper(module_spec_sb.release(), SWIGTYPE_p_lldb__SBModuleSpec);
 }
 
+PythonObject SWIGBridge::ToSWIGWrapper(
+    std::unique_ptr<lldb::SBDebugger> debugger_sb) {
+  return ToSWIGHelper(debugger_sb.release(), SWIGTYPE_p_lldb__SBDebugger);
+}
+
 } // namespace python
 } // namespace lldb_private
diff --git a/lldb/bindings/python/python-typemaps.swig b/lldb/bindings/python/python-typemaps.swig
index f8c33e15c03e66..c61edc31bad26f 100644
--- a/lldb/bindings/python/python-typemaps.swig
+++ b/lldb/bindings/python/python-typemaps.swig
@@ -452,6 +452,25 @@ template <> bool SetNumberFromPyObject<double>(double &number, PyObject *obj) {
   $1 = $1 || PyCallable_Check(reinterpret_cast<PyObject *>($input));
 }
 
+// For lldb::SBDebuggerCreateCallback
+%typemap(in) (lldb::SBDebuggerCreateCallback create_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 = LLDBSwigPythonCallPythonSBDebuggerCreateCallback;
+  $2 = $input;
+}
+
+%typemap(typecheck) (lldb::SBDebuggerCreateCallback create_callback, void *baton) {
+  $1 = $input == Py_None;
+  $1 = $1 || PyCallable_Check(reinterpret_cast<PyObject *>($input));
+}
+
 // For lldb::SBDebuggerDestroyCallback
 %typemap(in) (lldb::SBDebuggerDestroyCallback destroy_callback, void *baton) {
   if (!($input == Py_None ||
diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig
index 810673aaec5d19..9eef0479290073 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -1024,6 +1024,26 @@ static void LLDBSwigPythonCallPythonLogOutputCallback(const char *str,
   }
 }
 
+// For DebuggerCreateCallback functions
+static void LLDBSwigPythonCallPythonSBDebuggerCreateCallback(lldb::SBDebugger &debugger,
+                                                      void *baton) {
+  if (baton != Py_None) {
+    SWIG_PYTHON_THREAD_BEGIN_BLOCK;
+    PythonObject debugger_arg = SWIGBridge::ToSWIGWrapper(
+      std::make_unique<SBDebugger>(debugger));
+
+    // Create a tuple of arguments
+    PyObject *args = PyTuple_New(1);
+    PyTuple_SetItem(args, 0, debugger_arg.get());
+
+    // Call the Python function
+    PyObject *result = PyObject_CallFunction(
+            reinterpret_cast<PyObject *>(baton), const_cast<char *>("O"), args);
+    Py_XDECREF(result);
+    SWIG_PYTHON_THREAD_END_BLOCK;
+  }
+}
+
 // For DebuggerTerminateCallback functions
 static void LLDBSwigPythonCallPythonSBDebuggerTerminateCallback(lldb::user_id_t debugger_id,
                                                       void *baton) {
diff --git a/lldb/include/lldb/API/SBDebugger.h b/lldb/include/lldb/API/SBDebugger.h
index 84ea9c0f772e16..cef5307418fa2d 100644
--- a/lldb/include/lldb/API/SBDebugger.h
+++ b/lldb/include/lldb/API/SBDebugger.h
@@ -336,6 +336,16 @@ class LLDB_API SBDebugger {
   void SetDestroyCallback(lldb::SBDebuggerDestroyCallback destroy_callback,
                           void *baton);
 
+  /// Add a callback for when the debugger is created. Return a token, which
+  /// can be used to remove said callback. Multiple callbacks can be added by
+  /// calling this function multiple times, and will be invoked in FIFO order.
+  static lldb::callback_token_t
+  AddCreateCallback(lldb::SBDebuggerCreateCallback create_callback,
+                    void *baton);
+
+  /// Remove the specified callback. Return true if successful.
+  static bool RemoveCreateCallback(lldb::callback_token_t token);
+
   /// Add a callback for when the debugger is destroyed. Return a token, which
   /// can be used to remove said callback. Multiple callbacks can be added by
   /// calling this function multiple times, and will be invoked in FIFO order.
diff --git a/lldb/include/lldb/API/SBDefines.h b/lldb/include/lldb/API/SBDefines.h
index 9543ebc08a320f..104bdc8c79f44c 100644
--- a/lldb/include/lldb/API/SBDefines.h
+++ b/lldb/include/lldb/API/SBDefines.h
@@ -139,6 +139,8 @@ typedef bool (*SBBreakpointHitCallback)(void *baton, lldb::SBProcess &process,
                                         lldb::SBThread &thread,
                                         lldb::SBBreakpointLocation &location);
 
+typedef void (*SBDebuggerCreateCallback)(lldb::SBDebugger &debugger,
+                                         void *baton);
 typedef void (*SBDebuggerDestroyCallback)(lldb::user_id_t debugger_id,
                                           void *baton);
 
diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index a72c2596cc2c5e..16e3298f7b1ed5 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -569,6 +569,16 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
   SetDestroyCallback(lldb_private::DebuggerDestroyCallback destroy_callback,
                      void *baton);
 
+  /// Add a callback for when the debugger is created. Return a token, which
+  /// can be used to remove said callback. Multiple callbacks can be added by
+  /// calling this function multiple times, and will be invoked in FIFO order.
+  static lldb::callback_token_t
+  AddCreateCallback(lldb_private::DebuggerCreateCallback create_callback,
+                    void *baton, void *original_callback);
+
+  /// Remove the specified callback. Return true if successful.
+  static bool RemoveCreateCallback(lldb::callback_token_t token);
+
   /// Add a callback for when the debugger is destroyed. Return a token, which
   /// can be used to remove said callback. Multiple callbacks can be added by
   /// calling this function multiple times, and will be invoked in FIFO order.
@@ -737,19 +747,32 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
   lldb::TargetSP m_dummy_target_sp;
   Diagnostics::CallbackID m_diagnostics_callback_id;
 
-  std::mutex m_destroy_callback_mutex;
-  lldb::callback_token_t m_destroy_callback_next_token = 0;
-  struct DestroyCallbackInfo {
-    DestroyCallbackInfo() {}
-    DestroyCallbackInfo(lldb::callback_token_t token,
-                        lldb_private::DebuggerDestroyCallback callback,
-                        void *baton)
+  template <typename T> struct CallbackInfo {
+    CallbackInfo() {}
+    CallbackInfo(lldb::callback_token_t token, T callback, void *baton)
         : token(token), callback(callback), baton(baton) {}
     lldb::callback_token_t token;
-    lldb_private::DebuggerDestroyCallback callback;
+    T callback;
     void *baton;
   };
-  llvm::SmallVector<DestroyCallbackInfo, 2> m_destroy_callbacks;
+  template <typename T, typename ExtraT>
+  struct CallbackInfoExtraData : public CallbackInfo<T> {
+    CallbackInfoExtraData() {}
+    CallbackInfoExtraData(lldb::callback_token_t token, T callback, void *baton,
+                          ExtraT extra_data)
+        : CallbackInfo<T>(token, callback, baton), extra_data(extra_data) {}
+    ExtraT extra_data;
+  };
+  static std::mutex s_create_callback_mutex;
+  static lldb::callback_token_t s_create_callback_next_token;
+  static llvm::SmallVector<
+      CallbackInfoExtraData<lldb_private::DebuggerCreateCallback, void *>, 2>
+      s_create_callbacks;
+
+  std::mutex m_destroy_callback_mutex;
+  lldb::callback_token_t m_destroy_callback_next_token = 0;
+  llvm::SmallVector<CallbackInfo<lldb_private::DebuggerDestroyCallback>, 2>
+      m_destroy_callbacks;
 
   uint32_t m_interrupt_requested = 0; ///< Tracks interrupt requests
   std::mutex m_interrupt_mutex;
diff --git a/lldb/include/lldb/lldb-private-types.h b/lldb/include/lldb/lldb-private-types.h
index b82a2b8aa05744..c997538f62827a 100644
--- a/lldb/include/lldb/lldb-private-types.h
+++ b/lldb/include/lldb/lldb-private-types.h
@@ -143,6 +143,8 @@ typedef struct type256 { uint64_t x[4]; } type256;
 using ValueObjectProviderTy =
     std::function<lldb::ValueObjectSP(ConstString, StackFrame *)>;
 
+typedef void (*DebuggerCreateCallback)(lldb::DebuggerSP debugger, void *baton,
+                                       void *original_callback);
 typedef void (*DebuggerDestroyCallback)(lldb::user_id_t debugger_id,
                                         void *baton);
 typedef bool (*CommandOverrideCallbackWithResult)(
diff --git a/lldb/source/API/SBDebugger.cpp b/lldb/source/API/SBDebugger.cpp
index 6b72994fc96afb..806415aa7b3de0 100644
--- a/lldb/source/API/SBDebugger.cpp
+++ b/lldb/source/API/SBDebugger.cpp
@@ -1703,6 +1703,26 @@ void SBDebugger::SetDestroyCallback(
   }
 }
 
+lldb::callback_token_t
+SBDebugger::AddCreateCallback(lldb::SBDebuggerCreateCallback create_callback,
+                              void *baton) {
+  LLDB_INSTRUMENT_VA(create_callback, baton);
+
+  DebuggerCreateCallback callback = [](lldb::DebuggerSP debugger, void *baton,
+                                       void *original_callback) {
+    SBDebugger sb_debugger(debugger);
+    lldb::SBDebuggerCreateCallback original_callback_func =
+        (lldb::SBDebuggerCreateCallback)original_callback;
+    original_callback_func(sb_debugger, baton);
+  };
+  return Debugger::AddCreateCallback(callback, baton, (void *)create_callback);
+}
+
+bool SBDebugger::RemoveCreateCallback(lldb::callback_token_t token) {
+  LLDB_INSTRUMENT_VA(token);
+  return Debugger::RemoveCreateCallback(token);
+}
+
 lldb::callback_token_t
 SBDebugger::AddDestroyCallback(lldb::SBDebuggerDestroyCallback destroy_callback,
                                void *baton) {
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index 9bdc5a3949751d..7fee7eee78f7c0 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -106,6 +106,13 @@ static Debugger::DebuggerList *g_debugger_list_ptr =
     nullptr; // NOTE: intentional leak to avoid issues with C++ destructor chain
 static llvm::DefaultThreadPool *g_thread_pool = nullptr;
 
+std::mutex Debugger::s_create_callback_mutex;
+lldb::callback_token_t Debugger::s_create_callback_next_token = 0;
+llvm::SmallVector<Debugger::CallbackInfoExtraData<
+                      lldb_private::DebuggerCreateCallback, void *>,
+                  2>
+    Debugger::s_create_callbacks;
+
 static constexpr OptionEnumValueElement g_show_disassembly_enum_values[] = {
     {
         Debugger::eStopDisassemblyTypeNever,
@@ -739,6 +746,15 @@ DebuggerSP Debugger::CreateInstance(lldb::LogOutputCallback log_callback,
     g_debugger_list_ptr->push_back(debugger_sp);
   }
   debugger_sp->InstanceInitialize();
+
+  // Invoke all debugger create callbacks.
+  {
+    std::lock_guard<std::mutex> guard(s_create_callback_mutex);
+    for (const auto &callback_info : s_create_callbacks) {
+      callback_info.callback(debugger_sp, callback_info.baton,
+                             callback_info.extra_data);
+    }
+  }
   return debugger_sp;
 }
 
@@ -748,7 +764,7 @@ void Debugger::HandleDestroyCallback() {
   // added during this loop will be appended, invoked and then removed last.
   // Callbacks which are removed during this loop will not be invoked.
   while (true) {
-    DestroyCallbackInfo callback_info;
+    CallbackInfo<DebuggerDestroyCallback> callback_info;
     {
       std::lock_guard<std::mutex> guard(m_destroy_callback_mutex);
       if (m_destroy_callbacks.empty())
@@ -1447,6 +1463,28 @@ void Debugger::SetDestroyCallback(
   m_destroy_callbacks.emplace_back(token, destroy_callback, baton);
 }
 
+lldb::callback_token_t Debugger::AddCreateCallback(
+    lldb_private::DebuggerCreateCallback create_callback, void *baton,
+    void *original_callback) {
+  std::lock_guard<std::mutex> guard(s_create_callback_mutex);
+  const lldb::callback_token_t token = s_create_callback_next_token++;
+  s_create_callbacks.emplace_back(token, create_callback, baton,
+                                  original_callback);
+  return token;
+}
+
+bool Debugger::RemoveCreateCallback(lldb::callback_token_t token) {
+  std::lock_guard<std::mutex> guard(s_create_callback_mutex);
+  for (auto it = s_create_callbacks.begin(); it != s_create_callbacks.end();
+       ++it) {
+    if (it->token == token) {
+      s_create_callbacks.erase(it);
+      return true;
+    }
+  }
+  return false;
+}
+
 lldb::callback_token_t Debugger::AddDestroyCallback(
     lldb_private::DebuggerDestroyCallback destroy_callback, void *baton) {
   std::lock_guard<std::mutex> guard(m_destroy_callback_mutex);
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
index 97a3837fd7aa62..f050fb3a214a6c 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
@@ -33,6 +33,7 @@ class SBStructuredData;
 class SBFileSpec;
 class SBModuleSpec;
 class SBStringList;
+class SBDebugger;
 } // namespace lldb
 
 namespace lldb_private {
@@ -111,6 +112,8 @@ class SWIGBridge {
   ToSWIGWrapper(std::unique_ptr<lldb::SBFileSpec> file_spec_sb);
   static PythonObject
   ToSWIGWrapper(std::unique_ptr<lldb::SBModuleSpec> module_spec_sb);
+  static PythonObject
+  ToSWIGWrapper(std::unique_ptr<lldb::SBDebugger> debugger_sb);
 
   static python::ScopedPythonObject<lldb::SBCommandReturnObject>
   ToSWIGWrapper(CommandReturnObject &cmd_retobj);
diff --git a/lldb/test/API/python_api/debugger/TestDebuggerAPI.py b/lldb/test/API/python_api/debugger/TestDebuggerAPI.py
index 646ccce36530d4..6c7433f2c93163 100644
--- a/lldb/test/API/python_api/debugger/TestDebuggerAPI.py
+++ b/lldb/test/API/python_api/debugger/TestDebuggerAPI.py
@@ -286,3 +286,25 @@ def remove_bar(dbg_id):
             ('remove bar ret', False), # remove_bar should fail, because it's already invoked and removed
             ('foo called', original_dbg_id), # foo should be called
         ])
+
+    def test_AddRemoveDebuggerCreateCallback(self):
+        """
+        Test SBDebugger::AddCreateCallback and SBDebugger::RemoveCreateCallback
+        """
+        created_debuggers = []
+        def debugger_create_callback(debugger):
+            created_debuggers.append(debugger)
+        
+        create_callback_token = lldb.SBDebugger.AddCreateCallback(debugger_create_callback)
+        debugger1 = lldb.SBDebugger.Create()
+        debugger2 = lldb.SBDebugger.Create()     
+        
+        lldb.SBDebugger.RemoveCreateCallback(create_callback_token)
+        debugger3 = lldb.SBDebugger.Create()
+        
+        self.assertNotEqual(debugger1.GetID(), debugger2.GetID())
+        self.assertNotEqual(debugger1.GetID(), debugger3.GetID())
+        
+        self.assertEqual(len(created_debuggers), 2)
+        self.assertEqual(debugger1.GetID(), created_debuggers[0].GetID())
+        self.assertEqual(debugger2.GetID(), created_debuggers[1].GetID())

Copy link

github-actions bot commented Oct 4, 2024

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

@jimingham
Copy link
Collaborator

I'm of two minds about this. OTOH, it is certainly useful to be able to observe Debugger creation. OTOH, it would be equally useful to be able to observe Target creation and Process creation and probably other "debugger lifecycle" events.

One of the tasks I've not had time to get to yet is to make a "debugger lifecycle" extension to "target stop-hook" where you can add hooks to all these other lifecycle events.

I worry a little bit that backing into such a system piecemeal will make the ultimate structure harder to implement.

@jimingham
Copy link
Collaborator

Also, if you are going to hook creation, you should also hook destruction, shouldn't you? Whatever accounting you are doing for a debugger's once it exists might need to be torn down when that debugger goes away.

Copy link
Collaborator

@clayborg clayborg left a comment

Choose a reason for hiding this comment

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

As Jim was saying, we might be able to make something a bit more useful so that we can easily extend for other notifications of a debug session lifetime. Maybe instead of:

typedef void (*SBDebuggerCreateCallback)(lldb::SBDebugger &debugger, void *baton);

We can add something like:

enum NotificationType {
  eDebuggerWasCreated,
  eDebuggerWillBeDestroyed, // Call before debugger object is destroyed
  eTargetWasCreated, 
  eTargetWillBeDestroyed,
  eProcess...
};
typedef void (*SBNotificationCallback)(lldb::NotificationType type, lldb::SBDebugger &, lldb::SBExecutionContext &exe_ctx, void *baton);

Then we can easily add new registration callbacks that all use the same callback function.

@jeffreytan81
Copy link
Contributor Author

Also, if you are going to hook creation, you should also hook destruction, shouldn't you? Whatever accounting you are doing for a debugger's once it exists might need to be torn down when that debugger goes away.

Right, SBDebugger::AddDestroyCallback() already exists; it was added by #89868

@jeffreytan81
Copy link
Contributor Author

@jimingham , @clayborg, do you guys prefer target stop-hook style approach or SBNotificationCallback style python API callback?

@clayborg, for SBNotificationCallback, does lldb::SBExecutionContext wrap the process, target or debugger object that is creating/destroying?

@clayborg
Copy link
Collaborator

clayborg commented Oct 7, 2024

@jimingham , @clayborg, do you guys prefer target stop-hook style approach or SBNotificationCallback style python API callback?

@clayborg, for SBNotificationCallback, does lldb::SBExecutionContext wrap the process, target or debugger object that is creating/destroying?

lldb::SBExecutionContext has target, process, thread and frame, but not debugger. We could add the lldb::SBDebugger to the lldb::SBExecutionContext if we want to avoid passing both the lldb::SBDebugger' and the 'lldb::SBExecutionContext'. But right now lldb::SBExecutionContext` doesn't have a debugger.

But using lldb::SBExecutionContext will allow us to make notifications for just about anything (debugger, target, process, thread, frame lifetime events).

@clayborg
Copy link
Collaborator

clayborg commented Oct 7, 2024

I have wanted to be able to hook into target creation to be able to setup targets when they get created. It would be also nice to get an event when a process is not longer being debugged due to process exit or detach.

@jimingham
Copy link
Collaborator

jimingham commented Oct 7, 2024 via email

@labath
Copy link
Collaborator

labath commented Oct 8, 2024

Hello all,

I'm parachuting in here because we also have a use case that might benefit from something like this. In our case, we want to run some actions (set up data formatters, source paths, etc.) if we find that we're working with a certain kind of binary (which contains information on how to find these). We're currently using a combination of "module loaded" events and stop hooks for this, but neither of them is ideal because the events are asynchronous (so lldb doesn't wait for our processing to finish), and stop hooks happen slightly too late. Ideally, we'd like our hook to run (synchronously) after target creation (or after its executable has been set).

I don't really have much to say about the implementation except a note that a one-callback-to-rule-them all might not be ideal for the case where one wants to hook only a certain kind of events. Sure, you can do filtering in the callback, but it means you still get called, which may matter if some of the events occur frequently. This probably doesn't matter for target creation, but it may not be ideal to get called e.g. after every process stop if all you wanted to hook was debugger creation.

@jimingham
Copy link
Collaborator

jimingham commented Oct 8, 2024 via email

@jimingham
Copy link
Collaborator

jimingham commented Oct 8, 2024 via email

@jeffreytan81 jeffreytan81 force-pushed the add_debugger_create_callback branch from 4f11996 to d5820bd Compare October 8, 2024 19:10
@jeffreytan81 jeffreytan81 force-pushed the add_debugger_create_callback branch from d5820bd to f2d35f6 Compare October 8, 2024 19:21
@jeffreytan81
Copy link
Contributor Author

The PR is revised based on feedback. In this PR, it supports eDebuggerWillBeCreated and eDebuggerWillBeDestroyed notifications. We can add more in future. The callback is only called if the type filtering matches (for example, the client asks for eDebuggerWillBeCreated, it won't invoke the callback if eDebuggerWillBeDestroyed notification is invoked.

Let me know if this makes sense.

@jeffreytan81 jeffreytan81 changed the title Add SBDebugger::AddCreateCallback API Add SBDebugger:: AddNotificationCallback API Oct 8, 2024
@@ -1362,6 +1362,12 @@ enum Severity {
eSeverityInfo, // Equivalent to Remark used in clang.
};

enum NotificationType {
eDebuggerWillBeCreated = (1 << 0),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this be eDebuggerWasCreated to indicate that a debugger object was created and that the user can do something with it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, it's not useful to call this before the new Debugger is fully initialized.

Comment on lines +142 to +143
lldb::SBDebugger &,
lldb::SBExecutionContext &exe_ctx,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we add accessors to the SBExecutionContext for accessing a debugger? We can modify the lldb_private::ExecutionContext to add a lldb::DebuggerSP and add accessors to the debugger object and then add that to the `lldb::SBExecutionContext" API?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's always been a little weird that ExecutionContext didn't have a debugger - though TTTT that only mattered for default constructed ones: so long as any of the other entities are filled in you can get to the debugger.
OTOH, that's a pretty big patch so if we want to go that way, that should be a separate patch.
Another way to do this would be to add an SBExecutionContext::GetDebugger and manage that all on the SB side? After all, we haven't actually needed a Debugger in the ExecutionContext that I can remember, and this would be a much more limited change.
I'm also fine with passing the debugger alongside the SBExecutionContext, we do that in some other places.

@@ -569,6 +569,18 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
SetDestroyCallback(lldb_private::DebuggerDestroyCallback destroy_callback,
void *baton);

/// Add a notification callback when notification type event happens. Return a
Copy link
Collaborator

Choose a reason for hiding this comment

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

Read literally this says you are adding the notification callback when the event happens. Maybe:

Add a notification callback that will trigger when an event of the given NotificationType occurs.

You should also state when the creation and destruction callbacks trigger w.r.t. the action the event is reporting. I.e. Creation callbacks trigger synchronously after the given object is fully set up, and Destruction callbacks trigger right before the object is destroyed.

/// Add a notification callback when notification type event happens. Return a
/// token, which can be used to remove said callback. Multiple callbacks can
/// be added by calling this function multiple times, and will be invoked in
/// FIFO order.
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is original_callback for? You should document this if it's needed.

@@ -1362,6 +1362,12 @@ enum Severity {
eSeverityInfo, // Equivalent to Remark used in clang.
};

enum NotificationType {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might be good to put a comment here saying (a) this is used for SBNotificationCallbacks and (b) this is where you should add any other notification callbacks (i.e. target creation and destruction...) That should be made clear so we don't invent another mechanism for this elsewhere...

@jimingham
Copy link
Collaborator

jimingham commented Oct 8, 2024

I couldn't see what the original_callback argument and ivar are for? They just seem to get copied around but not used?

We should also add a command way to add these callbacks, but it doesn't make sense to do that for the debugger created and destroyed ones because the command interpreter really lives inside of one debugger. So I don't think you need to do that in this patch.

};
static std::mutex s_notification_callback_mutex;
static lldb::callback_token_t s_notification_callback_next_token;
static llvm::SmallVector<
Copy link
Collaborator

Choose a reason for hiding this comment

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

Every action we perform with the notifications except for "deleting by token" knows the type of the notification that it's adding/looking up. We could even encode the type in the token and we'd always know. So it seems awkward to store them in a flat list. When we get around to listing these and other management tasks, those will very likely also be requests by type.
Maybe storing them in a map of type -> vector of callbacks for that type would be a better data structure here?

@labath
Copy link
Collaborator

labath commented Oct 10, 2024

(This is in reply to Jim's comment here. I can't quote it, because github garbles it beyond recognition, probably because it was sent by email :/)

You're right, I have misunderstood the intention. The thing that threw me off was the discussion of about the callback signature (which still looks like it wants to be universal). Given that, I'd like to agree with what you said in the last part of your email -- I think it'd be better for the callback signatures to not be universal as well (no one-signature-to-rule-them-all). Here are my reasons for that:

  • we sidestep the "should execution context contain a debugger" question -- the debugger callback type can just take a debugger argument, and that's it. A thread callback type can take an SBThread instead of getting an SBExecutionContext and wondering what's inside it.
  • we sidestep the "what is the scope of lower-level callback types" question -- it's clear that debugger lifetime callbacks have to be global, but for example, I think it would be reasonable to have e.g. target creation callbacks scoped to the debugger they are being created in (so you can monitor targets in one debugger but not interfere with other debuggers you possibly know nothing about). With the current design we're basically committing to global callbacks, but with the other one, we leave the option open.
  • we can pass event-specific data to the callbacks. For example, for my use case, the ideal event type would be "module got added to a target" (basically, a synchronous version of (eBroadcastBitModulesLoaded) or "target's executable module has changed". Fitting that into a universal callback type would be very tricky.

If we do this right, I don't believe this should result in a lot of duplicated code, as we could write (template?) code to handle the (de)registration in a generic way.

enum NotificationType {
eDebuggerWillBeCreated = (1 << 0),
eDebuggerWillBeDestroyed =
(1 << 1), // Call before debugger object is destroyed
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we actually want to treat this as a bitfield? If not, maybe they should just get the usual sequential numbers?

original_callback_func(type, sb_debugger, sb_exe_ctx, baton);
};
return Debugger::AddNotificationCallback(type, callback, baton,
(void *)notification_callback);
Copy link
Collaborator

Choose a reason for hiding this comment

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

same here

Comment on lines +1716 to +1717
lldb::SBNotificationCallback original_callback_func =
(lldb::SBNotificationCallback)original_callback;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
lldb::SBNotificationCallback original_callback_func =
(lldb::SBNotificationCallback)original_callback;
auto original_callback_func =
reinterpret_cast<lldb::SBNotificationCallback>(original_callback);

(use c++ cast, and avoid repeating the type)

std::lock_guard<std::mutex> guard(s_notification_callback_mutex);
for (const auto &callback_info : s_notification_callbacks) {
if ((callback_info.type & notify_type) == notify_type)
callback_info.callback(notify_type, debugger_sp, nullptr,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note this will deadlock if any of the callbacks try to register another callback.

You may want to consider making a copy of the list and iterating over that (without holding the mutex).

Comment on lines +1042 to +1061
// Create a tuple of arguments (type, debugger, exe_ctx)
PyObject *args = PyTuple_New(3);

// Add NotificationType as an integer to the tuple
PyTuple_SetItem(args, 0, PyLong_FromLong(static_cast<long>(type)));

// Add debugger and exe_ctx to the tuple
Py_INCREF(debugger_arg.get());
PyTuple_SetItem(args, 1, debugger_arg.get());

Py_INCREF(exe_ctx_arg.get());
PyTuple_SetItem(args, 2, exe_ctx_arg.get());

// Call the Python function with the tuple of arguments (type, debugger, exe_ctx)
PyObject *result = PyObject_CallFunction(
reinterpret_cast<PyObject *>(baton), const_cast<char *>("O"), args);

// Clean up
Py_XDECREF(result);
Py_DECREF(args); // Decrement reference count for args
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could this use PythonObject wrappers instead?

Something like PythonCallable(baton)(PythonInteger(type), debugger_arg, exe_ctx_arg) ?

void Debugger::HandleDestroyCallback() {
const lldb::user_id_t user_id = GetID();
// Invoke and remove all the callbacks in an FIFO order. Callbacks which are
// added during this loop will be appended, invoked and then removed last.
// Callbacks which are removed during this loop will not be invoked.
while (true) {
DestroyCallbackInfo callback_info;
CallbackInfo<DebuggerDestroyCallback> callback_info;
Copy link
Collaborator

Choose a reason for hiding this comment

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

How about treating the destroy callbacks as (legacy) synonyms for NotificationCallbacks of the "destroy" kind?

Basically, have SBDebugger::AddDestroyCallback do a AddNotificationCallback(eDebuggerWillBeDestroyed, callback, baton)

Then we could delete all of this code...

@jimingham
Copy link
Collaborator

(This is in reply to Jim's comment here. I can't quote it, because github garbles it beyond recognition, probably because it was sent by email :/)

I'm trying to remember not to believe the bit where the footer in the email says you can reply to it directly. But it's SO convenient...

You're right, I have misunderstood the intention. The thing that threw me off was the discussion of about the callback signature (which still looks like it wants to be universal). Given that, I'd like to agree with what you said in the last part of your email -- I think it'd be better for the callback signatures to not be universal as well (no one-signature-to-rule-them-all). Here are my reasons for that:

  • we sidestep the "should execution context contain a debugger" question -- the debugger callback type can just take a debugger argument, and that's it. A thread callback type can take an SBThread instead of getting an SBExecutionContext and wondering what's inside it.

I don't think we need to have very many of these signatures, but I can think of more than just ExecutionContext ones pretty easily. Your example of Symbol side ones later on is the other really obvious example. And as you say, notifications are fine if you don't need to do something before the rest of the system sees the change, but sometimes you very much want to do that, particularly for module loading. So having a callback for this makes a lot of sense.

It also seems to me that Platform lifecycle events would be something you might want callbacks for, and they don't neatly fit in the ExecutionContext mold.

It's worthwhile to have a unified system for this. For instance, sometimes you want to observe and record information about some change, but you really don't want to junk up the console output with the results of that information. So having a way for the callback to produce a stored result, and then some commands to say "show me the stored results for callback X" or even "show me the stored results for all callbacks between StopID 100 and StopID 200, etc. would be handy. It would be sad if callback writers had to roll their own version of this sort of convenience...

  • we sidestep the "what is the scope of lower-level callback types" question -- it's clear that debugger lifetime callbacks have to be global, but for example, I think it would be reasonable to have e.g. target creation callbacks scoped to the debugger they are being created in (so you can monitor targets in one debugger but not interfere with other debuggers you possibly know nothing about). With the current design we're basically committing to global callbacks, but with the other one, we leave the option open.

Yes, particularly in environments like Xcode where all the concurrent debug session to a device share one lldb, it would be useful to scope these at least to the debugger. I think it's worth figuring out a convenient way to do this.

  • we can pass event-specific data to the callbacks. For example, for my use case, the ideal event type would be "module got added to a target" (basically, a synchronous version of (eBroadcastBitModulesLoaded) or "target's executable module has changed". Fitting that into a universal callback type would be very tricky.

If we do this right, I don't believe this should result in a lot of duplicated code, as we could write (template?) code to handle the (de)registration in a generic way.

Registration should just require a callback type, that part should be easy. To do this in a generic manner a callback entity would also need a filter, and a "callback-hit-instance-data". The filter would take the instance data, but and get a yes or no back. Other than that the generic callback management shouldn't need to reason about either of these bits, just pass them to the filter and the callback. This should be amenable to a generic approach.

@cmtice
Copy link
Contributor

cmtice commented Oct 16, 2024

I have wanted to be able to hook into target creation to be able to setup targets when they get created. It would be also nice to get an event when a process is not longer being debugged due to process exit or detach.

Something I have wanted for a long time is a stop hook just BEFORE the target is created, to that I can change the setting for target.debug-file-search-paths before LLDB tries to read all the debug info (that it can't find because the search path setting hasn't been properly set up for us).

@jimingham
Copy link
Collaborator

jimingham commented Oct 17, 2024 via email

DestroyCallbackInfo(lldb::callback_token_t token,
lldb_private::DebuggerDestroyCallback callback,
void *baton)
template <typename T> struct CallbackInfo {
Copy link
Contributor

Choose a reason for hiding this comment

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

IIUC, CallbackInfo is the base template callback class for all kinds of callback (debugger create/destroy, and some other callbacks to be added in the future). It's better to place it somewhere else to make easier to extended in the future.

@ZequanWu
Copy link
Contributor

Is there any update on this PR? I'm interested in working on a similar callback that can be triggered when target is created, but I want to wait for this one to get landed first as they will share some common code.

@jimingham
Copy link
Collaborator

There were a bunch of review comments that need to be addressed. But I also am a bit nervous about adding a feature that is intended to be a general "lldb interesting classes lifecycle notification" but when designed only supports Debugger Lifecycle Events. Those are the simplest and least common of all the events we'd want to handle.

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.

7 participants