[Lldb-commits] [lldb] [LLDB] Add module hook implementation (PR #185465)

via lldb-commits lldb-commits at lists.llvm.org
Tue Apr 7 16:45:16 PDT 2026


================
@@ -5328,6 +5328,606 @@ class CommandObjectMultiwordTargetStopHooks : public CommandObjectMultiword {
   ~CommandObjectMultiwordTargetStopHooks() override = default;
 };
 
+#pragma mark CommandObjectTargetHookAdd
+
+#define LLDB_OPTIONS_target_hook_add
+#include "CommandOptions.inc"
+
+class CommandObjectTargetHookAdd : public CommandObjectParsed,
+                                   public IOHandlerDelegateMultiline {
+public:
+  class CommandOptions : public OptionGroup {
+  public:
+    CommandOptions() = default;
+    ~CommandOptions() override = default;
+
+    llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
+      return llvm::ArrayRef(g_target_hook_add_options);
+    }
+
+    Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
+                          ExecutionContext *execution_context) override {
+      Status error;
+      const int short_option =
+          g_target_hook_add_options[option_idx].short_option;
+      switch (short_option) {
+      case 'o':
+        m_use_one_liner = true;
+        m_one_liner.push_back(std::string(option_arg));
+        break;
+      case 'L':
+        m_on_load = true;
+        break;
+      case 'u':
+        m_on_unload = true;
+        break;
+      case 'S':
+        m_on_stop = true;
+        break;
+      case 's':
+        m_module_name = std::string(option_arg);
+        m_sym_ctx_specified = true;
+        break;
+      case 'x': {
+        uint32_t thread_index;
+        if (option_arg.getAsInteger(0, thread_index))
+          error = Status::FromErrorStringWithFormat("invalid thread index '%s'",
+                                                    option_arg.str().c_str());
+        else
+          m_thread_index = thread_index;
+        m_thread_specified = true;
+        break;
+      }
+      case 't': {
+        lldb::tid_t thread_id;
+        if (option_arg.getAsInteger(0, thread_id))
+          error = Status::FromErrorStringWithFormat("invalid thread id '%s'",
+                                                    option_arg.str().c_str());
+        else
+          m_thread_id = thread_id;
+        m_thread_specified = true;
+        break;
+      }
+      case 'T':
+        m_thread_name = std::string(option_arg);
+        m_thread_specified = true;
+        break;
+      case 'q':
+        m_queue_name = std::string(option_arg);
+        m_thread_specified = true;
+        break;
+      case 'f':
+        m_file_name = std::string(option_arg);
+        m_sym_ctx_specified = true;
+        break;
+      case 'l': {
+        uint32_t line;
+        if (option_arg.getAsInteger(0, line))
+          error = Status::FromErrorStringWithFormat(
+              "invalid start line number '%s'", option_arg.str().c_str());
+        else
+          m_line_start = line;
+        m_sym_ctx_specified = true;
+        break;
+      }
+      case 'e': {
+        uint32_t line;
+        if (option_arg.getAsInteger(0, line))
+          error = Status::FromErrorStringWithFormat(
+              "invalid end line number '%s'", option_arg.str().c_str());
+        else
+          m_line_end = line;
+        m_sym_ctx_specified = true;
+        break;
+      }
+      case 'c':
+        m_class_name = std::string(option_arg);
+        m_sym_ctx_specified = true;
+        break;
+      case 'n':
+        m_function_name = std::string(option_arg);
+        m_sym_ctx_specified = true;
+        break;
+      case 'G': {
+        bool value, success;
+        value = OptionArgParser::ToBoolean(option_arg, false, &success);
+        if (success)
+          m_auto_continue = value;
+        else
+          error = Status::FromErrorStringWithFormat(
+              "invalid boolean value '%s' passed for -G option",
+              option_arg.str().c_str());
+        break;
+      }
+      case 'I': {
+        bool value, success;
+        value = OptionArgParser::ToBoolean(option_arg, true, &success);
+        if (success)
+          m_at_initial_stop = value;
+        else
+          error = Status::FromErrorStringWithFormat(
+              "invalid boolean value '%s' passed for -I option",
+              option_arg.str().c_str());
+        break;
+      }
+      default:
+        llvm_unreachable("unhandled option");
+      }
+      return error;
+    }
+
+    void OptionParsingStarting(ExecutionContext *execution_context) override {
+      m_use_one_liner = false;
+      m_one_liner.clear();
+      m_on_load = false;
+      m_on_unload = false;
+      m_on_stop = false;
+      m_sym_ctx_specified = false;
+      m_thread_specified = false;
+      m_module_name.clear();
+      m_file_name.clear();
+      m_class_name.clear();
+      m_function_name.clear();
+      m_line_start = 0;
+      m_line_end = UINT_MAX;
+      m_thread_id = LLDB_INVALID_THREAD_ID;
+      m_thread_index = UINT32_MAX;
+      m_thread_name.clear();
+      m_queue_name.clear();
+      m_auto_continue = false;
+      m_at_initial_stop = true;
+    }
+
+    std::vector<std::string> m_one_liner;
+    bool m_use_one_liner = false;
+    bool m_on_load = false;
+    bool m_on_unload = false;
+    bool m_on_stop = false;
+
+    // Filter options (for stop trigger).
+    bool m_sym_ctx_specified = false;
+    bool m_thread_specified = false;
+    std::string m_module_name;
+    std::string m_file_name;
+    std::string m_class_name;
+    std::string m_function_name;
+    uint32_t m_line_start = 0;
+    uint32_t m_line_end = UINT_MAX;
+    lldb::tid_t m_thread_id = LLDB_INVALID_THREAD_ID;
+    uint32_t m_thread_index = UINT32_MAX;
+    std::string m_thread_name;
+    std::string m_queue_name;
+    bool m_auto_continue = false;
+    bool m_at_initial_stop = true;
+  };
+
+  CommandObjectTargetHookAdd(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "target hook add",
+            "Add a hook to be executed on target lifecycle events.",
+            "target hook add"),
+        IOHandlerDelegateMultiline("DONE",
+                                   IOHandlerDelegate::Completion::LLDBCommand),
+        m_python_class_options("scripted hook", false, 'P') {
+    SetHelpLong(R"help(
+Command-based hooks:
+--------------------
+  Specify which triggers the hook responds to with --on-load (-L),
+  --on-unload (-u), and/or --on-stop (-S).  At least one trigger is required.
+  Provide commands with --one-liner (-o), or omit -o to enter an interactive
+  command editor.  All commands run for every trigger the hook is signed up
+  for; there is no per-trigger command list.
+
+  Examples:
+    target hook add -L -o "script print('module loaded')"
+    target hook add -L -u -o "script print('module event')"
+    target hook add -S -o "bt"
+    target hook add -L -u -S -o "script print('all events')"
+    target hook add -S -s mylib.so -n main -o "bt"
+    target hook add -S -G true -o "thread info"
+
+Python class hooks:
+-------------------
+  Provide a Python class with --python-class (-P).  The class controls which
+  events it handles by implementing the corresponding methods; you do not
+  specify triggers on the command line.  Use -k <key> -v <value> to pass
+  extra_args to the class constructor.
+
+  Examples:
+    target hook add -P mymodule.MyHook
+    target hook add -P mymodule.MyHook -k verbose -v true
+
+  The Python class should implement at least one of these methods:
+
+    class MyHook:
+        def __init__(self, target, extra_args, internal_dict):
+            self.target = target
+        def handle_module_loaded(self, stream):
+            pass
+        def handle_module_unloaded(self, stream):
+            pass
+        def handle_stop(self, exe_ctx, stream):
+            return True  # True = should_stop, False = continue
+
+Filter options:
+---------------
+  Filters (-s, -f, -l, -e, -c, -n, -x, -t, -T, -q) restrict when the hook
+  fires.  They apply to both command-based and Python class hooks.
+)help");
+    // Python class options (-P, -k, -v) are placed in Set 2 (dst_mask).
+    // src_mask must cover Set 1 | Set 2 to match the internal usage masks of
+    // OptionGroupPythonClassWithDict (class=Set1, key/value=Set2).
+    // Since -o, -L, -u, -S are Group<1> only, the parser prevents mixing.
+    m_all_options.Append(&m_python_class_options,
+                         LLDB_OPT_SET_1 | LLDB_OPT_SET_2, LLDB_OPT_SET_2);
+    m_all_options.Append(&m_options);
+    m_all_options.Finalize();
+  }
+
+  ~CommandObjectTargetHookAdd() override = default;
+
+  Options *GetOptions() override { return &m_all_options; }
+
+protected:
+  void IOHandlerActivated(IOHandler &io_handler, bool interactive) override {
+    if (interactive) {
+      if (lldb::LockableStreamFileSP output_sp =
+              io_handler.GetOutputStreamFileSP()) {
+        LockedStreamFile locked_stream = output_sp->Lock();
+        locked_stream.PutCString(
+            "Enter your hook command(s). Type 'DONE' to end.\n");
+      }
+    }
+  }
+
+  void IOHandlerInputComplete(IOHandler &io_handler,
+                              std::string &line) override {
+    if (m_hook_sp) {
+      if (line.empty()) {
+        if (lldb::LockableStreamFileSP error_sp =
+                io_handler.GetErrorStreamFileSP()) {
+          LockedStreamFile locked_stream = error_sp->Lock();
+          locked_stream.Printf("error: hook #%" PRIu64
+                               " aborted, no commands.\n",
+                               m_hook_sp->GetID());
+        }
+        GetTarget().UndoCreateHook(m_hook_sp->GetID());
+      } else {
+        auto *hook = static_cast<Target::HookCommandLine *>(m_hook_sp.get());
+        hook->SetActionFromString(line);
+        if (lldb::LockableStreamFileSP output_sp =
+                io_handler.GetOutputStreamFileSP()) {
+          LockedStreamFile locked_stream = output_sp->Lock();
+          locked_stream.Printf("Hook #%" PRIu64 " added.\n",
+                               m_hook_sp->GetID());
+        }
+      }
+      m_hook_sp.reset();
+    }
+    io_handler.SetIsDone(true);
+  }
+
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    m_hook_sp.reset();
+    Target &target = GetTarget();
+
+    bool is_python_class = !m_python_class_options.GetName().empty();
+
+    // Command-based hooks require at least one explicit trigger.
+    if (!is_python_class && !m_options.m_on_load && !m_options.m_on_unload &&
+        !m_options.m_on_stop) {
+      result.AppendError("at least one trigger must be specified: "
+                         "--on-load (-L), --on-unload (-u), or --on-stop (-S)");
+      return;
+    }
+
+    Target::Hook::HookKind hook_kind =
+        is_python_class ? Target::Hook::HookKind::ScriptBased
+                        : Target::Hook::HookKind::CommandBased;
+
+    Target::HookSP new_hook_sp = target.CreateHook(hook_kind);
+
+    if (is_python_class) {
+      // Python class hooks respond to all triggers; the class controls
+      // behavior by which callback methods it implements.
+      new_hook_sp->SetTriggerMask(Target::Hook::kModulesLoaded |
----------------
jimingham wrote:

I think this provides a confusing UI.  If I add a class that does modules loaded and modules unloaded and look at the description, it will tell me that it fires on stop.  That would be disconcerting.
I don't think it would be hard to go from the class to the triggers it supports, since that's just looking up methods in the Python class. 

https://github.com/llvm/llvm-project/pull/185465


More information about the lldb-commits mailing list