diff --git a/include/libcamera/object.h b/include/libcamera/object.h
index e3b39cf547b0..869200a57d8c 100644
--- a/include/libcamera/object.h
+++ b/include/libcamera/object.h
@@ -28,6 +28,16 @@ public:
 
 	void postMessage(std::unique_ptr<Message> msg);
 
+	template<typename T, typename... Args, typename std::enable_if<std::is_base_of<Object, T>::value>::type * = nullptr>
+	void invokeMethod(void (T::*func)(Args...), Args... args)
+	{
+		T *obj = static_cast<T *>(this);
+		BoundMethodBase *method = new BoundMemberMethod<T, Args...>(obj, this, func);
+		void *pack = new typename BoundMemberMethod<T, Args...>::PackType{ args... };
+
+		invokeMethod(method, pack);
+	}
+
 	Thread *thread() const { return thread_; }
 	void moveToThread(Thread *thread);
 
@@ -40,6 +50,8 @@ private:
 	friend class BoundMethodBase;
 	friend class Thread;
 
+	void invokeMethod(BoundMethodBase *method, void *pack);
+
 	void connect(SignalBase *signal);
 	void disconnect(SignalBase *signal);
 
diff --git a/src/libcamera/bound_method.cpp b/src/libcamera/bound_method.cpp
index 23b8f4122283..d89f84c03f4d 100644
--- a/src/libcamera/bound_method.cpp
+++ b/src/libcamera/bound_method.cpp
@@ -19,7 +19,7 @@ void BoundMethodBase::activatePack(void *pack)
 		invokePack(pack);
 	} else {
 		std::unique_ptr<Message> msg =
-			utils::make_unique<SignalMessage>(this, pack);
+			utils::make_unique<InvokeMessage>(this, pack);
 		object_->postMessage(std::move(msg));
 	}
 }
diff --git a/src/libcamera/include/message.h b/src/libcamera/include/message.h
index b4670c0eab76..92717e316cc3 100644
--- a/src/libcamera/include/message.h
+++ b/src/libcamera/include/message.h
@@ -9,6 +9,8 @@
 
 #include <atomic>
 
+#include <libcamera/bound_method.h>
+
 namespace libcamera {
 
 class BoundMethodBase;
@@ -20,7 +22,7 @@ class Message
 public:
 	enum Type {
 		None = 0,
-		SignalMessage = 1,
+		InvokeMessage = 1,
 		UserMessage = 1000,
 	};
 
@@ -41,16 +43,19 @@ private:
 	static std::atomic_uint nextUserType_;
 };
 
-class SignalMessage : public Message
+class InvokeMessage : public Message
 {
 public:
-	SignalMessage(BoundMethodBase *method, void *pack)
-		: Message(Message::SignalMessage), method_(method), pack_(pack)
-	{
-	}
+	InvokeMessage(BoundMethodBase *method, void *pack,
+		      bool deleteMethod = false);
+	~InvokeMessage();
 
+	void invoke();
+
+private:
 	BoundMethodBase *method_;
 	void *pack_;
+	bool deleteMethod_;
 };
 
 } /* namespace libcamera */
diff --git a/src/libcamera/message.cpp b/src/libcamera/message.cpp
index 8d3376d8a533..f6c39d40fc73 100644
--- a/src/libcamera/message.cpp
+++ b/src/libcamera/message.cpp
@@ -7,6 +7,8 @@
 
 #include "message.h"
 
+#include <libcamera/signal.h>
+
 #include "log.h"
 
 /**
@@ -43,8 +45,8 @@ std::atomic_uint Message::nextUserType_{ Message::UserMessage };
  * \brief The message type
  * \var Message::None
  * \brief Invalid message type
- * \var Message::SignalMessage
- * \brief Asynchronous signal delivery across threads
+ * \var Message::InvokeMessage
+ * \brief Asynchronous method invocation across threads
  * \var Message::UserMessage
  * \brief First value available for user-defined messages
  */
@@ -107,25 +109,47 @@ Message::Type Message::registerMessageType()
 }
 
 /**
- * \class SignalMessage
- * \brief A message carrying a Signal across threads
+ * \class InvokeMessage
+ * \brief A message carrying a method invocation across threads
  */
 
 /**
- * \fn SignalMessage::SignalMessage()
- * \brief Construct a SignalMessage
- * \param[in] method The slot that the signal targets
- * \param[in] pack The signal arguments
+ * \brief Construct an InvokeMessage for method invocation on an Object
+ * \param[in] method The bound method
+ * \param[in] pack The packed method arguments
+ * \param[in] deleteMethod True to delete the \a method when the message is
+ * destroyed
  */
+InvokeMessage::InvokeMessage(BoundMethodBase *method, void *pack,
+			     bool deleteMethod)
+	: Message(Message::InvokeMessage), method_(method), pack_(pack),
+	  deleteMethod_(deleteMethod)
+{
+}
+
+InvokeMessage::~InvokeMessage()
+{
+	if (deleteMethod_)
+		delete method_;
+}
+
+/**
+ * \brief Invoke the method bound to InvokeMessage::method_ with arguments
+ * InvokeMessage::pack_
+ */
+void InvokeMessage::invoke()
+{
+	method_->invokePack(pack_);
+}
 
 /**
- * \var SignalMessage::method_
- * \brief The slot that the signal targets
+ * \var InvokeMessage::method_
+ * \brief The method to be invoked
  */
 
 /**
- * \var SignalMessage::pack_
- * \brief The signal arguments
+ * \var InvokeMessage::pack_
+ * \brief The packed method invocation arguments
  */
 
 }; /* namespace libcamera */
diff --git a/src/libcamera/object.cpp b/src/libcamera/object.cpp
index 0adbc203add8..7d70ce21b5d0 100644
--- a/src/libcamera/object.cpp
+++ b/src/libcamera/object.cpp
@@ -12,6 +12,7 @@
 #include "log.h"
 #include "message.h"
 #include "thread.h"
+#include "utils.h"
 
 /**
  * \file object.h
@@ -88,9 +89,9 @@ void Object::postMessage(std::unique_ptr<Message> msg)
 void Object::message(Message *msg)
 {
 	switch (msg->type()) {
-	case Message::SignalMessage: {
-		SignalMessage *smsg = static_cast<SignalMessage *>(msg);
-		smsg->method_->invokePack(smsg->pack_);
+	case Message::InvokeMessage: {
+		InvokeMessage *iMsg = static_cast<InvokeMessage *>(msg);
+		iMsg->invoke();
 		break;
 	}
 
@@ -99,6 +100,28 @@ void Object::message(Message *msg)
 	}
 }
 
+/**
+ * \fn void Object::invokeMethod(void (T::*func)(Args...), Args... args)
+ * \brief Invoke a method asynchronously on an Object instance
+ * \param[in] func The object method to invoke
+ * \param[in] args The method arguments
+ *
+ * This method invokes the member method \a func when control returns to the
+ * event loop of the object's thread. The method is executed in the object's
+ * thread with arguments \a args.
+ *
+ * Arguments \a args passed by value or reference are copied, while pointers
+ * are passed untouched. The caller shall ensure that any pointer argument
+ * remains valid until the method is invoked.
+ */
+
+void Object::invokeMethod(BoundMethodBase *method, void *args)
+{
+	std::unique_ptr<Message> msg =
+		utils::make_unique<InvokeMessage>(method, args, true);
+	postMessage(std::move(msg));
+}
+
 /**
  * \fn Object::thread()
  * \brief Retrieve the thread the object is bound to
