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

Bar Soloveychik via lldb-commits lldb-commits at lists.llvm.org
Mon Mar 9 10:27:16 PDT 2026


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

Ran: 
`./bin/llvm-lit -v /home/barsolo/llvm-sand/external/llvm-project/lldb/test/Shell/Commands/command-module-hook*.test`
`./bin/llvm-lit -v /home/barsolo/llvm-sand/external/llvm-project/lldb/test/API/commands/target/module-hook/`

>From 69157a9d440450d35a792892d735c841d7a3f9a6 Mon Sep 17 00:00:00 2001
From: Bar Soloveychik <barsolo at fb.com>
Date: Mon, 9 Mar 2026 10:24:52 -0700
Subject: [PATCH] [LLDB] Add module hook

---
 .../Interfaces/ScriptedModuleHookInterface.h  |  35 ++
 .../lldb/Interpreter/ScriptInterpreter.h      |   5 +
 lldb/include/lldb/Target/Target.h             | 104 ++++++
 lldb/include/lldb/lldb-forward.h              |   3 +
 lldb/source/Commands/CommandObjectTarget.cpp  | 328 ++++++++++++++++++
 lldb/source/Commands/Options.td               |  11 +
 .../ScriptInterpreter/Python/CMakeLists.txt   |   1 +
 .../ScriptInterpreterPythonInterfaces.cpp     |   2 +
 .../ScriptInterpreterPythonInterfaces.h       |   1 +
 .../ScriptedModuleHookPythonInterface.cpp     |  64 ++++
 .../ScriptedModuleHookPythonInterface.h       |  52 +++
 .../Python/ScriptInterpreterPython.cpp        |   5 +
 .../Python/ScriptInterpreterPythonImpl.h      |   3 +
 lldb/source/Target/Target.cpp                 | 269 ++++++++++++++
 .../delete/TestTargetModuleHookDelete.py      |  19 +
 .../disable/TestTargetModuleHookDisable.py    |  19 +
 .../enable/TestTargetModuleHookEnable.py      |  19 +
 .../Commands/command-module-hook-fire.test    |  10 +
 .../Shell/Commands/command-module-hook.test   | 113 ++++++
 19 files changed, 1063 insertions(+)
 create mode 100644 lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
 create mode 100644 lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h
 create mode 100644 lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py
 create mode 100644 lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py
 create mode 100644 lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py
 create mode 100644 lldb/test/Shell/Commands/command-module-hook-fire.test
 create mode 100644 lldb/test/Shell/Commands/command-module-hook.test

diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h
new file mode 100644
index 0000000000000..7a06349eb9161
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h
@@ -0,0 +1,35 @@
+//===-- ScriptedModuleHookInterface.h ----------------------------*- C++
+//-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_INTERPRETER_INTERFACES_SCRIPTEDMODULEHOOKINTERFACE_H
+#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDMODULEHOOKINTERFACE_H
+
+#include "lldb/lldb-private.h"
+
+#include "ScriptedInterface.h"
+
+namespace lldb_private {
+class ScriptedModuleHookInterface : public ScriptedInterface {
+public:
+  virtual llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, lldb::TargetSP target_sp,
+                     const StructuredDataImpl &args_sp) = 0;
+
+  /// Called when modules are loaded into the target. Unlike stop hooks,
+  /// module hooks do not control whether the process should stop.
+  virtual void HandleModuleLoaded(lldb::StreamSP &output_sp) {}
+
+  /// Called when modules are unloaded from the target. Optional for
+  /// scripted hooks; if not implemented, the hook silently does nothing
+  /// on unload.
+  virtual void HandleModuleUnloaded(lldb::StreamSP &output_sp) {}
+};
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDMODULEHOOKINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 557d73a415452..402af875c7eb5 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -562,6 +562,11 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  virtual lldb::ScriptedModuleHookInterfaceSP
+  CreateScriptedModuleHookInterface() {
+    return {};
+  }
+
   virtual lldb::ScriptedBreakpointInterfaceSP
   CreateScriptedBreakpointInterface() {
     return {};
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index 4f5b022765f9e..bf5582d30921d 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -1641,6 +1641,106 @@ class Target : public std::enable_shared_from_this<Target>,
 
   typedef std::shared_ptr<StopHook> StopHookSP;
 
+  // Target Module Hooks
+  //
+  // Module hooks fire whenever modules are loaded or unloaded from the
+  // target (via ModulesDidLoad / ModulesDidUnload).
+  class ModuleHook : public UserID {
+  public:
+    ModuleHook(const ModuleHook &rhs);
+    virtual ~ModuleHook() = default;
+
+    enum class ModuleHookKind : uint32_t { CommandBased = 0, ScriptBased };
+
+    lldb::TargetSP &GetTarget() { return m_target_sp; }
+
+    bool IsActive() { return m_active; }
+    void SetIsActive(bool is_active) { m_active = is_active; }
+
+    bool GetFireOnUnload() const { return m_fire_on_unload; }
+    void SetFireOnUnload(bool fire) { m_fire_on_unload = fire; }
+
+    void GetDescription(Stream &s, lldb::DescriptionLevel level) const;
+    virtual void GetSubclassDescription(Stream &s,
+                                        lldb::DescriptionLevel level) const = 0;
+
+    virtual void HandleModuleLoaded(lldb::StreamSP output) = 0;
+    virtual void HandleModuleUnloaded(lldb::StreamSP output) = 0;
+
+  protected:
+    lldb::TargetSP m_target_sp;
+    bool m_active = true;
+    bool m_fire_on_unload = false;
+
+    ModuleHook(lldb::TargetSP target_sp, lldb::user_id_t uid);
+  };
+
+  class ModuleHookCommandLine : public ModuleHook {
+  public:
+    ~ModuleHookCommandLine() override = default;
+
+    StringList &GetCommands() { return m_commands; }
+    void SetActionFromString(const std::string &string);
+    void SetActionFromStrings(const std::vector<std::string> &strings);
+
+    void HandleModuleLoaded(lldb::StreamSP output) override;
+    void HandleModuleUnloaded(lldb::StreamSP output) override;
+    void GetSubclassDescription(Stream &s,
+                                lldb::DescriptionLevel level) const override;
+
+  private:
+    StringList m_commands;
+
+    ModuleHookCommandLine(lldb::TargetSP target_sp, lldb::user_id_t uid)
+        : ModuleHook(target_sp, uid) {}
+    friend class Target;
+  };
+
+  class ModuleHookScripted : public ModuleHook {
+  public:
+    ~ModuleHookScripted() override = default;
+
+    void HandleModuleLoaded(lldb::StreamSP output) override;
+    void HandleModuleUnloaded(lldb::StreamSP output) override;
+
+    Status SetScriptCallback(std::string class_name,
+                             StructuredData::ObjectSP extra_args_sp);
+
+    void GetSubclassDescription(Stream &s,
+                                lldb::DescriptionLevel level) const override;
+
+  private:
+    std::string m_class_name;
+    StructuredDataImpl m_extra_args;
+    lldb::ScriptedModuleHookInterfaceSP m_interface_sp;
+
+    ModuleHookScripted(lldb::TargetSP target_sp, lldb::user_id_t uid)
+        : ModuleHook(target_sp, uid) {}
+    friend class Target;
+  };
+
+  typedef std::shared_ptr<ModuleHook> ModuleHookSP;
+
+  ModuleHookSP CreateModuleHook(ModuleHook::ModuleHookKind kind);
+
+  void UndoCreateModuleHook(lldb::user_id_t uid);
+
+  bool RemoveModuleHookByID(lldb::user_id_t uid);
+
+  void RemoveAllModuleHooks();
+
+  ModuleHookSP GetModuleHookByID(lldb::user_id_t uid);
+
+  bool SetModuleHookActiveStateByID(lldb::user_id_t uid, bool active_state);
+
+  void SetAllModuleHooksActiveState(bool active_state);
+
+  size_t GetNumModuleHooks() const { return m_module_hooks.size(); }
+
+  ModuleHookSP GetModuleHookAtIndex(size_t index);
+
+  void RunModuleHooks(bool is_load);
+
   /// Add an empty stop hook to the Target's stop hook list, and returns a
   /// shared pointer to the new hook.
   StopHookSP CreateStopHook(StopHook::StopHookKind kind, bool internal = false);
@@ -1826,6 +1926,10 @@ class Target : public std::enable_shared_from_this<Target>,
   bool m_valid;
   bool m_suppress_stop_hooks; /// Used to not run stop hooks for expressions
   bool m_is_dummy_target;
+
+  typedef std::map<lldb::user_id_t, ModuleHookSP> ModuleHookCollection;
+  ModuleHookCollection m_module_hooks;
+  lldb::user_id_t m_module_hook_next_id = 0;
   unsigned m_next_persistent_variable_index = 0;
   lldb::user_id_t m_target_unique_id =
       LLDB_INVALID_GLOBALLY_UNIQUE_TARGET_ID; ///< The globally unique ID
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index ccfe5efa19e1d..fcff135236ad0 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -191,6 +191,7 @@ class ScriptedFrameInterface;
 class ScriptedFrameProviderInterface;
 class ScriptedMetadata;
 class ScriptedBreakpointInterface;
+class ScriptedModuleHookInterface;
 class ScriptedPlatformInterface;
 class ScriptedProcessInterface;
 class ScriptedStopHookInterface;
@@ -425,6 +426,8 @@ typedef std::unique_ptr<lldb_private::ScriptedProcessInterface>
     ScriptedProcessInterfaceUP;
 typedef std::shared_ptr<lldb_private::ScriptedStopHookInterface>
     ScriptedStopHookInterfaceSP;
+typedef std::shared_ptr<lldb_private::ScriptedModuleHookInterface>
+    ScriptedModuleHookInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedThreadInterface>
     ScriptedThreadInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedThreadPlanInterface>
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp
index 59ccf390dea31..b18705e0c6913 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -5333,6 +5333,331 @@ class CommandObjectMultiwordTargetStopHooks : public CommandObjectMultiword {
   ~CommandObjectMultiwordTargetStopHooks() override = default;
 };
 
+#pragma mark CommandObjectTargetModuleHookAdd
+
+#define LLDB_OPTIONS_target_module_hook_add
+#include "CommandOptions.inc"
+
+class CommandObjectTargetModuleHookAdd : 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_module_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_module_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 'u':
+        m_on_unload = true;
+        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_unload = false;
+    }
+
+    std::vector<std::string> m_one_liner;
+    bool m_use_one_liner = false;
+    bool m_on_unload = false;
+  };
+
+  CommandObjectTargetModuleHookAdd(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "target module-hook add",
+            "Add a hook to be executed whenever modules are loaded into the "
+            "target.",
+            "target module-hook add"),
+        IOHandlerDelegateMultiline("DONE",
+                                   IOHandlerDelegate::Completion::LLDBCommand),
+        m_python_class_options("scripted module hook", false, 'P') {
+    SetHelpLong(R"help(
+Command-based module hooks allow running LLDB commands every time modules are
+loaded into the target. For example:
+
+    target module-hook add -o "script print('module loaded')"
+
+Use --on-unload (-u) to also fire the hook when modules are unloaded:
+
+    target module-hook add -u -o "script print('module event')"
+
+Python-based module hooks allow running a Python class:
+
+    target module-hook add -P MyHook
+
+The Python class should implement:
+
+    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
+
+handle_module_loaded is required. handle_module_unloaded is optional and only
+called when --on-unload is specified.
+)help");
+    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();
+  }
+
+  ~CommandObjectTargetModuleHookAdd() 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 module hook command(s). Type 'DONE' to end.\n");
+      }
+    }
+  }
+
+  void IOHandlerInputComplete(IOHandler &io_handler,
+                              std::string &line) override {
+    if (m_module_hook_sp) {
+      if (line.empty()) {
+        if (lldb::LockableStreamFileSP error_sp =
+                io_handler.GetErrorStreamFileSP()) {
+          LockedStreamFile locked_stream = error_sp->Lock();
+          locked_stream.Printf("error: module hook #%" PRIu64
+                               " aborted, no commands.\n",
+                               m_module_hook_sp->GetID());
+        }
+        GetTarget().UndoCreateModuleHook(m_module_hook_sp->GetID());
+      } else {
+        auto *hook = static_cast<Target::ModuleHookCommandLine *>(
+            m_module_hook_sp.get());
+        hook->SetActionFromString(line);
+        if (lldb::LockableStreamFileSP output_sp =
+                io_handler.GetOutputStreamFileSP()) {
+          LockedStreamFile locked_stream = output_sp->Lock();
+          locked_stream.Printf("Module hook #%" PRIu64 " added.\n",
+                               m_module_hook_sp->GetID());
+        }
+      }
+      m_module_hook_sp.reset();
+    }
+    io_handler.SetIsDone(true);
+  }
+
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    m_module_hook_sp.reset();
+    Target &target = GetTarget();
+
+    Target::ModuleHook::ModuleHookKind hook_kind;
+    if (m_python_class_options.GetName().empty())
+      hook_kind = Target::ModuleHook::ModuleHookKind::CommandBased;
+    else
+      hook_kind = Target::ModuleHook::ModuleHookKind::ScriptBased;
+
+    Target::ModuleHookSP new_hook_sp = target.CreateModuleHook(hook_kind);
+
+    if (m_options.m_on_unload)
+      new_hook_sp->SetFireOnUnload(true);
+
+    if (m_options.m_use_one_liner) {
+      auto *hook =
+          static_cast<Target::ModuleHookCommandLine *>(new_hook_sp.get());
+      hook->SetActionFromStrings(m_options.m_one_liner);
+      result.AppendMessageWithFormat("Module hook #%" PRIu64 " added.\n",
+                                     new_hook_sp->GetID());
+    } else if (!m_python_class_options.GetName().empty()) {
+      auto *hook = static_cast<Target::ModuleHookScripted *>(new_hook_sp.get());
+      Status callback_error =
+          hook->SetScriptCallback(m_python_class_options.GetName(),
+                                  m_python_class_options.GetStructuredData());
+      if (callback_error.Fail()) {
+        result.AppendErrorWithFormat("error: couldn't add module hook: %s\n",
+                                     callback_error.AsCString());
+        target.UndoCreateModuleHook(new_hook_sp->GetID());
+        return;
+      }
+      result.AppendMessageWithFormat("Module hook #%" PRIu64 " added.\n",
+                                     new_hook_sp->GetID());
+    } else {
+      m_module_hook_sp = new_hook_sp;
+      m_interpreter.GetLLDBCommandsFromIOHandler("> ",   // prompt
+                                                 *this); // delegate
+    }
+    result.SetStatus(eReturnStatusSuccessFinishNoResult);
+  }
+
+private:
+  CommandOptions m_options;
+  OptionGroupPythonClassWithDict m_python_class_options;
+  OptionGroupOptions m_all_options;
+  Target::ModuleHookSP m_module_hook_sp;
+};
+
+#pragma mark CommandObjectTargetModuleHookDelete
+
+class CommandObjectTargetModuleHookDelete : public CommandObjectParsed {
+public:
+  CommandObjectTargetModuleHookDelete(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "target module-hook delete",
+                            "Delete a module-hook.",
+                            "target module-hook delete [<id>]") {
+    AddSimpleArgumentList(eArgTypeStopHookID, eArgRepeatStar);
+  }
+
+  ~CommandObjectTargetModuleHookDelete() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Target &target = GetTarget();
+    if (command.GetArgumentCount() == 0) {
+      target.RemoveAllModuleHooks();
+      result.SetStatus(eReturnStatusSuccessFinishNoResult);
+      return;
+    }
+
+    for (size_t i = 0; i < command.GetArgumentCount(); i++) {
+      lldb::user_id_t user_id;
+      if (!llvm::to_integer(command.GetArgumentAtIndex(i), user_id)) {
+        result.AppendErrorWithFormat("invalid module hook id: \"%s\".\n",
+                                     command.GetArgumentAtIndex(i));
+        return;
+      }
+      if (!target.RemoveModuleHookByID(user_id)) {
+        result.AppendErrorWithFormat("unknown module hook id: \"%s\".\n",
+                                     command.GetArgumentAtIndex(i));
+        return;
+      }
+    }
+    result.SetStatus(eReturnStatusSuccessFinishNoResult);
+  }
+};
+
+#pragma mark CommandObjectTargetModuleHookEnableDisable
+
+class CommandObjectTargetModuleHookEnableDisable : public CommandObjectParsed {
+public:
+  CommandObjectTargetModuleHookEnableDisable(CommandInterpreter &interpreter,
+                                             bool enable, const char *name,
+                                             const char *help,
+                                             const char *syntax)
+      : CommandObjectParsed(interpreter, name, help, syntax), m_enable(enable) {
+    AddSimpleArgumentList(eArgTypeStopHookID, eArgRepeatStar);
+  }
+
+  ~CommandObjectTargetModuleHookEnableDisable() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Target &target = GetTarget();
+    if (command.GetArgumentCount() == 0) {
+      target.SetAllModuleHooksActiveState(m_enable);
+      result.SetStatus(eReturnStatusSuccessFinishNoResult);
+      return;
+    }
+
+    for (size_t i = 0; i < command.GetArgumentCount(); i++) {
+      lldb::user_id_t user_id;
+      if (!llvm::to_integer(command.GetArgumentAtIndex(i), user_id)) {
+        result.AppendErrorWithFormat("invalid module hook id: \"%s\".\n",
+                                     command.GetArgumentAtIndex(i));
+        return;
+      }
+      if (!target.SetModuleHookActiveStateByID(user_id, m_enable)) {
+        result.AppendErrorWithFormat("unknown module hook id: \"%s\".\n",
+                                     command.GetArgumentAtIndex(i));
+        return;
+      }
+    }
+    result.SetStatus(eReturnStatusSuccessFinishNoResult);
+  }
+
+private:
+  bool m_enable;
+};
+
+#pragma mark CommandObjectTargetModuleHookList
+
+class CommandObjectTargetModuleHookList : public CommandObjectParsed {
+public:
+  CommandObjectTargetModuleHookList(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "target module-hook list",
+                            "List all module-hooks.",
+                            "target module-hook list") {}
+
+  ~CommandObjectTargetModuleHookList() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Target &target = GetTarget();
+    size_t num_hooks = target.GetNumModuleHooks();
+    if (num_hooks == 0) {
+      result.GetOutputStream().PutCString("No module hooks.\n");
+    } else {
+      for (size_t i = 0; i < num_hooks; i++) {
+        Target::ModuleHookSP hook_sp = target.GetModuleHookAtIndex(i);
+        if (hook_sp)
+          hook_sp->GetDescription(result.GetOutputStream(),
+                                  eDescriptionLevelFull);
+      }
+    }
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+  }
+};
+
+#pragma mark CommandObjectMultiwordTargetModuleHooks
+
+class CommandObjectMultiwordTargetModuleHooks : public CommandObjectMultiword {
+public:
+  CommandObjectMultiwordTargetModuleHooks(CommandInterpreter &interpreter)
+      : CommandObjectMultiword(
+            interpreter, "target module-hook",
+            "Commands for operating on debugger target module-hooks.",
+            "target module-hook <subcommand> [<subcommand-options>]") {
+    LoadSubCommand("add", CommandObjectSP(new CommandObjectTargetModuleHookAdd(
+                              interpreter)));
+    LoadSubCommand(
+        "delete",
+        CommandObjectSP(new CommandObjectTargetModuleHookDelete(interpreter)));
+    LoadSubCommand(
+        "disable",
+        CommandObjectSP(new CommandObjectTargetModuleHookEnableDisable(
+            interpreter, false, "target module-hook disable [<id>]",
+            "Disable a module-hook.", "target module-hook disable")));
+    LoadSubCommand(
+        "enable",
+        CommandObjectSP(new CommandObjectTargetModuleHookEnableDisable(
+            interpreter, true, "target module-hook enable [<id>]",
+            "Enable a module-hook.", "target module-hook enable")));
+    LoadSubCommand(
+        "list",
+        CommandObjectSP(new CommandObjectTargetModuleHookList(interpreter)));
+  }
+
+  ~CommandObjectMultiwordTargetModuleHooks() override = default;
+};
+
 #pragma mark CommandObjectTargetDumpTypesystem
 
 /// Dumps the TypeSystem of the selected Target.
@@ -5625,6 +5950,9 @@ CommandObjectMultiwordTarget::CommandObjectMultiwordTarget(
   LoadSubCommand(
       "stop-hook",
       CommandObjectSP(new CommandObjectMultiwordTargetStopHooks(interpreter)));
+  LoadSubCommand("module-hook",
+                 CommandObjectSP(
+                     new CommandObjectMultiwordTargetModuleHooks(interpreter)));
   LoadSubCommand("modules",
                  CommandObjectSP(new CommandObjectTargetModules(interpreter)));
   LoadSubCommand("symbols",
diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td
index a4d72010d2c4c..8eee059b7c9c0 100644
--- a/lldb/source/Commands/Options.td
+++ b/lldb/source/Commands/Options.td
@@ -1901,6 +1901,17 @@ let Command = "target stop_hook list" in {
         Desc<"Show debugger ${i}nternal stop hooks.">;
 }
 
+let Command = "target module_hook add" in {
+  def target_module_hook_add_one_liner
+      : Option<"one-liner", "o">,
+        Arg<"OneLiner">,
+        Desc<"Add a command for the module hook. Can be specified more than "
+             "once, and commands will be run in the order they appear.">;
+  def target_module_hook_add_on_unload
+      : Option<"on-unload", "u">,
+        Desc<"Also fire the hook when modules are ${u}nloaded.">;
+}
+
 let Command = "thread backtrace" in {
   def thread_backtrace_count : Option<"count", "c">,
                                Group<1>,
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
index 303e17a938912..817520bc37271 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
@@ -31,6 +31,7 @@ add_lldb_library(lldbPluginScriptInterpreterPython PLUGIN
   Interfaces/ScriptedProcessPythonInterface.cpp
   Interfaces/ScriptedPythonInterface.cpp
   Interfaces/ScriptedStopHookPythonInterface.cpp
+  Interfaces/ScriptedModuleHookPythonInterface.cpp
   Interfaces/ScriptedBreakpointPythonInterface.cpp
   Interfaces/ScriptedThreadPlanPythonInterface.cpp
   Interfaces/ScriptedThreadPythonInterface.cpp
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
index 3f6f9cfd4da8d..d7368de2bda67 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
@@ -24,6 +24,7 @@ void ScriptInterpreterPythonInterfaces::Initialize() {
   ScriptedPlatformPythonInterface::Initialize();
   ScriptedProcessPythonInterface::Initialize();
   ScriptedStopHookPythonInterface::Initialize();
+  ScriptedModuleHookPythonInterface::Initialize();
   ScriptedBreakpointPythonInterface::Initialize();
   ScriptedThreadPlanPythonInterface::Initialize();
   ScriptedFrameProviderPythonInterface::Initialize();
@@ -34,6 +35,7 @@ void ScriptInterpreterPythonInterfaces::Terminate() {
   ScriptedPlatformPythonInterface::Terminate();
   ScriptedProcessPythonInterface::Terminate();
   ScriptedStopHookPythonInterface::Terminate();
+  ScriptedModuleHookPythonInterface::Terminate();
   ScriptedBreakpointPythonInterface::Terminate();
   ScriptedThreadPlanPythonInterface::Terminate();
   ScriptedFrameProviderPythonInterface::Terminate();
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
index 58b19760cb8e1..6866aabb22666 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
@@ -16,6 +16,7 @@
 #include "ScriptedBreakpointPythonInterface.h"
 #include "ScriptedFrameProviderPythonInterface.h"
 #include "ScriptedFramePythonInterface.h"
+#include "ScriptedModuleHookPythonInterface.h"
 #include "ScriptedPlatformPythonInterface.h"
 #include "ScriptedProcessPythonInterface.h"
 #include "ScriptedStopHookPythonInterface.h"
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
new file mode 100644
index 0000000000000..5b2de9a4a27e2
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.cpp
@@ -0,0 +1,64 @@
+//===-- ScriptedModuleHookPythonInterface.cpp
+//------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/lldb-enumerations.h"
+
+#include "../SWIGPythonBridge.h"
+#include "../ScriptInterpreterPythonImpl.h"
+#include "../lldb-python.h"
+#include "ScriptedModuleHookPythonInterface.h"
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::python;
+
+ScriptedModuleHookPythonInterface::ScriptedModuleHookPythonInterface(
+    ScriptInterpreterPythonImpl &interpreter)
+    : ScriptedModuleHookInterface(), ScriptedPythonInterface(interpreter) {}
+
+llvm::Expected<StructuredData::GenericSP>
+ScriptedModuleHookPythonInterface::CreatePluginObject(
+    llvm::StringRef class_name, lldb::TargetSP target_sp,
+    const StructuredDataImpl &args_sp) {
+  return ScriptedPythonInterface::CreatePluginObject(class_name, nullptr,
+                                                     target_sp, args_sp);
+}
+
+void ScriptedModuleHookPythonInterface::HandleModuleLoaded(
+    lldb::StreamSP &output_sp) {
+  Status error;
+  // We pass only the output stream to Python. The Python class can access
+  // self.target (set during __init__) to query loaded modules if needed.
+  Dispatch("handle_module_loaded", error, output_sp);
+}
+
+void ScriptedModuleHookPythonInterface::HandleModuleUnloaded(
+    lldb::StreamSP &output_sp) {
+  Status error;
+  // Optional method. If the Python class does not implement
+  // handle_module_unloaded, the dispatch will fail silently.
+  Dispatch("handle_module_unloaded", error, output_sp);
+}
+
+void ScriptedModuleHookPythonInterface::Initialize() {
+  const std::vector<llvm::StringRef> ci_usages = {
+      "target module-hook add -P <script-name> [-k key -v value ...]"};
+  const std::vector<llvm::StringRef> api_usages = {};
+  PluginManager::RegisterPlugin(
+      GetPluginNameStatic(),
+      llvm::StringRef("Perform actions whenever modules are loaded into the "
+                      "target."),
+      CreateInstance, eScriptLanguagePython, {ci_usages, api_usages});
+}
+
+void ScriptedModuleHookPythonInterface::Terminate() {
+  PluginManager::UnregisterPlugin(CreateInstance);
+}
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h
new file mode 100644
index 0000000000000..1a5f4f14c0eab
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedModuleHookPythonInterface.h
@@ -0,0 +1,52 @@
+//===-- ScriptedModuleHookPythonInterface.h ----------------------*- C++
+//-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDMODULEHOOKPYTHONINTERFACE_H
+#define LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDMODULEHOOKPYTHONINTERFACE_H
+
+#include "lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h"
+
+#include "ScriptedPythonInterface.h"
+
+namespace lldb_private {
+class ScriptedModuleHookPythonInterface : public ScriptedModuleHookInterface,
+                                          public ScriptedPythonInterface,
+                                          public PluginInterface {
+public:
+  ScriptedModuleHookPythonInterface(ScriptInterpreterPythonImpl &interpreter);
+
+  llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, lldb::TargetSP target_sp,
+                     const StructuredDataImpl &args_sp) override;
+
+  llvm::SmallVector<AbstractMethodRequirement>
+  GetAbstractMethodRequirements() const override {
+    return llvm::SmallVector<AbstractMethodRequirement>(
+        {{"handle_module_loaded", 1}});
+  }
+
+  void HandleModuleLoaded(lldb::StreamSP &output_sp) override;
+
+  /// Optional: only called if the Python class implements
+  /// handle_module_unloaded. Silently does nothing otherwise.
+  void HandleModuleUnloaded(lldb::StreamSP &output_sp) override;
+
+  static void Initialize();
+
+  static void Terminate();
+
+  static llvm::StringRef GetPluginNameStatic() {
+    return "ScriptedModuleHookPythonInterface";
+  }
+
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+};
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDMODULEHOOKPYTHONINTERFACE_H
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index 24228a62708fd..1c94c70fa4ccb 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -1509,6 +1509,11 @@ ScriptInterpreterPythonImpl::CreateScriptedStopHookInterface() {
   return std::make_shared<ScriptedStopHookPythonInterface>(*this);
 }
 
+ScriptedModuleHookInterfaceSP
+ScriptInterpreterPythonImpl::CreateScriptedModuleHookInterface() {
+  return std::make_shared<ScriptedModuleHookPythonInterface>(*this);
+}
+
 ScriptedBreakpointInterfaceSP
 ScriptInterpreterPythonImpl::CreateScriptedBreakpointInterface() {
   return std::make_shared<ScriptedBreakpointPythonInterface>(*this);
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
index f08b2334f9ccb..59c92b08e127e 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
@@ -90,6 +90,9 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
 
   lldb::ScriptedStopHookInterfaceSP CreateScriptedStopHookInterface() override;
 
+  lldb::ScriptedModuleHookInterfaceSP
+  CreateScriptedModuleHookInterface() override;
+
   lldb::ScriptedBreakpointInterfaceSP
   CreateScriptedBreakpointInterface() override;
 
diff --git a/lldb/source/Target/Target.cpp b/lldb/source/Target/Target.cpp
index f3ee0d91a88d9..c29538ca48cc7 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -38,6 +38,7 @@
 #include "lldb/Interpreter/CommandInterpreter.h"
 #include "lldb/Interpreter/CommandReturnObject.h"
 #include "lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h"
+#include "lldb/Interpreter/Interfaces/ScriptedModuleHookInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h"
 #include "lldb/Interpreter/OptionGroupWatchpoint.h"
 #include "lldb/Interpreter/OptionValues.h"
@@ -1873,6 +1874,7 @@ void Target::ModulesDidLoad(ModuleList &module_list) {
     if (m_process_sp) {
       m_process_sp->ModulesDidLoad(module_list);
     }
+    RunModuleHooks(/*is_load=*/true);
     auto data_sp =
         std::make_shared<TargetEventData>(shared_from_this(), module_list);
     BroadcastEvent(eBroadcastBitModulesLoaded, data_sp);
@@ -1932,6 +1934,8 @@ void Target::ModulesDidUnload(ModuleList &module_list, bool delete_locations) {
 
     if (should_flush_type_systems)
       m_scratch_type_system_map.Clear();
+
+    RunModuleHooks(/*is_load=*/false);
   }
 }
 
@@ -4229,6 +4233,271 @@ void Target::StopHookScripted::GetSubclassDescription(
   as_dict->ForEach(print_one_element);
 }
 
+// ModuleHook
+
+Target::ModuleHook::ModuleHook(lldb::TargetSP target_sp, lldb::user_id_t uid)
+    : UserID(uid), m_target_sp(std::move(target_sp)) {}
+
+Target::ModuleHook::ModuleHook(const ModuleHook &rhs)
+    : UserID(rhs.GetID()), m_target_sp(rhs.m_target_sp), m_active(rhs.m_active),
+      m_fire_on_unload(rhs.m_fire_on_unload) {}
+
+void Target::ModuleHook::GetDescription(Stream &s,
+                                        lldb::DescriptionLevel level) const {
+  s.Printf("Hook: %" PRIu64 "\n", GetID());
+  if (level == eDescriptionLevelBrief)
+    return;
+  s.IndentMore();
+  s.Indent();
+  s.Printf("State: %s\n", m_active ? "enabled" : "disabled");
+  if (m_fire_on_unload) {
+    s.Indent();
+    s.PutCString("Fires on: load, unload\n");
+  }
+  GetSubclassDescription(s, level);
+  s.IndentLess();
+}
+
+// ModuleHookCommandLine
+
+void Target::ModuleHookCommandLine::SetActionFromString(
+    const std::string &string) {
+  GetCommands().SplitIntoLines(string);
+}
+
+void Target::ModuleHookCommandLine::SetActionFromStrings(
+    const std::vector<std::string> &strings) {
+  for (const auto &string : strings)
+    GetCommands().AppendString(string.c_str());
+}
+
+void Target::ModuleHookCommandLine::GetSubclassDescription(
+    Stream &s, lldb::DescriptionLevel level) const {
+  if (level == eDescriptionLevelBrief) {
+    if (m_commands.GetSize() == 1)
+      s.PutCString(m_commands.GetStringAtIndex(0));
+    else
+      s.Printf("%" PRIu64 " commands", (uint64_t)m_commands.GetSize());
+    return;
+  }
+
+  s.Indent("Commands: \n");
+  s.IndentMore();
+  for (uint32_t i = 0; i < m_commands.GetSize(); i++) {
+    s.Indent(m_commands.GetStringAtIndex(i));
+    s.PutCString("\n");
+  }
+  s.IndentLess();
+}
+
+void Target::ModuleHookCommandLine::HandleModuleLoaded(StreamSP output_sp) {
+  if (!m_commands.GetSize())
+    return;
+
+  TargetSP target_sp = GetTarget();
+  if (!target_sp)
+    return;
+
+  CommandReturnObject result(false);
+  result.SetImmediateOutputStream(output_sp);
+  result.SetInteractive(false);
+  Debugger &debugger = target_sp->GetDebugger();
+
+  ExecutionContext exe_ctx;
+  if (target_sp->GetProcessSP())
+    exe_ctx.SetContext(target_sp->GetProcessSP());
+  else
+    exe_ctx.SetContext(target_sp, false);
+
+  CommandInterpreterRunOptions options;
+  options.SetStopOnContinue(true);
+  options.SetStopOnError(true);
+  options.SetEchoCommands(false);
+  options.SetPrintResults(true);
+  options.SetPrintErrors(true);
+  options.SetAddToHistory(false);
+
+  bool old_async = debugger.GetAsyncExecution();
+  debugger.SetAsyncExecution(true);
+  debugger.GetCommandInterpreter().HandleCommands(GetCommands(), exe_ctx,
+                                                  options, result);
+  debugger.SetAsyncExecution(old_async);
+}
+
+void Target::ModuleHookCommandLine::HandleModuleUnloaded(StreamSP output_sp) {
+  // Command-based hooks run the same commands on unload as on load.
+  HandleModuleLoaded(output_sp);
+}
+
+// ModuleHookScripted
+
+Status Target::ModuleHookScripted::SetScriptCallback(
+    std::string class_name, StructuredData::ObjectSP extra_args_sp) {
+  ScriptInterpreter *script_interp =
+      GetTarget()->GetDebugger().GetScriptInterpreter();
+  if (!script_interp)
+    return Status::FromErrorString("No script interpreter installed.");
+
+  m_interface_sp = script_interp->CreateScriptedModuleHookInterface();
+  if (!m_interface_sp)
+    return Status::FromErrorStringWithFormat(
+        "ScriptedModuleHook::%s () - ERROR: %s", __FUNCTION__,
+        "Script interpreter couldn't create Scripted Module Hook Interface");
+
+  m_class_name = std::move(class_name);
+  m_extra_args.SetObjectSP(extra_args_sp);
+
+  auto obj_or_err = m_interface_sp->CreatePluginObject(
+      m_class_name, GetTarget(), m_extra_args);
+  if (!obj_or_err)
+    return Status::FromError(obj_or_err.takeError());
+
+  StructuredData::ObjectSP object_sp = *obj_or_err;
+  if (!object_sp || !object_sp->IsValid())
+    return Status::FromErrorStringWithFormat(
+        "ScriptedModuleHook::%s () - ERROR: %s", __FUNCTION__,
+        "Failed to create valid script object");
+
+  return {};
+}
+
+void Target::ModuleHookScripted::HandleModuleLoaded(StreamSP output_sp) {
+  if (!m_interface_sp)
+    return;
+
+  StreamSP stream = std::make_shared<StreamString>();
+  m_interface_sp->HandleModuleLoaded(stream);
+  output_sp->PutCString(
+      reinterpret_cast<StreamString *>(stream.get())->GetData());
+}
+
+void Target::ModuleHookScripted::HandleModuleUnloaded(StreamSP output_sp) {
+  if (!m_interface_sp)
+    return;
+
+  StreamSP stream = std::make_shared<StreamString>();
+  m_interface_sp->HandleModuleUnloaded(stream);
+  output_sp->PutCString(
+      reinterpret_cast<StreamString *>(stream.get())->GetData());
+}
+
+void Target::ModuleHookScripted::GetSubclassDescription(
+    Stream &s, lldb::DescriptionLevel level) const {
+  if (level == eDescriptionLevelBrief) {
+    s.PutCString(m_class_name);
+    return;
+  }
+  s.Indent("Class:");
+  s.Printf("%s\n", m_class_name.c_str());
+
+  if (!m_extra_args.IsValid())
+    return;
+  StructuredData::ObjectSP object_sp = m_extra_args.GetObjectSP();
+  if (!object_sp || !object_sp->IsValid())
+    return;
+
+  StructuredData::Dictionary *as_dict = object_sp->GetAsDictionary();
+  if (!as_dict || !as_dict->IsValid())
+    return;
+
+  uint32_t num_keys = as_dict->GetSize();
+  if (num_keys == 0)
+    return;
+
+  s.Indent("Args:\n");
+  s.IndentMore();
+
+  auto print_one_element = [&s](llvm::StringRef key,
+                                StructuredData::Object *object) {
+    s.Indent();
+    s.Format("{0} : {1}\n", key, object->GetStringValue());
+    return true;
+  };
+
+  as_dict->ForEach(print_one_element);
+  s.IndentLess();
+}
+
+// Module Hook management methods
+
+Target::ModuleHookSP Target::CreateModuleHook(ModuleHook::ModuleHookKind kind) {
+  lldb::user_id_t new_uid = ++m_module_hook_next_id;
+  ModuleHookSP hook_sp;
+  switch (kind) {
+  case ModuleHook::ModuleHookKind::CommandBased:
+    hook_sp.reset(new ModuleHookCommandLine(shared_from_this(), new_uid));
+    break;
+  case ModuleHook::ModuleHookKind::ScriptBased:
+    hook_sp.reset(new ModuleHookScripted(shared_from_this(), new_uid));
+    break;
+  }
+  m_module_hooks[new_uid] = hook_sp;
+  return hook_sp;
+}
+
+void Target::UndoCreateModuleHook(lldb::user_id_t uid) {
+  if (!RemoveModuleHookByID(uid))
+    return;
+  if (uid > 0)
+    --m_module_hook_next_id;
+}
+
+bool Target::RemoveModuleHookByID(lldb::user_id_t uid) {
+  size_t num_removed = m_module_hooks.erase(uid);
+  return (num_removed != 0);
+}
+
+void Target::RemoveAllModuleHooks() { m_module_hooks.clear(); }
+
+Target::ModuleHookSP Target::GetModuleHookByID(lldb::user_id_t uid) {
+  auto iter = m_module_hooks.find(uid);
+  if (iter == m_module_hooks.end())
+    return {};
+  return iter->second;
+}
+
+Target::ModuleHookSP Target::GetModuleHookAtIndex(size_t index) {
+  if (index >= m_module_hooks.size())
+    return {};
+  auto iter = m_module_hooks.begin();
+  std::advance(iter, index);
+  return iter->second;
+}
+
+bool Target::SetModuleHookActiveStateByID(lldb::user_id_t uid,
+                                          bool active_state) {
+  auto iter = m_module_hooks.find(uid);
+  if (iter == m_module_hooks.end())
+    return false;
+  iter->second->SetIsActive(active_state);
+  return true;
+}
+
+void Target::SetAllModuleHooksActiveState(bool active_state) {
+  for (auto &[_, hook] : m_module_hooks)
+    hook->SetIsActive(active_state);
+}
+
+void Target::RunModuleHooks(bool is_load) {
+  if (m_module_hooks.empty())
+    return;
+
+  StreamSP output_sp = m_debugger.GetAsyncOutputStream();
+
+  for (auto &[_, hook_sp] : m_module_hooks) {
+    if (!hook_sp->IsActive())
+      continue;
+    if (!is_load && !hook_sp->GetFireOnUnload())
+      continue;
+    if (is_load)
+      hook_sp->HandleModuleLoaded(output_sp);
+    else
+      hook_sp->HandleModuleUnloaded(output_sp);
+  }
+
+  output_sp->Flush();
+}
+
 static constexpr OptionEnumValueElement g_dynamic_value_types[] = {
     {
         eNoDynamicValues,
diff --git a/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py b/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py
new file mode 100644
index 0000000000000..51f4f386b2b0a
--- /dev/null
+++ b/lldb/test/API/commands/target/module-hook/delete/TestTargetModuleHookDelete.py
@@ -0,0 +1,19 @@
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestCase(TestBase):
+    @no_debug_info_test
+    def test_invalid_arg(self):
+        self.expect(
+            "target module-hook delete -1",
+            error=True,
+            startstr='error: invalid module hook id: "-1".',
+        )
+        self.expect(
+            "target module-hook delete abcdfx",
+            error=True,
+            startstr='error: invalid module hook id: "abcdfx".',
+        )
diff --git a/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py b/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py
new file mode 100644
index 0000000000000..455e555072f8e
--- /dev/null
+++ b/lldb/test/API/commands/target/module-hook/disable/TestTargetModuleHookDisable.py
@@ -0,0 +1,19 @@
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestCase(TestBase):
+    @no_debug_info_test
+    def test_invalid_arg(self):
+        self.expect(
+            "target module-hook disable -1",
+            error=True,
+            startstr='error: invalid module hook id: "-1".',
+        )
+        self.expect(
+            "target module-hook disable abcdfx",
+            error=True,
+            startstr='error: invalid module hook id: "abcdfx".',
+        )
diff --git a/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py b/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py
new file mode 100644
index 0000000000000..12f1d66ba03d0
--- /dev/null
+++ b/lldb/test/API/commands/target/module-hook/enable/TestTargetModuleHookEnable.py
@@ -0,0 +1,19 @@
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TestCase(TestBase):
+    @no_debug_info_test
+    def test_invalid_arg(self):
+        self.expect(
+            "target module-hook enable -1",
+            error=True,
+            startstr='error: invalid module hook id: "-1".',
+        )
+        self.expect(
+            "target module-hook enable abcdfx",
+            error=True,
+            startstr='error: invalid module hook id: "abcdfx".',
+        )
diff --git a/lldb/test/Shell/Commands/command-module-hook-fire.test b/lldb/test/Shell/Commands/command-module-hook-fire.test
new file mode 100644
index 0000000000000..84791d91351e1
--- /dev/null
+++ b/lldb/test/Shell/Commands/command-module-hook-fire.test
@@ -0,0 +1,10 @@
+# Test that module hooks fire when modules are loaded.
+#
+# RUN: %clang_host -g %S/Inputs/main.c -o %t
+# RUN: %lldb -b -o 'file %t' \
+# RUN:   -o 'target module-hook add -o "script print(\"HOOK_FIRED\")"' \
+# RUN:   -o 'target modules add %t' \
+# RUN:   2>&1 | FileCheck %s
+
+# CHECK: Module hook #1 added.
+# CHECK: HOOK_FIRED
diff --git a/lldb/test/Shell/Commands/command-module-hook.test b/lldb/test/Shell/Commands/command-module-hook.test
new file mode 100644
index 0000000000000..3cc69a07732f6
--- /dev/null
+++ b/lldb/test/Shell/Commands/command-module-hook.test
@@ -0,0 +1,113 @@
+# Test module hook add/list/delete/enable/disable commands.
+#
+# RUN: %lldb -b -s %s 2>&1 | FileCheck %s
+
+# Adding hooks assigns incrementing IDs.
+target module-hook add -o "script print('hook1')"
+# CHECK: Module hook #1 added.
+target module-hook add -o "script print('hook2')"
+# CHECK: Module hook #2 added.
+target module-hook add -o "script print('hook3')"
+# CHECK: Module hook #3 added.
+
+# List all hooks.
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: Hook: 1
+# CHECK:   State: enabled
+# CHECK:   Commands:
+# CHECK:       script print('hook1')
+# CHECK: Hook: 2
+# CHECK:   State: enabled
+# CHECK:   Commands:
+# CHECK:       script print('hook2')
+# CHECK: Hook: 3
+# CHECK:   State: enabled
+# CHECK:   Commands:
+# CHECK:       script print('hook3')
+
+# Disable a hook.
+target module-hook disable 2
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: Hook: 1
+# CHECK:   State: enabled
+# CHECK: Hook: 2
+# CHECK:   State: disabled
+# CHECK: Hook: 3
+# CHECK:   State: enabled
+
+# Re-enable the hook.
+target module-hook enable 2
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: Hook: 1
+# CHECK:   State: enabled
+# CHECK: Hook: 2
+# CHECK:   State: enabled
+# CHECK: Hook: 3
+# CHECK:   State: enabled
+
+# Delete a hook in the middle.
+target module-hook delete 2
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK:     Hook: 1
+# CHECK-NOT: Hook: 2
+# CHECK:     Hook: 3
+
+# New hook gets a new ID (not reused).
+target module-hook add -o "script print('hook4')"
+# CHECK: Module hook #4 added.
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK:     Hook: 1
+# CHECK-NOT: Hook: 2
+# CHECK:     Hook: 3
+# CHECK:     Hook: 4
+
+# Delete all hooks.
+target module-hook delete
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: No module hooks.
+
+# Add a hook with --on-unload and verify the description.
+target module-hook add -u -o "script print('load+unload')"
+# CHECK: Module hook #5 added.
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: Hook: 5
+# CHECK:   State: enabled
+# CHECK:   Fires on: load, unload
+# CHECK:   Commands:
+# CHECK:       script print('load+unload')
+
+# Add a hook with multiple one-liner commands.
+target module-hook add -o "script print('first')" -o "script print('second')"
+# CHECK: Module hook #6 added.
+target module-hook list
+# CHECK: Hook: 6
+# CHECK:   State: enabled
+# CHECK-NOT: Fires on:
+# CHECK:   Commands:
+# CHECK:       script print('first')
+# CHECK:       script print('second')
+
+# Disable all hooks.
+target module-hook disable
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: Hook: 5
+# CHECK:   State: disabled
+# CHECK: Hook: 6
+# CHECK:   State: disabled
+
+# Enable all hooks.
+target module-hook enable
+target module-hook list
+# CHECK: (lldb) target module-hook list
+# CHECK: Hook: 5
+# CHECK:   State: enabled
+# CHECK: Hook: 6
+# CHECK:   State: enabled



More information about the lldb-commits mailing list