From 737f04abc7cc6677e042d1641fda8661bed6c299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20Angelovi=C4=8D?= Date: Sun, 29 Jan 2023 22:12:10 +0100 Subject: [PATCH] feat: add support for std::future-based async calls --- docs/using-sdbus-c++.md | 50 +++++++++++++++++-- include/sdbus-c++/ConvenienceApiClasses.h | 5 ++ include/sdbus-c++/ConvenienceApiClasses.inl | 23 +++++++++ include/sdbus-c++/IProxy.h | 43 ++++++++++++++-- include/sdbus-c++/TypeTraits.h | 27 ++++++++++ src/Proxy.cpp | 23 +++++++++ src/Proxy.h | 2 + .../DBusAsyncMethodsTests.cpp | 27 +++++++++- tests/integrationtests/TestProxy.cpp | 23 +++++++++ tests/integrationtests/TestProxy.h | 4 ++ 10 files changed, 218 insertions(+), 9 deletions(-) diff --git a/docs/using-sdbus-c++.md b/docs/using-sdbus-c++.md index dad215d..53568da 100644 --- a/docs/using-sdbus-c++.md +++ b/docs/using-sdbus-c++.md @@ -1066,11 +1066,11 @@ For a real example of a server-side asynchronous D-Bus method, please look at sd Asynchronous client-side methods -------------------------------- -sdbus-c++ also supports asynchronous approach at the client (the proxy) side. With this approach, we can issue a D-Bus method call without blocking current thread's execution while waiting for the reply. We go on doing other things, and when the reply comes, a given callback is invoked within the context of the D-Bus dispatcher thread. +sdbus-c++ also supports asynchronous approach at the client (the proxy) side. With this approach, we can issue a D-Bus method call without blocking current thread's execution while waiting for the reply. We go on doing other things, and when the reply comes, either a given callback handler will be invoked within the context of the event loop thread, or a future object returned by the async call will be set the returned value.6 ### Lower-level API -Considering the Concatenator example based on lower-level API, if we wanted to call `concatenate` in an async way, we'd have to pass a callback to the proxy when issuing the call, and that callback gets invoked when the reply arrives: +Considering the Concatenator example based on lower-level API, if we wanted to call `concatenate` in an async way, we have two options: We either pass a callback to the proxy when issuing the call, and that callback gets invoked when the reply arrives: ```c++ int main(int argc, char *argv[]) @@ -1115,9 +1115,34 @@ int main(int argc, char *argv[]) The callback is a void-returning function taking two arguments: a reference to the reply message, and a pointer to the prospective `sdbus::Error` instance. Zero `Error` pointer means that no D-Bus error occurred while making the call, and the reply message contains valid reply. Non-zero `Error` pointer, however, points to the valid `Error` instance, meaning that an error occurred. Error name and message can then be read out by the client from that instance. +There is also an overload of this `IProxy::callMethod()` function taking method call timeout argument. + +Another option is to use `std::future`-based overload of the `IProxy::callMethod()` function. A future object will be returned which will later, when the reply arrives, be set to contain the returned reply message. Or if the call returns an error, `sdbus::Error` will be thrown by `std::future::get()`. + +```c++ + ... + // Invoke concatenate on given interface of the object + { + auto method = concatenatorProxy->createMethodCall(interfaceName, "concatenate"); + method << numbers << separator; + auto future = concatenatorProxy->callMethod(method, sdbus::with_future); + try + { + auto reply = future.get(); // This will throw if call ends with an error + std::string result; + reply >> result; + std::cout << "Got concatenate result: " << result << std::endl; + } + catch (const sdbus::Error& e) + { + std::cerr << "Got concatenate error " << e.getName() << " with message " << e.getMessage() << std::endl; + } + } +``` + ### Convenience API -On the convenience API level, the call statement starts with `callMethodAsync()`, and ends with `uponReplyInvoke()` that takes a callback handler. The callback is a void-returning function that takes at least one argument: pointer to the `sdbus::Error` instance. All subsequent arguments shall exactly reflect the D-Bus method output arguments. A concatenator example: +On the convenience API level, the call statement starts with `callMethodAsync()`, and one option is to finish the statement with `uponReplyInvoke()` that takes a callback handler. The callback is a void-returning function that takes at least one argument: pointer to the `sdbus::Error` instance. All subsequent arguments shall exactly reflect the D-Bus method output arguments. A concatenator example: ```c++ int main(int argc, char *argv[]) @@ -1152,6 +1177,25 @@ int main(int argc, char *argv[]) When the `Error` pointer is zero, it means that no D-Bus error occurred while making the call, and subsequent arguments are valid D-Bus method return values. Non-zero `Error` pointer, however, points to the valid `Error` instance, meaning that an error occurred during the call (and subsequent arguments are simply default-constructed). Error name and message can then be read out by the client from `Error` instance. +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. + +```c++ + ... + auto future = concatenatorProxy->callMethodAsync("concatenate").onInterface(interfaceName).withArguments(numbers, separator).getResultAsFuture(); + try + { + auto concatenatedString = future.get(); // This waits for the reply + std::cout << "Got concatenate result: " << concatenatedString << std::endl; + } + catch (const sdbus::Error& e) + { + std::cerr << "Got concatenate error " << e.getName() << " with message " << e.getMessage() << std::endl; + } + ... +``` + ### Marking client-side async methods in the IDL sdbus-c++ stub generator can generate stub code for client-side async methods. We just need to annotate the method with `org.freedesktop.DBus.Method.Async`. The annotation element value must be either `client` (async on the client-side only) or `clientserver` (async method on both client- and server-side): diff --git a/include/sdbus-c++/ConvenienceApiClasses.h b/include/sdbus-c++/ConvenienceApiClasses.h index 2f8ce0c..6f6996c 100644 --- a/include/sdbus-c++/ConvenienceApiClasses.h +++ b/include/sdbus-c++/ConvenienceApiClasses.h @@ -34,6 +34,7 @@ #include #include #include +#include #include // Forward declarations @@ -195,6 +196,10 @@ namespace sdbus { AsyncMethodInvoker& withTimeout(const std::chrono::duration<_Rep, _Period>& timeout); template AsyncMethodInvoker& withArguments(_Args&&... args); template PendingAsyncCall uponReplyInvoke(_Function&& callback); + // Returned future will be std::future for no (void) D-Bus method return value + // or std::future for single D-Bus method return value + // or std::future> for multiple method return values + template std::future> getResultAsFuture(); private: IProxy& proxy_; diff --git a/include/sdbus-c++/ConvenienceApiClasses.inl b/include/sdbus-c++/ConvenienceApiClasses.inl index 984a552..6beb411 100644 --- a/include/sdbus-c++/ConvenienceApiClasses.inl +++ b/include/sdbus-c++/ConvenienceApiClasses.inl @@ -610,6 +610,29 @@ namespace sdbus { return proxy_.callMethod(method_, std::move(asyncReplyHandler), timeout_); } + template + std::future> AsyncMethodInvoker::getResultAsFuture() + { + auto promise = std::make_shared>>(); + auto future = promise->get_future(); + + uponReplyInvoke([promise = std::move(promise)](const Error* error, _Args... args) + { + if (error == nullptr) + if constexpr (!std::is_void_v>) + promise->set_value({std::move(args)...}); + else + promise->set_value(); + else + promise->set_exception(std::make_exception_ptr(*error)); + }); + + // Will be std::future for no D-Bus method return value + // or std::future for single D-Bus method return value + // or std::future> for multiple method return values + return future; + } + /*** ---------------- ***/ /*** SignalSubscriber ***/ /*** ---------------- ***/ diff --git a/include/sdbus-c++/IProxy.h b/include/sdbus-c++/IProxy.h index a1cc461..a055872 100644 --- a/include/sdbus-c++/IProxy.h +++ b/include/sdbus-c++/IProxy.h @@ -28,10 +28,12 @@ #define SDBUS_CXX_IPROXY_H_ #include +#include #include #include #include #include +#include // Forward declarations namespace sdbus { @@ -322,6 +324,33 @@ namespace sdbus { * @return A pointer to the currently processed D-Bus message */ virtual const Message* getCurrentlyProcessedMessage() const = 0; + + /*! + * @brief Calls method on the proxied D-Bus object asynchronously + * + * @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 + * + * The call 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 connection + * I/O event loop thread. + * + * Note: To avoid messing with messages, use higher-level API defined below. + * + * @throws sdbus::Error in case of failure + */ + virtual std::future callMethod(const MethodCall& message, with_future_t) = 0; + virtual std::future callMethod(const MethodCall& message, uint64_t timeout, with_future_t) = 0; + + /*! + * @copydoc IProxy::callMethod(const MethodCall&,uint64_t,with_future_t) + */ + template + std::future callMethod( const MethodCall& message + , const std::chrono::duration<_Rep, _Period>& timeout + , with_future_t ); }; /********************************************//** @@ -382,6 +411,15 @@ namespace sdbus { return callMethod(message, std::move(asyncReplyCallback), microsecs.count()); } + template + inline std::future IProxy::callMethod( const MethodCall& message + , const std::chrono::duration<_Rep, _Period>& timeout + , with_future_t ) + { + auto microsecs = std::chrono::duration_cast(timeout); + return callMethod(message, microsecs.count(), with_future); + } + inline MethodInvoker IProxy::callMethod(const std::string& methodName) { return MethodInvoker(*this, methodName); @@ -412,11 +450,6 @@ namespace sdbus { return PropertySetter(*this, propertyName); } - // Tag specifying that the proxy shall not run an event loop thread on its D-Bus connection. - // Such proxies are typically created to carry out a simple synchronous D-Bus call(s) and then are destroyed. - struct dont_run_event_loop_thread_t { explicit dont_run_event_loop_thread_t() = default; }; - inline constexpr dont_run_event_loop_thread_t dont_run_event_loop_thread{}; - /*! * @brief Creates a proxy object for a specific remote D-Bus object * diff --git a/include/sdbus-c++/TypeTraits.h b/include/sdbus-c++/TypeTraits.h index 0423fd6..960d479 100644 --- a/include/sdbus-c++/TypeTraits.h +++ b/include/sdbus-c++/TypeTraits.h @@ -81,6 +81,13 @@ namespace sdbus { // Tag denoting the assumption that the caller has already obtained fd ownership struct adopt_fd_t { explicit adopt_fd_t() = default; }; inline constexpr adopt_fd_t adopt_fd{}; + // Tag specifying that the proxy shall not run an event loop thread on its D-Bus connection. + // Such proxies are typically created to carry out a simple synchronous D-Bus call(s) and then are destroyed. + struct dont_run_event_loop_thread_t { explicit dont_run_event_loop_thread_t() = default; }; + inline constexpr dont_run_event_loop_thread_t dont_run_event_loop_thread{}; + // Tag denoting an asynchronous call that returns std::future as a handle + struct with_future_t { explicit with_future_t() = default; }; + inline constexpr with_future_t with_future{}; // Template specializations for getting D-Bus signatures from C++ types template @@ -540,6 +547,26 @@ namespace sdbus { } }; + + template struct future_return + { + typedef std::tuple<_Args...> type; + }; + + template <> struct future_return<> + { + typedef void type; + }; + + template struct future_return<_Type> + { + typedef _Type type; + }; + + template + using future_return_t = typename future_return<_Args...>::type; + + namespace detail { template diff --git a/src/Proxy.cpp b/src/Proxy.cpp index 8e03bb3..322415a 100644 --- a/src/Proxy.cpp +++ b/src/Proxy.cpp @@ -131,6 +131,29 @@ PendingAsyncCall Proxy::callMethod(const MethodCall& message, async_reply_handle return {weakData}; } +std::future Proxy::callMethod(const MethodCall& message, with_future_t) +{ + return Proxy::callMethod(message, {}, with_future); +} + +std::future Proxy::callMethod(const MethodCall& message, uint64_t timeout, with_future_t) +{ + auto promise = std::make_shared>(); + auto future = promise->get_future(); + + async_reply_handler asyncReplyCallback = [promise = std::move(promise)](MethodReply& reply, const Error* error) noexcept + { + if (error == nullptr) + promise->set_value(reply); // TODO: std::move? Can't move now because currently processed message. TODO: Refactor + else + promise->set_exception(std::make_exception_ptr(*error)); + }; + + (void)Proxy::callMethod(message, std::move(asyncReplyCallback), timeout); + + return future; +} + MethodReply Proxy::sendMethodCallMessageAndWaitForReply(const MethodCall& message, uint64_t timeout) { /*thread_local*/ SyncCallReplyData syncCallReplyData; diff --git a/src/Proxy.h b/src/Proxy.h index 3ab3a33..0f9cf25 100644 --- a/src/Proxy.h +++ b/src/Proxy.h @@ -58,6 +58,8 @@ namespace sdbus::internal { MethodCall createMethodCall(const std::string& interfaceName, const std::string& methodName) override; MethodReply callMethod(const MethodCall& message, uint64_t timeout) override; PendingAsyncCall callMethod(const MethodCall& message, async_reply_handler asyncReplyCallback, uint64_t timeout) override; + std::future callMethod(const MethodCall& message, with_future_t) override; + std::future callMethod(const MethodCall& message, uint64_t timeout, with_future_t) override; void registerSignalHandler( const std::string& interfaceName , const std::string& signalName diff --git a/tests/integrationtests/DBusAsyncMethodsTests.cpp b/tests/integrationtests/DBusAsyncMethodsTests.cpp index 29cb220..1ee1f73 100644 --- a/tests/integrationtests/DBusAsyncMethodsTests.cpp +++ b/tests/integrationtests/DBusAsyncMethodsTests.cpp @@ -161,6 +161,24 @@ TEST_F(SdbusTestObject, InvokesMethodAsynchronouslyOnClientSide) ASSERT_THAT(future.get(), Eq(100)); } +TEST_F(SdbusTestObject, InvokesMethodAsynchronouslyOnClientSideWithFuture) +{ + auto future = m_proxy->doOperationClientSideAsync(100, sdbus::with_future); + + ASSERT_THAT(future.get(), Eq(100)); +} + +TEST_F(SdbusTestObject, InvokesMethodAsynchronouslyOnClientSideWithFutureOnBasicAPILevel) +{ + auto future = m_proxy->doOperationClientSideAsyncOnBasicAPILevel(100); + + auto methodReply = future.get(); + uint32_t returnValue{}; + methodReply >> returnValue; + + ASSERT_THAT(returnValue, Eq(100)); +} + TEST_F(SdbusTestObject, AnswersThatAsyncCallIsPendingIfItIsInProgress) { m_proxy->installDoOperationClientSideAsyncReplyHandler([&](uint32_t /*res*/, const sdbus::Error* /*err*/){}); @@ -222,7 +240,7 @@ TEST_F(SdbusTestObject, SupportsAsyncCallCopyAssignment) ASSERT_TRUE(call.isPending()); } -TEST_F(SdbusTestObject, InvokesErroneousMethodAsynchronouslyOnClientSide) +TEST_F(SdbusTestObject, ReturnsNonnullErrorWhenAsynchronousMethodCallFails) { std::promise promise; auto future = promise.get_future(); @@ -238,3 +256,10 @@ TEST_F(SdbusTestObject, InvokesErroneousMethodAsynchronouslyOnClientSide) ASSERT_THROW(future.get(), sdbus::Error); } + +TEST_F(SdbusTestObject, ThrowsErrorWhenClientSideAsynchronousMethodCallWithFutureFails) +{ + auto future = m_proxy->doErroneousOperationClientSideAsync(sdbus::with_future); + + ASSERT_THROW(future.get(), sdbus::Error); +} diff --git a/tests/integrationtests/TestProxy.cpp b/tests/integrationtests/TestProxy.cpp index 3b8a27b..b0777da 100644 --- a/tests/integrationtests/TestProxy.cpp +++ b/tests/integrationtests/TestProxy.cpp @@ -123,6 +123,22 @@ sdbus::PendingAsyncCall TestProxy::doOperationClientSideAsync(uint32_t param) }); } +std::future TestProxy::doOperationClientSideAsync(uint32_t param, with_future_t) +{ + return getProxy().callMethodAsync("doOperation") + .onInterface(sdbus::test::INTERFACE_NAME) + .withArguments(param) + .getResultAsFuture(); +} + +std::future TestProxy::doOperationClientSideAsyncOnBasicAPILevel(uint32_t param) +{ + auto methodCall = getProxy().createMethodCall(sdbus::test::INTERFACE_NAME, "doOperation"); + methodCall << param; + + return getProxy().callMethod(methodCall, sdbus::with_future); +} + void TestProxy::doErroneousOperationClientSideAsync() { getProxy().callMethodAsync("throwError") @@ -133,6 +149,13 @@ void TestProxy::doErroneousOperationClientSideAsync() }); } +std::future TestProxy::doErroneousOperationClientSideAsync(with_future_t) +{ + return getProxy().callMethodAsync("throwError") + .onInterface(sdbus::test::INTERFACE_NAME) + .getResultAsFuture<>();; +} + void TestProxy::doOperationClientSideAsyncWithTimeout(const std::chrono::microseconds &timeout, uint32_t param) { using namespace std::chrono_literals; diff --git a/tests/integrationtests/TestProxy.h b/tests/integrationtests/TestProxy.h index 37fff82..d2d1a1a 100644 --- a/tests/integrationtests/TestProxy.h +++ b/tests/integrationtests/TestProxy.h @@ -32,6 +32,7 @@ #include #include #include +#include namespace sdbus { namespace test { @@ -94,6 +95,9 @@ public: void installDoOperationClientSideAsyncReplyHandler(std::function handler); uint32_t doOperationWithTimeout(const std::chrono::microseconds &timeout, uint32_t param); sdbus::PendingAsyncCall doOperationClientSideAsync(uint32_t param); + std::future doOperationClientSideAsync(uint32_t param, with_future_t); + std::future doOperationClientSideAsyncOnBasicAPILevel(uint32_t param); + std::future doErroneousOperationClientSideAsync(with_future_t); void doErroneousOperationClientSideAsync(); void doOperationClientSideAsyncWithTimeout(const std::chrono::microseconds &timeout, uint32_t param); int32_t callNonexistentMethod();