feat: add Slot-returning overloads of async method calls (#433)

This commit is contained in:
Stanislav Angelovič
2024-04-18 19:53:35 +02:00
parent 310161207a
commit 83ece48ab0
12 changed files with 413 additions and 94 deletions

View File

@ -262,6 +262,7 @@ v2.0.0
- Fix for external event loops in which the event loop thread ID was not correctly initialized (now fixed and simplified by not needing the thread ID anymore)
- Introduce native integration for sd-event
- Add method to get currently processed message also to `IConnection`
- Add Slot-returning overloads of `callMethodAsync()` functions
- `[[nodiscard]]` attribute has been added to relevant API methods.
- Add new `SDBUSCPP_SDBUS_LIB` CMake configuration variable determining which sd-bus library shall be picked
- Switch to C++20 standard (but C++20 is not required, and the used C++20 features are conditionally compiled)

View File

@ -1231,6 +1231,10 @@ int main(int argc, char *argv[])
Empty `error` parameter means that no D-Bus error occurred while making the call, and subsequent arguments are valid D-Bus method return values. However, `error` parameter containing a value means that an error occurred during the call (and subsequent arguments are simply default-constructed), and the underlying `Error` instance provides us with the error name and message.
> **_Tip_:** The function returns the `sdbus::PendingAsyncCall` object, a non-owning, observing handle to the async call. It can be used to query whether the call is still in progress, and to cancel the call.
> **_Tip_:** There is also the `.uponReplyInvoke(callback, sdbus::return_slot);` variant with the `return_slot` tag, which returns `Slot` object, an owning RAII handle to the async call. This makes the client an owner of the pending async call. Letting go of the handle means cancelling the call.
Another option is to finish the async call statement with `getResultAsFuture()`, which is a template function which takes the list of types returned by the D-Bus method (empty list in case of `void`-returning method) which returns a `std::future` object, which will later, when the reply arrives, be set to contain the return value(s). Or if the call returns an error, `sdbus::Error` will be thrown by `std::future::get()`.
The future object will contain void for a void-returning D-Bus method, a single type for a single value returning D-Bus method, and a `std::tuple` to hold multiple return values of a D-Bus method.

View File

@ -131,6 +131,7 @@ namespace sdbus {
AsyncMethodInvoker& withTimeout(const std::chrono::duration<_Rep, _Period>& timeout);
template <typename... _Args> AsyncMethodInvoker& withArguments(_Args&&... args);
template <typename _Function> PendingAsyncCall uponReplyInvoke(_Function&& callback);
template <typename _Function> [[nodiscard]] Slot uponReplyInvoke(_Function&& callback, return_slot_t);
// Returned future will be std::future<void> for no (void) D-Bus method return value
// or std::future<T> for single D-Bus method return value
// or std::future<std::tuple<...>> for multiple method return values
@ -140,6 +141,7 @@ namespace sdbus {
friend IProxy;
AsyncMethodInvoker(IProxy& proxy, const MethodName& methodName);
AsyncMethodInvoker(IProxy& proxy, const char* methodName);
template <typename _Function> async_reply_handler makeAsyncReplyHandler(_Function&& callback);
private:
IProxy& proxy_;
@ -190,6 +192,7 @@ namespace sdbus {
public:
AsyncPropertyGetter& onInterface(std::string_view interfaceName);
template <typename _Function> PendingAsyncCall uponReplyInvoke(_Function&& callback);
template <typename _Function> [[nodiscard]] Slot uponReplyInvoke(_Function&& callback, return_slot_t);
std::future<Variant> getResultAsFuture();
private:
@ -232,6 +235,7 @@ namespace sdbus {
template <typename _Value> AsyncPropertySetter& toValue(_Value&& value);
AsyncPropertySetter& toValue(Variant value);
template <typename _Function> PendingAsyncCall uponReplyInvoke(_Function&& callback);
template <typename _Function> [[nodiscard]] Slot uponReplyInvoke(_Function&& callback, return_slot_t);
std::future<void> getResultAsFuture();
private:
@ -267,6 +271,7 @@ namespace sdbus {
public:
AsyncAllPropertiesGetter& onInterface(std::string_view interfaceName);
template <typename _Function> PendingAsyncCall uponReplyInvoke(_Function&& callback);
template <typename _Function> [[nodiscard]] Slot uponReplyInvoke(_Function&& callback, return_slot_t);
std::future<std::map<PropertyName, Variant>> getResultAsFuture();
private:

View File

@ -287,7 +287,24 @@ namespace sdbus {
{
assert(method_.isValid()); // onInterface() must be placed/called prior to this function
auto asyncReplyHandler = [callback = std::forward<_Function>(callback)](MethodReply reply, std::optional<Error> error)
return proxy_.callMethodAsync(method_, makeAsyncReplyHandler(std::forward<_Function>(callback)), timeout_);
}
template <typename _Function>
[[nodiscard]] Slot AsyncMethodInvoker::uponReplyInvoke(_Function&& callback, return_slot_t)
{
assert(method_.isValid()); // onInterface() must be placed/called prior to this function
return proxy_.callMethodAsync( method_
, makeAsyncReplyHandler(std::forward<_Function>(callback))
, timeout_
, return_slot );
}
template <typename _Function>
inline async_reply_handler AsyncMethodInvoker::makeAsyncReplyHandler(_Function&& callback)
{
return [callback = std::forward<_Function>(callback)](MethodReply reply, std::optional<Error> error)
{
// Create a tuple of callback input arguments' types, which will be used
// as a storage for the argument values deserialized from the message.
@ -312,8 +329,6 @@ namespace sdbus {
// Invoke callback with input arguments from the tuple.
sdbus::apply(callback, std::move(error), args);
};
return proxy_.callMethodAsync(method_, std::move(asyncReplyHandler), timeout_);
}
template <typename... _Args>
@ -474,7 +489,8 @@ namespace sdbus {
template <typename _Function>
PendingAsyncCall AsyncPropertyGetter::uponReplyInvoke(_Function&& callback)
{
static_assert(std::is_invocable_r_v<void, _Function, std::optional<Error>, Variant>, "Property get callback function must accept std::optional<Error> and property value as Variant");
static_assert( std::is_invocable_r_v<void, _Function, std::optional<Error>, Variant>
, "Property get callback function must accept std::optional<Error> and property value as Variant" );
assert(!interfaceName_.empty()); // onInterface() must be placed/called prior to this function
@ -484,6 +500,20 @@ namespace sdbus {
.uponReplyInvoke(std::forward<_Function>(callback));
}
template <typename _Function>
[[nodiscard]] Slot AsyncPropertyGetter::uponReplyInvoke(_Function&& callback, return_slot_t)
{
static_assert( std::is_invocable_r_v<void, _Function, std::optional<Error>, Variant>
, "Property get callback function must accept std::optional<Error> and property value as Variant" );
assert(!interfaceName_.empty()); // onInterface() must be placed/called prior to this function
return proxy_.callMethodAsync("Get")
.onInterface(DBUS_PROPERTIES_INTERFACE_NAME)
.withArguments(interfaceName_, propertyName_)
.uponReplyInvoke(std::forward<_Function>(callback), return_slot);
}
inline std::future<Variant> AsyncPropertyGetter::getResultAsFuture()
{
assert(!interfaceName_.empty()); // onInterface() must be placed/called prior to this function
@ -575,7 +605,8 @@ namespace sdbus {
template <typename _Function>
PendingAsyncCall AsyncPropertySetter::uponReplyInvoke(_Function&& callback)
{
static_assert(std::is_invocable_r_v<void, _Function, std::optional<Error>>, "Property set callback function must accept std::optional<Error> only");
static_assert( std::is_invocable_r_v<void, _Function, std::optional<Error>>
, "Property set callback function must accept std::optional<Error> only" );
assert(!interfaceName_.empty()); // onInterface() must be placed/called prior to this function
@ -585,6 +616,20 @@ namespace sdbus {
.uponReplyInvoke(std::forward<_Function>(callback));
}
template <typename _Function>
[[nodiscard]] Slot AsyncPropertySetter::uponReplyInvoke(_Function&& callback, return_slot_t)
{
static_assert( std::is_invocable_r_v<void, _Function, std::optional<Error>>
, "Property set callback function must accept std::optional<Error> only" );
assert(!interfaceName_.empty()); // onInterface() must be placed/called prior to this function
return proxy_.callMethodAsync("Set")
.onInterface(DBUS_PROPERTIES_INTERFACE_NAME)
.withArguments(interfaceName_, propertyName_, std::move(value_))
.uponReplyInvoke(std::forward<_Function>(callback), return_slot);
}
inline std::future<void> AsyncPropertySetter::getResultAsFuture()
{
assert(!interfaceName_.empty()); // onInterface() must be placed/called prior to this function
@ -644,6 +689,20 @@ namespace sdbus {
.uponReplyInvoke(std::forward<_Function>(callback));
}
template <typename _Function>
[[nodiscard]] Slot AsyncAllPropertiesGetter::uponReplyInvoke(_Function&& callback, return_slot_t)
{
static_assert( std::is_invocable_r_v<void, _Function, std::optional<Error>, std::map<PropertyName, Variant>>
, "All properties get callback function must accept std::optional<Error> and a map of property names to their values" );
assert(!interfaceName_.empty()); // onInterface() must be placed/called prior to this function
return proxy_.callMethodAsync("GetAll")
.onInterface(DBUS_PROPERTIES_INTERFACE_NAME)
.withArguments(interfaceName_)
.uponReplyInvoke(std::forward<_Function>(callback), return_slot);
}
inline std::future<std::map<PropertyName, Variant>> AsyncAllPropertiesGetter::getResultAsFuture()
{
assert(!interfaceName_.empty()); // onInterface() must be placed/called prior to this function

View File

@ -89,7 +89,6 @@ namespace sdbus {
* @brief Calls method on the remote D-Bus object
*
* @param[in] message Message representing a method call
* @param[in] timeout Timeout for dbus call in microseconds
* @return A method reply message
*
* The call does not block if the method call has dont-expect-reply flag set. In that case,
@ -108,11 +107,44 @@ namespace sdbus {
* its own bus connection. So-called light-weight proxies (ones created with `dont_run_event_loop_thread`
* tag are designed for exactly that purpose.
*
* The default D-Bus method call timeout is used. See IConnection::getMethodCallTimeout().
*
* Note: To avoid messing with messages, use API on a higher level of abstraction defined below.
*
* @throws sdbus::Error in case of failure (also in case the remote function returned an error)
*/
virtual MethodReply callMethod(const MethodCall& message, uint64_t timeout = 0) = 0;
virtual MethodReply callMethod(const MethodCall& message) = 0;
/*!
* @brief Calls method on the remote D-Bus object
*
* @param[in] message Message representing a method call
* @param[in] timeout Method call timeout (in microseconds)
* @return A method reply message
*
* The call does not block if the method call has dont-expect-reply flag set. In that case,
* the call returns immediately and the return value is an empty, invalid method reply.
*
* The call blocks otherwise, waiting for the remote peer to send back a reply or an error,
* or until the call times out.
*
* While blocking, other concurrent operations (in other threads) on the underlying bus
* connection are stalled until the call returns. This is not an issue in vast majority of
* (simple, single-threaded) applications. In asynchronous, multi-threaded designs involving
* shared bus connections, this may be an issue. It is advised to instead use an asynchronous
* callMethod() function overload, which does not block the bus connection, or do the synchronous
* call from another Proxy instance created just before the call and then destroyed (which is
* anyway quite a typical approach in D-Bus implementations). Such proxy instance must have
* its own bus connection. So-called light-weight proxies (ones created with `dont_run_event_loop_thread`
* tag are designed for exactly that purpose.
*
* If timeout is zero, the default D-Bus method call timeout is used. See IConnection::getMethodCallTimeout().
*
* Note: To avoid messing with messages, use API on a higher level of abstraction defined below.
*
* @throws sdbus::Error in case of failure (also in case the remote function returned an error)
*/
virtual MethodReply callMethod(const MethodCall& message, uint64_t timeout) = 0;
/*!
* @copydoc IProxy::callMethod(const MethodCall&,uint64_t)
@ -125,8 +157,7 @@ namespace sdbus {
*
* @param[in] message Message representing an async method call
* @param[in] asyncReplyCallback Handler for the async reply
* @param[in] timeout Timeout for dbus call in microseconds
* @return Cookie for the the pending asynchronous call
* @return Observing handle for the the pending asynchronous call
*
* This is a callback-based way of asynchronously calling a remote D-Bus method.
*
@ -134,13 +165,95 @@ namespace sdbus {
* the provided async reply handler will get invoked from the context of the bus
* connection I/O event loop thread.
*
* An non-owning, observing async call handle is returned that can be used to query call status or cancel the call.
*
* The default D-Bus method call timeout is used. See IConnection::getMethodCallTimeout().
*
* Note: To avoid messing with messages, use API on a higher level of abstraction defined below.
*
* @throws sdbus::Error in case of failure
*/
virtual PendingAsyncCall callMethodAsync(const MethodCall& message, async_reply_handler asyncReplyCallback) = 0;
/*!
* @brief Calls method on the D-Bus object asynchronously
*
* @param[in] message Message representing an async method call
* @param[in] asyncReplyCallback Handler for the async reply
* @return RAII-style slot handle representing the ownership of the async call
*
* This is a callback-based way of asynchronously calling a remote D-Bus method.
*
* The call itself is non-blocking. It doesn't wait for the reply. Once the reply arrives,
* the provided async reply handler will get invoked from the context of the bus
* connection I/O event loop thread.
*
* A slot (an owning handle) is returned for the async call. Lifetime of the call is bound to the lifetime of the slot.
* The slot can be used to cancel the method call at a later time by simply destroying it.
*
* The default D-Bus method call timeout is used. See IConnection::getMethodCallTimeout().
*
* Note: To avoid messing with messages, use API on a higher level of abstraction defined below.
*
* @throws sdbus::Error in case of failure
*/
[[nodiscard]] virtual Slot callMethodAsync( const MethodCall& message
, async_reply_handler asyncReplyCallback
, return_slot_t ) = 0;
/*!
* @brief Calls method on the D-Bus object asynchronously, with custom timeout
*
* @param[in] message Message representing an async method call
* @param[in] asyncReplyCallback Handler for the async reply
* @param[in] timeout Method call timeout (in microseconds)
* @return Observing handle for the the pending asynchronous call
*
* This is a callback-based way of asynchronously calling a remote D-Bus method.
*
* The call itself is non-blocking. It doesn't wait for the reply. Once the reply arrives,
* the provided async reply handler will get invoked from the context of the bus
* connection I/O event loop thread.
*
* An non-owning, observing async call handle is returned that can be used to query call status or cancel the call.
*
* If timeout is zero, the default D-Bus method call timeout is used. See IConnection::getMethodCallTimeout().
*
* Note: To avoid messing with messages, use API on a higher level of abstraction defined below.
*
* @throws sdbus::Error in case of failure
*/
virtual PendingAsyncCall callMethodAsync( const MethodCall& message
, async_reply_handler asyncReplyCallback
, uint64_t timeout = 0 ) = 0;
, uint64_t timeout ) = 0;
/*!
* @brief Calls method on the D-Bus object asynchronously, with custom timeout
*
* @param[in] message Message representing an async method call
* @param[in] asyncReplyCallback Handler for the async reply
* @param[in] timeout Method call timeout (in microseconds)
* @return RAII-style slot handle representing the ownership of the async call
*
* This is a callback-based way of asynchronously calling a remote D-Bus method.
*
* The call itself is non-blocking. It doesn't wait for the reply. Once the reply arrives,
* the provided async reply handler will get invoked from the context of the bus
* connection I/O event loop thread.
*
* A slot (an owning handle) is returned for the async call. Lifetime of the call is bound to the lifetime of the slot.
* The slot can be used to cancel the method call at a later time by simply destroying it.
*
* If timeout is zero, the default D-Bus method call timeout is used. See IConnection::getMethodCallTimeout().
*
* Note: To avoid messing with messages, use API on a higher level of abstraction defined below.
*
* @throws sdbus::Error in case of failure
*/
[[nodiscard]] virtual Slot callMethodAsync( const MethodCall& message
, async_reply_handler asyncReplyCallback
, uint64_t timeout
, return_slot_t ) = 0;
/*!
* @copydoc IProxy::callMethod(const MethodCall&,async_reply_handler,uint64_t)
@ -150,6 +263,15 @@ namespace sdbus {
, async_reply_handler asyncReplyCallback
, const std::chrono::duration<_Rep, _Period>& timeout );
/*!
* @copydoc IProxy::callMethod(const MethodCall&,async_reply_handler,uint64_t,return_slot_t)
*/
template <typename _Rep, typename _Period>
[[nodiscard]] Slot callMethodAsync( const MethodCall& message
, async_reply_handler asyncReplyCallback
, const std::chrono::duration<_Rep, _Period>& timeout
, return_slot_t );
/*!
* @brief Calls method on the D-Bus object asynchronously
*
@ -163,6 +285,8 @@ namespace sdbus {
* the provided future object will be set to contain the reply (or sdbus::Error
* in case the remote method threw an exception).
*
* The default D-Bus method call timeout is used. See IConnection::getMethodCallTimeout().
*
* Note: To avoid messing with messages, use higher-level API defined below.
*
* @throws sdbus::Error in case of failure
@ -183,6 +307,8 @@ namespace sdbus {
* the provided future object will be set to contain the reply (or sdbus::Error
* in case the remote method threw an exception, or the call timed out).
*
* If timeout is zero, the default D-Bus method call timeout is used. See IConnection::getMethodCallTimeout().
*
* Note: To avoid messing with messages, use higher-level API defined below.
*
* @throws sdbus::Error in case of failure
@ -273,8 +399,12 @@ namespace sdbus {
* @param[in] signalName Name of the signal
* @param[in] signalHandler Callback that implements the body of the signal handler
*
* A signal can be subscribed to and unsubscribed from at any time during proxy
* lifetime. The subscription is active immediately after the call.
* A signal can be subscribed to at any time during proxy lifetime. The subscription
* is active immediately after the call, and stays active for the entire lifetime
* of the Proxy object.
*
* To be able to unsubscribe from the signal at a later time, use the registerSignalHandler()
* overload with request_slot tag.
*
* @throws sdbus::Error in case of failure
*/
@ -292,8 +422,9 @@ namespace sdbus {
* @return RAII-style slot handle representing the ownership of the subscription
*
* A signal can be subscribed to and unsubscribed from at any time during proxy
* lifetime. The subscription is active immediately after the call. The subscription
* is unregistered when the client destroys the returned slot object.
* lifetime. The subscription is active immediately after the call. The lifetime
* of the subscription is bound to the lifetime of the slot object. The subscription
* is unregistered by letting go of the slot object.
*
* @throws sdbus::Error in case of failure
*/
@ -566,10 +697,10 @@ namespace sdbus {
private:
friend internal::Proxy;
PendingAsyncCall(std::weak_ptr<void> callData);
PendingAsyncCall(std::weak_ptr<void> callInfo);
private:
std::weak_ptr<void> callData_;
std::weak_ptr<void> callInfo_;
};
// Out-of-line member definitions
@ -590,6 +721,16 @@ namespace sdbus {
return callMethodAsync(message, std::move(asyncReplyCallback), microsecs.count());
}
template <typename _Rep, typename _Period>
inline Slot IProxy::callMethodAsync( const MethodCall& message
, async_reply_handler asyncReplyCallback
, const std::chrono::duration<_Rep, _Period>& timeout
, return_slot_t )
{
auto microsecs = std::chrono::duration_cast<std::chrono::microseconds>(timeout);
return callMethodAsync(message, std::move(asyncReplyCallback), microsecs.count(), return_slot);
}
template <typename _Rep, typename _Period>
inline std::future<MethodReply> IProxy::callMethodAsync( const MethodCall& message
, const std::chrono::duration<_Rep, _Period>& timeout

View File

@ -162,12 +162,24 @@ namespace sdbus {
return proxy_->getPropertyAsync(propertyName).onInterface(interfaceName).uponReplyInvoke(std::forward<_Function>(callback));
}
template <typename _Function>
[[nodiscard]] Slot GetAsync(const InterfaceName& interfaceName, const PropertyName& propertyName, _Function&& callback, return_slot_t)
{
return proxy_->getPropertyAsync(propertyName).onInterface(interfaceName).uponReplyInvoke(std::forward<_Function>(callback), return_slot);
}
template <typename _Function>
PendingAsyncCall GetAsync(std::string_view interfaceName, std::string_view propertyName, _Function&& callback)
{
return proxy_->getPropertyAsync(propertyName).onInterface(interfaceName).uponReplyInvoke(std::forward<_Function>(callback));
}
template <typename _Function>
[[nodiscard]] Slot GetAsync(std::string_view interfaceName, std::string_view propertyName, _Function&& callback, return_slot_t)
{
return proxy_->getPropertyAsync(propertyName).onInterface(interfaceName).uponReplyInvoke(std::forward<_Function>(callback), return_slot);
}
std::future<sdbus::Variant> GetAsync(const InterfaceName& interfaceName, const PropertyName& propertyName, with_future_t)
{
return proxy_->getPropertyAsync(propertyName).onInterface(interfaceName).getResultAsFuture();
@ -204,12 +216,24 @@ namespace sdbus {
return proxy_->setPropertyAsync(propertyName).onInterface(interfaceName).toValue(value).uponReplyInvoke(std::forward<_Function>(callback));
}
template <typename _Function>
PendingAsyncCall SetAsync(const InterfaceName& interfaceName, const PropertyName& propertyName, const sdbus::Variant& value, _Function&& callback, return_slot_t)
{
return proxy_->setPropertyAsync(propertyName).onInterface(interfaceName).toValue(value).uponReplyInvoke(std::forward<_Function>(callback), return_slot);
}
template <typename _Function>
PendingAsyncCall SetAsync(std::string_view interfaceName, std::string_view propertyName, const sdbus::Variant& value, _Function&& callback)
{
return proxy_->setPropertyAsync(propertyName).onInterface(interfaceName).toValue(value).uponReplyInvoke(std::forward<_Function>(callback));
}
template <typename _Function>
PendingAsyncCall SetAsync(std::string_view interfaceName, std::string_view propertyName, const sdbus::Variant& value, _Function&& callback, return_slot_t)
{
return proxy_->setPropertyAsync(propertyName).onInterface(interfaceName).toValue(value).uponReplyInvoke(std::forward<_Function>(callback), return_slot);
}
std::future<void> SetAsync(const InterfaceName& interfaceName, const PropertyName& propertyName, const sdbus::Variant& value, with_future_t)
{
return proxy_->setPropertyAsync(propertyName).onInterface(interfaceName).toValue(value).getResultAsFuture();
@ -236,12 +260,24 @@ namespace sdbus {
return proxy_->getAllPropertiesAsync().onInterface(interfaceName).uponReplyInvoke(std::forward<_Function>(callback));
}
template <typename _Function>
PendingAsyncCall GetAllAsync(const InterfaceName& interfaceName, _Function&& callback, return_slot_t)
{
return proxy_->getAllPropertiesAsync().onInterface(interfaceName).uponReplyInvoke(std::forward<_Function>(callback), return_slot);
}
template <typename _Function>
PendingAsyncCall GetAllAsync(std::string_view interfaceName, _Function&& callback)
{
return proxy_->getAllPropertiesAsync().onInterface(interfaceName).uponReplyInvoke(std::forward<_Function>(callback));
}
template <typename _Function>
PendingAsyncCall GetAllAsync(std::string_view interfaceName, _Function&& callback, return_slot_t)
{
return proxy_->getAllPropertiesAsync().onInterface(interfaceName).uponReplyInvoke(std::forward<_Function>(callback), return_slot);
}
std::future<std::map<PropertyName, sdbus::Variant>> GetAllAsync(const InterfaceName& interfaceName, with_future_t)
{
return proxy_->getAllPropertiesAsync().onInterface(interfaceName).getResultAsFuture();

View File

@ -84,7 +84,7 @@ namespace sdbus {
// Type-erased RAII-style handle to callbacks/subscriptions registered to sdbus-c++
using Slot = std::unique_ptr<void, std::function<void(void*)>>;
// Tag specifying that an owning slot handle shall be returned from a registration/subscription function to the caller
// Tag specifying that an owning handle (so-called slot) of the logical resource shall be provided to the client
struct return_slot_t { explicit return_slot_t() = default; };
inline constexpr return_slot_t return_slot{};
// Tag specifying that the library shall own the slot resulting from the call of the function (so-called floating slot)

View File

@ -94,6 +94,11 @@ MethodCall Proxy::createMethodCall(const char* interfaceName, const char* method
return connection_->createMethodCall(destination_.c_str(), objectPath_.c_str(), interfaceName, methodName);
}
MethodReply Proxy::callMethod(const MethodCall& message)
{
return Proxy::callMethod(message, /*timeout*/ 0);
}
MethodReply Proxy::callMethod(const MethodCall& message, uint64_t timeout)
{
SDBUS_THROW_ERROR_IF(!message.isValid(), "Invalid method call message provided", EINVAL);
@ -101,19 +106,44 @@ MethodReply Proxy::callMethod(const MethodCall& message, uint64_t timeout)
return connection_->callMethod(message, timeout);
}
PendingAsyncCall Proxy::callMethodAsync(const MethodCall& message, async_reply_handler asyncReplyCallback)
{
return Proxy::callMethodAsync(message, std::move(asyncReplyCallback), /*timeout*/ 0);
}
Slot Proxy::callMethodAsync(const MethodCall& message, async_reply_handler asyncReplyCallback, return_slot_t)
{
return Proxy::callMethodAsync(message, std::move(asyncReplyCallback), /*timeout*/ 0, return_slot);
}
PendingAsyncCall Proxy::callMethodAsync(const MethodCall& message, async_reply_handler asyncReplyCallback, uint64_t timeout)
{
SDBUS_THROW_ERROR_IF(!message.isValid(), "Invalid async method call message provided", EINVAL);
auto callback = (void*)&Proxy::sdbus_async_reply_handler;
auto callData = std::make_shared<AsyncCalls::CallData>(AsyncCalls::CallData{*this, std::move(asyncReplyCallback)});
auto weakData = std::weak_ptr<AsyncCalls::CallData>{callData};
auto asyncCallInfo = std::make_shared<AsyncCallInfo>(AsyncCallInfo{ .callback = std::move(asyncReplyCallback)
, .proxy = *this
, .floating = false });
callData->slot = connection_->callMethod(message, callback, callData.get(), timeout);
asyncCallInfo->slot = connection_->callMethod(message, (void*)&Proxy::sdbus_async_reply_handler, asyncCallInfo.get(), timeout);
pendingAsyncCalls_.addCall(std::move(callData));
auto asyncCallInfoWeakPtr = std::weak_ptr{asyncCallInfo};
return {weakData};
floatingAsyncCallSlots_.push_back(std::move(asyncCallInfo));
return {asyncCallInfoWeakPtr};
}
Slot Proxy::callMethodAsync(const MethodCall& message, async_reply_handler asyncReplyCallback, uint64_t timeout, return_slot_t)
{
SDBUS_THROW_ERROR_IF(!message.isValid(), "Invalid async method call message provided", EINVAL);
auto asyncCallInfo = std::make_unique<AsyncCallInfo>(AsyncCallInfo{ .callback = std::move(asyncReplyCallback)
, .proxy = *this
, .floating = true });
asyncCallInfo->slot = connection_->callMethod(message, (void*)&Proxy::sdbus_async_reply_handler, asyncCallInfo.get(), timeout);
return {asyncCallInfo.release(), [](void *ptr){ delete static_cast<AsyncCallInfo*>(ptr); }};
}
std::future<MethodReply> Proxy::callMethodAsync(const MethodCall& message, with_future_t)
@ -186,7 +216,7 @@ Slot Proxy::registerSignalHandler( const char* interfaceName
void Proxy::unregister()
{
pendingAsyncCalls_.clear();
floatingAsyncCallSlots_.clear();
floatingSignalSlots_.clear();
}
@ -207,17 +237,17 @@ Message Proxy::getCurrentlyProcessedMessage() const
int Proxy::sdbus_async_reply_handler(sd_bus_message *sdbusMessage, void *userData, sd_bus_error *retError)
{
auto* asyncCallData = static_cast<AsyncCalls::CallData*>(userData);
assert(asyncCallData != nullptr);
assert(asyncCallData->callback);
auto& proxy = asyncCallData->proxy;
auto* asyncCallInfo = static_cast<AsyncCallInfo*>(userData);
assert(asyncCallInfo != nullptr);
assert(asyncCallInfo->callback);
auto& proxy = asyncCallInfo->proxy;
// We are removing the CallData item at the complete scope exit, after the callback has been invoked.
// We can't do it earlier (before callback invocation for example), because CallBack data (slot release)
// is the synchronization point between callback invocation and Proxy::unregister.
SCOPE_EXIT
{
proxy.pendingAsyncCalls_.removeCall(asyncCallData);
proxy.floatingAsyncCallSlots_.erase(asyncCallInfo);
};
auto message = Message::Factory::create<MethodReply>(sdbusMessage, &proxy.connection_->getSdBusInterface());
@ -227,12 +257,12 @@ int Proxy::sdbus_async_reply_handler(sd_bus_message *sdbusMessage, void *userDat
const auto* error = sd_bus_message_get_error(sdbusMessage);
if (error == nullptr)
{
asyncCallData->callback(std::move(message), {});
asyncCallInfo->callback(std::move(message), {});
}
else
{
Error exception(Error::Name{error->name}, error->message);
asyncCallData->callback(std::move(message), std::move(exception));
asyncCallInfo->callback(std::move(message), std::move(exception));
}
}, retError);
@ -253,21 +283,64 @@ int Proxy::sdbus_signal_handler(sd_bus_message *sdbusMessage, void *userData, sd
return ok ? 0 : -1;
}
Proxy::FloatingAsyncCallSlots::~FloatingAsyncCallSlots()
{
clear();
}
void Proxy::FloatingAsyncCallSlots::push_back(std::shared_ptr<AsyncCallInfo> asyncCallInfo)
{
std::lock_guard lock(mutex_);
if (!asyncCallInfo->finished) // The call may have finished in the meantime
slots_.emplace_back(std::move(asyncCallInfo));
}
void Proxy::FloatingAsyncCallSlots::erase(AsyncCallInfo* info)
{
std::unique_lock lock(mutex_);
info->finished = true;
auto it = std::find_if(slots_.begin(), slots_.end(), [info](auto const& entry){ return entry.get() == info; });
if (it != slots_.end())
{
auto callInfo = std::move(*it);
slots_.erase(it);
lock.unlock();
// Releasing call slot pointer acquires global sd-bus mutex. We have to perform the release
// out of the `mutex_' critical section here, because if the `removeCall` is called by some
// thread and at the same time Proxy's async reply handler (which already holds global sd-bus
// mutex) is in progress in a different thread, we get double-mutex deadlock.
}
}
void Proxy::FloatingAsyncCallSlots::clear()
{
std::unique_lock lock(mutex_);
auto asyncCallSlots = std::move(slots_);
slots_ = {};
lock.unlock();
// Releasing call slot pointer acquires global sd-bus mutex. We have to perform the release
// out of the `mutex_' critical section here, because if the `clear` is called by some thread
// and at the same time Proxy's async reply handler (which already holds global sd-bus
// mutex) is in progress in a different thread, we get double-mutex deadlock.
}
}
namespace sdbus {
PendingAsyncCall::PendingAsyncCall(std::weak_ptr<void> callData)
: callData_(std::move(callData))
PendingAsyncCall::PendingAsyncCall(std::weak_ptr<void> callInfo)
: callInfo_(std::move(callInfo))
{
}
void PendingAsyncCall::cancel()
{
if (auto ptr = callData_.lock(); ptr != nullptr)
if (auto ptr = callInfo_.lock(); ptr != nullptr)
{
auto* callData = static_cast<internal::Proxy::AsyncCalls::CallData*>(ptr.get());
callData->proxy.pendingAsyncCalls_.removeCall(callData);
auto* asyncCallInfo = static_cast<internal::Proxy::AsyncCallInfo*>(ptr.get());
asyncCallInfo->proxy.floatingAsyncCallSlots_.erase(asyncCallInfo);
// At this point, the callData item is being deleted, leading to the release of the
// sd-bus slot pointer. This release locks the global sd-bus mutex. If the async
@ -278,7 +351,7 @@ void PendingAsyncCall::cancel()
bool PendingAsyncCall::isPending() const
{
return !callData_.expired();
return !callInfo_.expired();
}
}

View File

@ -58,8 +58,19 @@ namespace sdbus::internal {
MethodCall createMethodCall(const InterfaceName& interfaceName, const MethodName& methodName) override;
MethodCall createMethodCall(const char* interfaceName, const char* methodName) override;
MethodReply callMethod(const MethodCall& message) override;
MethodReply callMethod(const MethodCall& message, uint64_t timeout) override;
PendingAsyncCall callMethodAsync(const MethodCall& message, async_reply_handler asyncReplyCallback, uint64_t timeout) override;
PendingAsyncCall callMethodAsync(const MethodCall& message, async_reply_handler asyncReplyCallback) override;
Slot callMethodAsync( const MethodCall& message
, async_reply_handler asyncReplyCallback
, return_slot_t ) override;
PendingAsyncCall callMethodAsync( const MethodCall& message
, async_reply_handler asyncReplyCallback
, uint64_t timeout ) override;
Slot callMethodAsync( const MethodCall& message
, async_reply_handler asyncReplyCallback
, uint64_t timeout
, return_slot_t ) override;
std::future<MethodReply> callMethodAsync(const MethodCall& message, with_future_t) override;
std::future<MethodReply> callMethodAsync(const MethodCall& message, uint64_t timeout, with_future_t) override;
@ -105,67 +116,30 @@ namespace sdbus::internal {
Slot slot;
};
// We need to keep track of pending async calls. When the proxy is being destructed, we must
// remove all slots of these pending calls, otherwise in case when the connection outlives
// the proxy, we might get async reply handlers invoked for pending async calls after the proxy
// has been destroyed, which is a free ticket into the realm of undefined behavior.
class AsyncCalls
struct AsyncCallInfo
{
async_reply_handler callback;
Proxy& proxy;
Slot slot{};
bool finished{false};
bool floating;
};
// Container keeping track of pending async calls
class FloatingAsyncCallSlots
{
public:
struct CallData
{
Proxy& proxy;
async_reply_handler callback;
Slot slot{};
bool finished{false};
};
~AsyncCalls()
{
clear();
}
void addCall(std::shared_ptr<CallData> asyncCallData)
{
std::lock_guard lock(mutex_);
if (!asyncCallData->finished) // The call may have finished in the meantime
calls_.emplace_back(std::move(asyncCallData));
}
void removeCall(CallData* data)
{
std::unique_lock lock(mutex_);
data->finished = true;
if (auto it = std::find_if(calls_.begin(), calls_.end(), [data](auto const& entry){ return entry.get() == data; }); it != calls_.end())
{
auto callData = std::move(*it);
calls_.erase(it);
lock.unlock();
// Releasing call slot pointer acquires global sd-bus mutex. We have to perform the release
// out of the `mutex_' critical section here, because if the `removeCall` is called by some
// thread and at the same time Proxy's async reply handler (which already holds global sd-bus
// mutex) is in progress in a different thread, we get double-mutex deadlock.
}
}
void clear()
{
std::unique_lock lock(mutex_);
auto asyncCallSlots = std::move(calls_);
calls_ = {};
lock.unlock();
// Releasing call slot pointer acquires global sd-bus mutex. We have to perform the release
// out of the `mutex_' critical section here, because if the `clear` is called by some thread
// and at the same time Proxy's async reply handler (which already holds global sd-bus
// mutex) is in progress in a different thread, we get double-mutex deadlock.
}
~FloatingAsyncCallSlots();
void push_back(std::shared_ptr<AsyncCallInfo> asyncCallInfo);
void erase(AsyncCallInfo* info);
void clear();
private:
std::mutex mutex_;
std::deque<std::shared_ptr<CallData>> calls_;
} pendingAsyncCalls_;
std::deque<std::shared_ptr<AsyncCallInfo>> slots_;
};
FloatingAsyncCallSlots floatingAsyncCallSlots_;
};
}

View File

@ -198,6 +198,20 @@ TYPED_TEST(AsyncSdbusTestObject, CancelsPendingAsyncCallOnClientSide)
ASSERT_THAT(future.wait_for(300ms), Eq(std::future_status::timeout));
}
TYPED_TEST(AsyncSdbusTestObject, CancelsPendingAsyncCallOnClientSideByDestroyingOwningSlot)
{
std::promise<uint32_t> promise;
auto future = promise.get_future();
this->m_proxy->installDoOperationClientSideAsyncReplyHandler([&](uint32_t /*res*/, std::optional<sdbus::Error> /*err*/){ promise.set_value(1); });
{
auto slot = this->m_proxy->doOperationClientSideAsync(100, sdbus::return_slot);
// Now the slot is destroyed, cancelling the async call
}
ASSERT_THAT(future.wait_for(300ms), Eq(std::future_status::timeout));
}
TYPED_TEST(AsyncSdbusTestObject, AnswersThatAsyncCallIsNotPendingAfterItHasBeenCancelled)
{
std::promise<uint32_t> promise;

View File

@ -124,6 +124,17 @@ sdbus::PendingAsyncCall TestProxy::doOperationClientSideAsync(uint32_t param)
});
}
Slot TestProxy::doOperationClientSideAsync(uint32_t param, sdbus::return_slot_t)
{
return getProxy().callMethodAsync("doOperation")
.onInterface(sdbus::test::INTERFACE_NAME)
.withArguments(param)
.uponReplyInvoke([this](std::optional<sdbus::Error> error, uint32_t returnValue)
{
this->onDoOperationReply(returnValue, std::move(error));
}, sdbus::return_slot);
}
std::future<uint32_t> TestProxy::doOperationClientSideAsync(uint32_t param, with_future_t)
{
return getProxy().callMethodAsync("doOperation")

View File

@ -96,6 +96,7 @@ public:
void installDoOperationClientSideAsyncReplyHandler(std::function<void(uint32_t res, std::optional<sdbus::Error> err)> handler);
uint32_t doOperationWithTimeout(const std::chrono::microseconds &timeout, uint32_t param);
sdbus::PendingAsyncCall doOperationClientSideAsync(uint32_t param);
[[nodiscard]] sdbus::Slot doOperationClientSideAsync(uint32_t param, sdbus::return_slot_t);
std::future<uint32_t> doOperationClientSideAsync(uint32_t param, with_future_t);
std::future<MethodReply> doOperationClientSideAsyncOnBasicAPILevel(uint32_t param);
std::future<void> doErroneousOperationClientSideAsync(with_future_t);