From b7efcddafcf188bb3dfdf708e1ae2c6502d32b16 Mon Sep 17 00:00:00 2001 From: Alex Cani Date: Sun, 1 Mar 2026 15:07:44 +0100 Subject: [PATCH 01/15] chore(git): add vscode directory to ignore list Include .vscode/ in .gitignore to prevent IDE-specific files from being tracked in the repository. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 097e5ff2..d1e17071 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ tests/run-test-on-device.sh .settings *.log +# vscode +.vscode/ + #autotools sdbus-cpp.pc *Makefile From eae54ea608c4f182505b82b72efd99a8f2f4fb7d Mon Sep 17 00:00:00 2001 From: Alex Cani Date: Sun, 1 Mar 2026 15:00:42 +0100 Subject: [PATCH 02/15] feat(awaitable): add Awaitable class Prepare for C++20 coroutine support for asynchronous D-Bus method calls by introducing the Awaitable class, enabling co_await syntax. This class contains the three necessary methods to support the co_await operator: await_ready, await_suspend and await_resume. A shared data structure is used to hold data needed by the callback that will resume the coroutine and feed in the result or exception. This work similarly to how a future/promise pair work. Potential race conditions in the multithreaded scenario are covered by atomic operations on an AwaitableState value, preventing situations that could lead to a double coroutine resume or permanent suspension. --- CMakeLists.txt | 1 + include/sdbus-c++/Awaitable.h | 135 ++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 include/sdbus-c++/Awaitable.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cf200fdb..6a8632cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -164,6 +164,7 @@ set(SDBUSCPP_HDR_SRCS ${SDBUSCPP_SOURCE_DIR}/ISdBus.h) set(SDBUSCPP_PUBLIC_HDRS + ${SDBUSCPP_INCLUDE_DIR}/Awaitable.h ${SDBUSCPP_INCLUDE_DIR}/ConvenienceApiClasses.h ${SDBUSCPP_INCLUDE_DIR}/ConvenienceApiClasses.inl ${SDBUSCPP_INCLUDE_DIR}/VTableItems.h diff --git a/include/sdbus-c++/Awaitable.h b/include/sdbus-c++/Awaitable.h new file mode 100644 index 00000000..bbcca71b --- /dev/null +++ b/include/sdbus-c++/Awaitable.h @@ -0,0 +1,135 @@ +/** + * (C) 2016 - 2021 KISTLER INSTRUMENTE AG, Winterthur, Switzerland + * (C) 2016 - 2026 Stanislav Angelovic + * (C) 2026 - Alex Cani + * + * @file Awaitable.h + * + * Created on: Feb 28, 2026 + * Project: sdbus-c++ + * Description: High-level D-Bus IPC C++ library based on sd-bus + * + * This file is part of sdbus-c++. + * + * sdbus-c++ is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the License, or + * (at your option) any later version. + * + * sdbus-c++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with sdbus-c++. If not, see . + */ + +#ifndef SDBUS_CXX_AWAITABLE_H_ +#define SDBUS_CXX_AWAITABLE_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sdbus +{ + +/********************************************//** + * @enum AwaitableState + * + * Represents the lifecycle state of an asynchronous + * operation in the coroutine awaitable protocol. + * Used for atomic coordination between the coroutine + * and the D-Bus callback thread. + * + ***********************************************/ +enum class AwaitableState : uint8_t +{ + NotReady, // Initial state: callback hasn't fired yet + Waiting, // Coroutine is suspended and waiting for callback + Completed // Callback completed, result is ready +}; + +// Shared data +template +struct AwaitableData +{ + using result_type = std::conditional_t, std::monostate, T>; + std::optional result; + std::optional exception; + std::coroutine_handle<> handle; + std::atomic status{AwaitableState::NotReady}; +}; + +/********************************************//** + * @class Awaitable + * + * A C++20 coroutine awaitable that represents an asynchronous + * operation. Allows suspending a coroutine until a D-Bus method + * call completes, then resuming with the result or exception. + * + * This is not a full-fledged coroutine type, but a simple awaitable + * that can be used with `co_await` to retrieve results of async D-Bus calls. + * This is independent of any specific coroutine framework or scheduler, + * as it relies on the D-Bus callback mechanism to resume the coroutine. + * + * You most likely don't need to use this class directly. Instead, use the + * respective low-level or high-level API functions that return an Awaitable + * instance, such as IProxy::callMethodAsync with with_awaitable_t tag, + * or the .getResultAsAwaitable() methods of the high-level API. + * + ***********************************************/ +template +class Awaitable +{ +public: + explicit Awaitable(std::shared_ptr> data) : data_(std::move(data)) {}; + + // Called when the coroutine is co_await'ed. Returns true if the coroutine should be suspended. + [[nodiscard]] bool await_ready() const noexcept + { + return data_->status.load(std::memory_order_acquire) == AwaitableState::Completed; + } + + // Called when the coroutine is suspended, returning false here will immediately + // resume the coroutine. + bool await_suspend(std::coroutine_handle<> handle) noexcept + { + data_->handle = handle; + + // Attempt transition from NotReady to Waiting. + AwaitableState expected = AwaitableState::NotReady; + return data_->status.compare_exchange_strong(expected, AwaitableState::Waiting, std::memory_order_acq_rel); + } + + // Called when the coroutine is resumed. Returns the result or throws the exception. + T await_resume() const + { + if (data_->exception) + { + std::rethrow_exception(*data_->exception); + } + + if constexpr (std::is_void_v) + { + return; + } + else + { + return std::move(*data_->result); + } + } + +private: + std::shared_ptr> data_; +}; + +} // namespace sdbus + +#endif // SDBUS_CXX_AWAITABLE_H_ From dbe7826e3cd9abadaccd47a5ac5ca19b9e2ebe91 Mon Sep 17 00:00:00 2001 From: Alex Cani Date: Sun, 1 Mar 2026 15:04:02 +0100 Subject: [PATCH 03/15] feat(awaitable): add low-level API methods Add three new flavors of the callMethodAsync method in the IProxy interface that use the with_awaitable_t tag to return an Awaitable. Implementation relies on the callback-based mechanism to insert the results into the shared AwaitableData, transition the state to Completed and resume the coroutine. --- include/sdbus-c++/IProxy.h | 55 ++++++++++++++++++++++++++++++++++ include/sdbus-c++/TypeTraits.h | 3 ++ src/Proxy.cpp | 31 +++++++++++++++++++ src/Proxy.h | 4 +++ 4 files changed, 93 insertions(+) diff --git a/include/sdbus-c++/IProxy.h b/include/sdbus-c++/IProxy.h index b93c1542..ce00c890 100644 --- a/include/sdbus-c++/IProxy.h +++ b/include/sdbus-c++/IProxy.h @@ -29,6 +29,7 @@ #include #include +#include #include #include @@ -604,6 +605,52 @@ namespace sdbus { , const std::chrono::duration& timeout , with_future_t ); + /*! + * @brief Calls method on the D-Bus object asynchronously + * + * @param[in] message Message representing a D-Bus method call + * @return An awaitable object that can be co_await'ed to retrieve the result + * + * This function call the remote D-Bus object asynchronously and return + * an awaitable that can be used with `co_await` to suspend a coroutine + * until the result is available. + * + * The call itself is non-blocking: the method call is performed and the method + * returns. The awaitable should be used to retrieve the result. + * + * The coroutine continuation (code after `co_await`) runs on the context of + * the bus connection I/O event loop thread. + * + * The default D-Bus method call timeout is used. See IConnection::getMethodCallTimeout(). + * + * @throws sdbus::Error in case of failure (propagated when awaited) + */ + virtual Awaitable callMethodAsync(const MethodCall& message, with_awaitable_t) = 0; + + /*! + * @brief Calls method on the D-Bus object asynchronously, with custom timeout + * + * @param[in] message Message representing a D-Bus method call + * @param[in] timeout Timeout for the method call in microseconds + * @return An awaitable object that can be co_await'ed to retrieve the result + * + * This behaves the same as IProxy::callMethodAsync(const MethodCall&, with_awaitable_t), + * but with a custom timeout for the method call. If timeout is zero, the behavior is identical. + * + * @throws sdbus::Error in case of failure (propagated when awaited) + */ + virtual Awaitable callMethodAsync( const MethodCall& message + , uint64_t timeout + , with_awaitable_t ) = 0; + + /*! + * @copydoc IProxy::callMethodAsync(const MethodCall&,uint64_t,with_awaitable_t) + */ + template + Awaitable callMethodAsync( const MethodCall& message + , const std::chrono::duration& timeout + , with_awaitable_t ); + /*! * @brief Registers a handler for the desired signal emitted by the D-Bus object * @@ -738,6 +785,14 @@ namespace sdbus { return callMethodAsync(message, microsecs.count(), with_future); } + template + inline Awaitable IProxy::callMethodAsync( const MethodCall& message + , const std::chrono::duration& timeout + , with_awaitable_t ) { + auto microsecs = std::chrono::duration_cast(timeout); + return callMethodAsync(message, microsecs.count(), with_awaitable); + } + inline MethodInvoker IProxy::callMethod(const MethodName& methodName) { return {*this, methodName}; diff --git a/include/sdbus-c++/TypeTraits.h b/include/sdbus-c++/TypeTraits.h index 39ed6351..c759abc3 100644 --- a/include/sdbus-c++/TypeTraits.h +++ b/include/sdbus-c++/TypeTraits.h @@ -109,6 +109,9 @@ namespace sdbus { // Tag denoting that the variant shall embed the other variant as its value, instead of creating a copy struct embed_variant_t { explicit embed_variant_t() = default; }; inline constexpr embed_variant_t embed_variant{}; + // Tag denoting an asynchronous call that returns an awaitable as a handle + struct with_awaitable_t { explicit with_awaitable_t() = default; }; + inline constexpr with_awaitable_t with_awaitable{}; // Helper for static assert template constexpr bool always_false = false; diff --git a/src/Proxy.cpp b/src/Proxy.cpp index 1d567194..aed6d9e9 100644 --- a/src/Proxy.cpp +++ b/src/Proxy.cpp @@ -38,6 +38,7 @@ #include "Utils.h" #include +#include #include #include #include @@ -181,6 +182,36 @@ std::future Proxy::callMethodAsync(const MethodCall& message, uint6 return future; } +Awaitable Proxy::callMethodAsync(const MethodCall& message, with_awaitable_t) { + return Proxy::callMethodAsync(message, /*timeout*/ 0, with_awaitable); +} + +Awaitable Proxy::callMethodAsync(const MethodCall& message, uint64_t timeout, with_awaitable_t) +{ + auto data = std::make_shared>(); + async_reply_handler asyncReplyCallback = [data](MethodReply reply, std::optional error) noexcept + { + if (!error) + { + data->result = std::move(reply); + } + else + { + data->exception = std::make_exception_ptr(*std::move(error)); + } + + auto previous = data->status.exchange(AwaitableState::Completed, std::memory_order_acq_rel); + if (previous == AwaitableState::Waiting) + { + data->handle.resume(); + } + }; + + (void)Proxy::callMethodAsync(message, std::move(asyncReplyCallback), timeout); + + return Awaitable{data}; +} + void Proxy::registerSignalHandler( const InterfaceName& interfaceName , const SignalName& signalName , signal_handler signalHandler ) diff --git a/src/Proxy.h b/src/Proxy.h index d474e42b..0d496413 100644 --- a/src/Proxy.h +++ b/src/Proxy.h @@ -73,6 +73,10 @@ namespace sdbus::internal { , return_slot_t ) override; std::future callMethodAsync(const MethodCall& message, with_future_t) override; std::future callMethodAsync(const MethodCall& message, uint64_t timeout, with_future_t) override; + Awaitable callMethodAsync(const MethodCall& message, with_awaitable_t) override; + Awaitable callMethodAsync( const MethodCall& message + , uint64_t timeout + , with_awaitable_t ) override; void registerSignalHandler( const InterfaceName& interfaceName , const SignalName& signalName From af389c407db20428fc1606cf2f536039d9a003ab Mon Sep 17 00:00:00 2001 From: Alex Cani Date: Sun, 1 Mar 2026 15:06:48 +0100 Subject: [PATCH 04/15] feat(awaitable): add high-level API methods Add a .getResultAsAwaitable method to each of the relevant high-level API helpers (AsyncMethodInvoker, AsyncPropertyGetter, AsyncPropertySetter, and AsyncAllPropertiesGetter). Implementation is similar to the low-level API: rely on the .uponReplyInvoke() mechanism to set the results on the shared data, atomically flip the state, and resume the suspended coroutine. --- include/sdbus-c++/ConvenienceApiClasses.h | 5 ++ include/sdbus-c++/ConvenienceApiClasses.inl | 63 +++++++++++++++++++++ include/sdbus-c++/TypeTraits.h | 5 ++ 3 files changed, 73 insertions(+) diff --git a/include/sdbus-c++/ConvenienceApiClasses.h b/include/sdbus-c++/ConvenienceApiClasses.h index ac4f0f64..c9d38fad 100644 --- a/include/sdbus-c++/ConvenienceApiClasses.h +++ b/include/sdbus-c++/ConvenienceApiClasses.h @@ -27,6 +27,7 @@ #ifndef SDBUS_CXX_CONVENIENCEAPICLASSES_H_ #define SDBUS_CXX_CONVENIENCEAPICLASSES_H_ +#include #include #include #include @@ -139,6 +140,7 @@ namespace sdbus { // or std::future for single D-Bus method return value // or std::future> for multiple method return values template std::future> getResultAsFuture(); + template Awaitable> getResultAsAwaitable(); private: friend IProxy; @@ -194,6 +196,7 @@ namespace sdbus { template PendingAsyncCall uponReplyInvoke(Function&& callback); template [[nodiscard]] Slot uponReplyInvoke(Function&& callback, return_slot_t); std::future getResultAsFuture(); + Awaitable getResultAsAwaitable(); private: friend IProxy; @@ -235,6 +238,7 @@ namespace sdbus { template PendingAsyncCall uponReplyInvoke(Function&& callback); template [[nodiscard]] Slot uponReplyInvoke(Function&& callback, return_slot_t); std::future getResultAsFuture(); + Awaitable getResultAsAwaitable(); private: friend IProxy; @@ -269,6 +273,7 @@ namespace sdbus { template PendingAsyncCall uponReplyInvoke(Function&& callback); template [[nodiscard]] Slot uponReplyInvoke(Function&& callback, return_slot_t); std::future> getResultAsFuture(); + Awaitable> getResultAsAwaitable(); private: friend IProxy; diff --git a/include/sdbus-c++/ConvenienceApiClasses.inl b/include/sdbus-c++/ConvenienceApiClasses.inl index bf74f1f9..7122f31c 100644 --- a/include/sdbus-c++/ConvenienceApiClasses.inl +++ b/include/sdbus-c++/ConvenienceApiClasses.inl @@ -354,6 +354,39 @@ namespace sdbus { return future; } + template + Awaitable> AsyncMethodInvoker::getResultAsAwaitable() + { + auto data = std::make_shared>>(); + uponReplyInvoke( + [data](std::optional error, Args... args) + { + if (error) + { + data->exception = std::make_exception_ptr(*std::move(error)); + } + else + { + if constexpr (!std::is_void_v>) + { + data->result.emplace(std::move(args)...); + } + else + { + data->result = std::monostate{}; + } + } + + auto previous = data->status.exchange(AwaitableState::Completed, std::memory_order_acq_rel); + if (previous == AwaitableState::Waiting) + { + data->handle.resume(); + } + }); + + return Awaitable>(data); + } + /*** ---------------- ***/ /*** SignalSubscriber ***/ /*** ---------------- ***/ @@ -524,6 +557,16 @@ namespace sdbus { .getResultAsFuture(); } + inline Awaitable AsyncPropertyGetter::getResultAsAwaitable() + { + assert(!interfaceName_.empty()); // onInterface() must be placed/called prior to this function + + return proxy_.callMethodAsync("Get") + .onInterface(DBUS_PROPERTIES_INTERFACE_NAME) + .withArguments(interfaceName_, propertyName_) + .getResultAsAwaitable(); + } + /*** -------------- ***/ /*** PropertySetter ***/ /*** -------------- ***/ @@ -640,6 +683,16 @@ namespace sdbus { .getResultAsFuture<>(); } + inline Awaitable AsyncPropertySetter::getResultAsAwaitable() + { + 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_)) + .getResultAsAwaitable<>(); + } + /*** ------------------- ***/ /*** AllPropertiesGetter ***/ /*** ------------------- ***/ @@ -713,6 +766,16 @@ namespace sdbus { .getResultAsFuture>(); } + inline Awaitable> AsyncAllPropertiesGetter::getResultAsAwaitable() + { + assert(!interfaceName_.empty()); // onInterface() must be placed/called prior to this function + + return proxy_.callMethodAsync("GetAll") + .onInterface(DBUS_PROPERTIES_INTERFACE_NAME) + .withArguments(interfaceName_) + .getResultAsAwaitable>(); + } + } // namespace sdbus #endif /* SDBUS_CPP_CONVENIENCEAPICLASSES_INL_ */ diff --git a/include/sdbus-c++/TypeTraits.h b/include/sdbus-c++/TypeTraits.h index c759abc3..5e6e9af0 100644 --- a/include/sdbus-c++/TypeTraits.h +++ b/include/sdbus-c++/TypeTraits.h @@ -589,6 +589,11 @@ namespace sdbus { template using future_return_t = typename future_return::type; + // For awaitable return types, the same scheme from futures can be reused + // so just provide an alias for visual distinction between the two + template + using awaitable_return_t = typename future_return::type; + // Credit: Piotr Skotnicki (https://stackoverflow.com/a/57639506) template constexpr bool is_one_of_variants_types = false; From 8c281e0861e14d02821b8ab33f2a5f30c582d0df Mon Sep 17 00:00:00 2001 From: Alex Cani Date: Sun, 1 Mar 2026 18:00:58 +0100 Subject: [PATCH 05/15] feat(awaitable): update StandardInterfaces.h Adapt the StandardInterface.h classes to include an awaitable-based overload of the async methods. --- include/sdbus-c++/StandardInterfaces.h | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/include/sdbus-c++/StandardInterfaces.h b/include/sdbus-c++/StandardInterfaces.h index 7afc41a6..34f0540c 100644 --- a/include/sdbus-c++/StandardInterfaces.h +++ b/include/sdbus-c++/StandardInterfaces.h @@ -173,6 +173,11 @@ namespace sdbus { return m_proxy.getPropertyAsync(propertyName).onInterface(interfaceName).getResultAsFuture(); } + Awaitable GetAsync(const InterfaceName& interfaceName, const PropertyName& propertyName, with_awaitable_t) + { + return m_proxy.getPropertyAsync(propertyName).onInterface(interfaceName).getResultAsAwaitable(); + } + template PendingAsyncCall GetAsync(std::string_view interfaceName, std::string_view propertyName, Function&& callback) { @@ -190,6 +195,11 @@ namespace sdbus { return m_proxy.getPropertyAsync(propertyName).onInterface(interfaceName).getResultAsFuture(); } + Awaitable GetAsync(std::string_view interfaceName, std::string_view propertyName, with_awaitable_t) + { + return m_proxy.getPropertyAsync(propertyName).onInterface(interfaceName).getResultAsAwaitable(); + } + void Set(const InterfaceName& interfaceName, const PropertyName& propertyName, const Variant& value) { m_proxy.setProperty(propertyName).onInterface(interfaceName).toValue(value); @@ -227,6 +237,11 @@ namespace sdbus { return m_proxy.setPropertyAsync(propertyName).onInterface(interfaceName).toValue(value).getResultAsFuture(); } + Awaitable SetAsync(const InterfaceName& interfaceName, const PropertyName& propertyName, const Variant& value, with_awaitable_t) + { + return m_proxy.setPropertyAsync(propertyName).onInterface(interfaceName).toValue(value).getResultAsAwaitable(); + } + template PendingAsyncCall SetAsync(std::string_view interfaceName, std::string_view propertyName, const Variant& value, Function&& callback) { @@ -244,6 +259,11 @@ namespace sdbus { return m_proxy.setPropertyAsync(propertyName).onInterface(interfaceName).toValue(value).getResultAsFuture(); } + Awaitable SetAsync(std::string_view interfaceName, std::string_view propertyName, const Variant& value, with_awaitable_t) + { + return m_proxy.setPropertyAsync(propertyName).onInterface(interfaceName).toValue(value).getResultAsAwaitable(); + } + std::map GetAll(const InterfaceName& interfaceName) { return m_proxy.getAllProperties().onInterface(interfaceName); @@ -271,6 +291,11 @@ namespace sdbus { return m_proxy.getAllPropertiesAsync().onInterface(interfaceName).getResultAsFuture(); } + Awaitable> GetAllAsync(const InterfaceName& interfaceName, with_awaitable_t) + { + return m_proxy.getAllPropertiesAsync().onInterface(interfaceName).getResultAsAwaitable(); + } + template PendingAsyncCall GetAllAsync(std::string_view interfaceName, Function&& callback) { @@ -288,6 +313,11 @@ namespace sdbus { return m_proxy.getAllPropertiesAsync().onInterface(interfaceName).getResultAsFuture(); } + Awaitable> GetAllAsync(std::string_view interfaceName, with_awaitable_t) + { + return m_proxy.getAllPropertiesAsync().onInterface(interfaceName).getResultAsAwaitable(); + } + private: IProxy& m_proxy; }; @@ -361,6 +391,11 @@ namespace sdbus { return m_proxy.callMethodAsync("GetManagedObjects").onInterface(INTERFACE_NAME).getResultAsFuture>>>(); } + Awaitable>>> GetManagedObjectsAsync(with_awaitable_t) + { + return m_proxy.callMethodAsync("GetManagedObjects").onInterface(INTERFACE_NAME).getResultAsAwaitable>>>(); + } + private: IProxy& m_proxy; }; From a683220f6c6b6312b80970c23603df670c1e5c38 Mon Sep 17 00:00:00 2001 From: Alex Cani Date: Sun, 1 Mar 2026 13:15:47 +0100 Subject: [PATCH 06/15] refactor(codegen): use enum and optional to represent async implementation The current implementation relies on two flags to determine if async is enabled for methods/properties. In preparation to introduce the generation of the coroutine-friendly awaitable methods as an async implementation, refactor this as an enum containing the implementations and an std::optional to mark simultaneously if async is enabled and the method. There are no behavioral changes or new functonality. C++ 17 is needed for std::optional, therefore the used C++ standard for the tool has been changed. --- tools/CMakeLists.txt | 2 +- tools/xml2cpp-codegen/ProxyGenerator.cpp | 61 +++++++++++++----------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index d220550e..7b3af0e5 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -35,7 +35,7 @@ set(SDBUSCPP_XML2CPP_SRCS # GENERAL COMPILER CONFIGURATION #------------------------------- -set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 17) unset(CMAKE_CXX_CLANG_TIDY) # Do not propagate clang-tidy to tools #---------------------------------- diff --git a/tools/xml2cpp-codegen/ProxyGenerator.cpp b/tools/xml2cpp-codegen/ProxyGenerator.cpp index b80bac58..841bd759 100644 --- a/tools/xml2cpp-codegen/ProxyGenerator.cpp +++ b/tools/xml2cpp-codegen/ProxyGenerator.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include using std::endl; @@ -41,6 +42,9 @@ using sdbuscpp::xml::Document; using sdbuscpp::xml::Node; using sdbuscpp::xml::Nodes; +// Possible implementation backends of async methods +enum class AsyncImpl { Callback, Future }; + /** * Generate proxy code - client glue */ @@ -158,8 +162,7 @@ std::tuple ProxyGenerator::processMethods(const Nodes& Nodes outArgs = args.select("direction" , "out"); bool dontExpectReply{false}; - bool async{false}; - bool future{false}; // Async methods implemented by means of either std::future or callbacks + std::optional asyncImpl; std::string timeoutValue; std::smatch smTimeout; @@ -175,11 +178,11 @@ std::tuple ProxyGenerator::processMethods(const Nodes& { if (annotationName == "org.freedesktop.DBus.Method.Async" && (annotationValue == "client" || annotationValue == "clientserver" || annotationValue == "client-server")) - async = true; + asyncImpl = AsyncImpl::Callback; // Default to callback else if (annotationName == "org.freedesktop.DBus.Method.Async.ClientImpl" && annotationValue == "callback") - future = false; + asyncImpl = AsyncImpl::Callback; else if (annotationName == "org.freedesktop.DBus.Method.Async.ClientImpl" && (annotationValue == "future" || annotationValue == "std::future")) - future = true; + asyncImpl = AsyncImpl::Future; } if (annotationName == "org.freedesktop.DBus.Method.Timeout") timeoutValue = annotationValue; @@ -211,7 +214,9 @@ std::tuple ProxyGenerator::processMethods(const Nodes& std::string outArgStr, outArgTypeStr; std::tie(outArgStr, outArgTypeStr, std::ignore, std::ignore) = argsToNamesAndTypes(outArgs); - const std::string realRetType = (async && !dontExpectReply ? (future ? "std::future<" + retType + ">" : "sdbus::PendingAsyncCall") : async ? "void" : retType); + const std::string realRetType = (asyncImpl.has_value() && !dontExpectReply + ? (*asyncImpl == AsyncImpl::Future ? "std::future<" + retType + ">" : "sdbus::PendingAsyncCall") + : asyncImpl.has_value() ? "void" : retType); definitionSS << tab << realRetType << " " << nameSafe << "(" << inArgTypeStr << ")" << endl << tab << "{" << endl; @@ -220,13 +225,13 @@ std::tuple ProxyGenerator::processMethods(const Nodes& definitionSS << tab << tab << "using namespace std::chrono_literals;" << endl; } - if (outArgs.size() > 0 && !async) + if (outArgs.size() > 0 && !asyncImpl.has_value()) { definitionSS << tab << tab << retType << " result;" << endl; } - definitionSS << tab << tab << (async && !dontExpectReply ? "return " : "") - << "m_proxy.callMethod" << (async ? "Async" : "") << "(\"" << name << "\").onInterface(INTERFACE_NAME)"; + definitionSS << tab << tab << (asyncImpl.has_value() && !dontExpectReply ? "return " : "") + << "m_proxy.callMethod" << (asyncImpl.has_value() ? "Async" : "") << "(\"" << name << "\").onInterface(INTERFACE_NAME)"; if (!timeoutValue.empty()) { @@ -240,12 +245,12 @@ std::tuple ProxyGenerator::processMethods(const Nodes& definitionSS << ".withArguments(" << inArgStr << ")"; } - if (async && !dontExpectReply) + if (asyncImpl.has_value() && !dontExpectReply) { auto nameBigFirst = name; nameBigFirst[0] = islower(nameBigFirst[0]) ? nameBigFirst[0] + 'A' - 'a' : nameBigFirst[0]; - if (future) // Async methods implemented through future + if (*asyncImpl == AsyncImpl::Future) // Async methods implemented through future { definitionSS << ".getResultAsFuture<" << retTypeBare << ">()"; } @@ -315,10 +320,8 @@ std::tuple ProxyGenerator::processProperties(const Nod auto propertyArg = std::string("value"); auto propertyTypeArg = std::string("const ") + propertyType + "& " + propertyArg; - bool asyncGet{false}; - bool futureGet{false}; // Async property getter implemented by means of either std::future or callbacks - bool asyncSet{false}; - bool futureSet{false}; // Async property setter implemented by means of either std::future or callbacks + std::optional asyncImplGet; + std::optional asyncImplSet; Nodes annotations = (*property)["annotation"]; for (const auto& annotation : annotations) @@ -327,28 +330,28 @@ std::tuple ProxyGenerator::processProperties(const Nod const auto annotationValue = annotation->get("value"); if (annotationName == "org.freedesktop.DBus.Property.Get.Async" && annotationValue == "client") // Server-side not supported (may be in the future) - asyncGet = true; + asyncImplGet = AsyncImpl::Callback; // Default to callback else if (annotationName == "org.freedesktop.DBus.Property.Get.Async.ClientImpl" && annotationValue == "callback") - futureGet = false; + asyncImplGet = AsyncImpl::Callback; else if (annotationName == "org.freedesktop.DBus.Property.Get.Async.ClientImpl" && (annotationValue == "future" || annotationValue == "std::future")) - futureGet = true; + asyncImplGet = AsyncImpl::Future; else if (annotationName == "org.freedesktop.DBus.Property.Set.Async" && annotationValue == "client") // Server-side not supported (may be in the future) - asyncSet = true; + asyncImplSet = AsyncImpl::Callback; // Default to callback else if (annotationName == "org.freedesktop.DBus.Property.Set.Async.ClientImpl" && annotationValue == "callback") - futureSet = false; + asyncImplSet = AsyncImpl::Callback; else if (annotationName == "org.freedesktop.DBus.Property.Set.Async.ClientImpl" && (annotationValue == "future" || annotationValue == "std::future")) - futureSet = true; + asyncImplSet = AsyncImpl::Future; } if (propertyAccess == "read" || propertyAccess == "readwrite") { - const std::string realRetType = (asyncGet ? (futureGet ? "std::future" : "sdbus::PendingAsyncCall") : propertyType); + const std::string realRetType = (asyncImplGet.has_value() ? (*asyncImplGet == AsyncImpl::Future ? "std::future" : "sdbus::PendingAsyncCall") : propertyType); propertySS << tab << realRetType << " " << propertyNameSafe << "()" << endl << tab << "{" << endl; - propertySS << tab << tab << "return m_proxy.getProperty" << (asyncGet ? "Async" : "") << "(\"" << propertyName << "\")" + propertySS << tab << tab << "return m_proxy.getProperty" << (asyncImplGet.has_value() ? "Async" : "") << "(\"" << propertyName << "\")" ".onInterface(INTERFACE_NAME)"; - if (!asyncGet) + if (!asyncImplGet.has_value()) { propertySS << ".get<" << realRetType << ">()"; } @@ -357,7 +360,7 @@ std::tuple ProxyGenerator::processProperties(const Nod auto nameBigFirst = propertyName; nameBigFirst[0] = islower(nameBigFirst[0]) ? nameBigFirst[0] + 'A' - 'a' : nameBigFirst[0]; - if (futureGet) // Async methods implemented through future + if (*asyncImplGet == AsyncImpl::Future) // Async methods implemented through future { propertySS << ".getResultAsFuture()"; } @@ -378,21 +381,21 @@ std::tuple ProxyGenerator::processProperties(const Nod if (propertySignature == "v") propertyArg = "{" + propertyArg + ", sdbus::embed_variant}"; - const std::string realRetType = (asyncSet ? (futureSet ? "std::future" : "sdbus::PendingAsyncCall") : "void"); + const std::string realRetType = (asyncImplSet.has_value() ? (*asyncImplSet == AsyncImpl::Future ? "std::future" : "sdbus::PendingAsyncCall") : "void"); propertySS << tab << realRetType << " " << propertyNameSafe << "(" << propertyTypeArg << ")" << endl << tab << "{" << endl; - propertySS << tab << tab << (asyncSet ? "return " : "") << "m_proxy.setProperty" << (asyncSet ? "Async" : "") + propertySS << tab << tab << (asyncImplSet.has_value() ? "return " : "") << "m_proxy.setProperty" << (asyncImplSet.has_value() ? "Async" : "") << "(\"" << propertyName << "\")" ".onInterface(INTERFACE_NAME)" ".toValue(" << propertyArg << ")"; - if (asyncSet) + if (asyncImplSet.has_value()) { auto nameBigFirst = propertyName; nameBigFirst[0] = islower(nameBigFirst[0]) ? nameBigFirst[0] + 'A' - 'a' : nameBigFirst[0]; - if (futureSet) // Async methods implemented through future + if (*asyncImplSet == AsyncImpl::Future) // Async methods implemented through future { propertySS << ".getResultAsFuture()"; } From 64b6173abb53659e4af45a2fdf3b96ffa0856584 Mon Sep 17 00:00:00 2001 From: Alex Cani Date: Sun, 1 Mar 2026 13:44:27 +0100 Subject: [PATCH 07/15] feat(codegen): add awaitable async implementation support Use the new awaitable-returning methods from the high level API to add support to a third async implementation mode alongside the existing callback and future implementations. Add a new enum entry to AsyncImpl and parse the "awaitable" or "coroutine" values in the relevant annotations for methods and properties. Methods annotated with this implementation will return sdbus::Awaitable and call .getResultAsAwaitable<>() on the high-level API. Properties follow the same pattern, returning sdbus::Awaitable for getters and sdbus::Awaitable for setters. The nested ternary expressions for determining return types have been refactored into explicit if-else blocks for improved readability and to accommodate the additional implementation type. The existing callback and future implementations remain unchanged in behavior. --- tools/xml2cpp-codegen/ProxyGenerator.cpp | 87 ++++++++++++++++++++---- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/tools/xml2cpp-codegen/ProxyGenerator.cpp b/tools/xml2cpp-codegen/ProxyGenerator.cpp index 841bd759..2541c9a7 100644 --- a/tools/xml2cpp-codegen/ProxyGenerator.cpp +++ b/tools/xml2cpp-codegen/ProxyGenerator.cpp @@ -43,7 +43,7 @@ using sdbuscpp::xml::Node; using sdbuscpp::xml::Nodes; // Possible implementation backends of async methods -enum class AsyncImpl { Callback, Future }; +enum class AsyncImpl { Callback, Future, Awaitable }; /** * Generate proxy code - client glue @@ -183,6 +183,8 @@ std::tuple ProxyGenerator::processMethods(const Nodes& asyncImpl = AsyncImpl::Callback; else if (annotationName == "org.freedesktop.DBus.Method.Async.ClientImpl" && (annotationValue == "future" || annotationValue == "std::future")) asyncImpl = AsyncImpl::Future; + else if (annotationName == "org.freedesktop.DBus.Method.Async.ClientImpl" && (annotationValue == "awaitable" || annotationValue == "coroutine")) + asyncImpl = AsyncImpl::Awaitable; } if (annotationName == "org.freedesktop.DBus.Method.Timeout") timeoutValue = annotationValue; @@ -214,9 +216,26 @@ std::tuple ProxyGenerator::processMethods(const Nodes& std::string outArgStr, outArgTypeStr; std::tie(outArgStr, outArgTypeStr, std::ignore, std::ignore) = argsToNamesAndTypes(outArgs); - const std::string realRetType = (asyncImpl.has_value() && !dontExpectReply - ? (*asyncImpl == AsyncImpl::Future ? "std::future<" + retType + ">" : "sdbus::PendingAsyncCall") - : asyncImpl.has_value() ? "void" : retType); + // Determine return type based on async implementation + std::string realRetType; + if (asyncImpl.has_value() && !dontExpectReply) + { + if (*asyncImpl == AsyncImpl::Future) + realRetType = "std::future<" + retType + ">"; + else if (*asyncImpl == AsyncImpl::Awaitable) + realRetType = "sdbus::Awaitable<" + retType + ">"; + else // Callback + realRetType = "sdbus::PendingAsyncCall"; + } + else if (asyncImpl.has_value()) + { + realRetType = "void"; + } + else + { + realRetType = retType; + } + definitionSS << tab << realRetType << " " << nameSafe << "(" << inArgTypeStr << ")" << endl << tab << "{" << endl; @@ -250,11 +269,15 @@ std::tuple ProxyGenerator::processMethods(const Nodes& auto nameBigFirst = name; nameBigFirst[0] = islower(nameBigFirst[0]) ? nameBigFirst[0] + 'A' - 'a' : nameBigFirst[0]; - if (*asyncImpl == AsyncImpl::Future) // Async methods implemented through future + if (*asyncImpl == AsyncImpl::Future) { definitionSS << ".getResultAsFuture<" << retTypeBare << ">()"; } - else // Async methods implemented through callbacks + else if (*asyncImpl == AsyncImpl::Awaitable) + { + definitionSS << ".getResultAsAwaitable<" << retTypeBare << ">()"; + } + else // Callback { definitionSS << ".uponReplyInvoke([this](std::optional error" << (outArgTypeStr.empty() ? "" : ", ") << outArgTypeStr << ")" "{ this->on" << nameBigFirst << "Reply(" << outArgStr << (outArgStr.empty() ? "" : ", ") << "std::move(error)); })"; @@ -335,17 +358,35 @@ std::tuple ProxyGenerator::processProperties(const Nod asyncImplGet = AsyncImpl::Callback; else if (annotationName == "org.freedesktop.DBus.Property.Get.Async.ClientImpl" && (annotationValue == "future" || annotationValue == "std::future")) asyncImplGet = AsyncImpl::Future; + else if (annotationName == "org.freedesktop.DBus.Property.Get.Async.ClientImpl" && (annotationValue == "awaitable" || annotationValue == "coroutine")) + asyncImplGet = AsyncImpl::Awaitable; else if (annotationName == "org.freedesktop.DBus.Property.Set.Async" && annotationValue == "client") // Server-side not supported (may be in the future) asyncImplSet = AsyncImpl::Callback; // Default to callback else if (annotationName == "org.freedesktop.DBus.Property.Set.Async.ClientImpl" && annotationValue == "callback") asyncImplSet = AsyncImpl::Callback; else if (annotationName == "org.freedesktop.DBus.Property.Set.Async.ClientImpl" && (annotationValue == "future" || annotationValue == "std::future")) asyncImplSet = AsyncImpl::Future; + else if (annotationName == "org.freedesktop.DBus.Property.Set.Async.ClientImpl" && (annotationValue == "awaitable" || annotationValue == "coroutine")) + asyncImplSet = AsyncImpl::Awaitable; } if (propertyAccess == "read" || propertyAccess == "readwrite") { - const std::string realRetType = (asyncImplGet.has_value() ? (*asyncImplGet == AsyncImpl::Future ? "std::future" : "sdbus::PendingAsyncCall") : propertyType); + // Determine return type based on async implementation + std::string realRetType; + if (asyncImplGet.has_value()) + { + if (*asyncImplGet == AsyncImpl::Future) + realRetType = "std::future"; + else if (*asyncImplGet == AsyncImpl::Awaitable) + realRetType = "sdbus::Awaitable"; + else // Callback + realRetType = "sdbus::PendingAsyncCall"; + } + else + { + realRetType = propertyType; + } propertySS << tab << realRetType << " " << propertyNameSafe << "()" << endl << tab << "{" << endl; @@ -360,11 +401,15 @@ std::tuple ProxyGenerator::processProperties(const Nod auto nameBigFirst = propertyName; nameBigFirst[0] = islower(nameBigFirst[0]) ? nameBigFirst[0] + 'A' - 'a' : nameBigFirst[0]; - if (*asyncImplGet == AsyncImpl::Future) // Async methods implemented through future + if (*asyncImplGet == AsyncImpl::Future) { propertySS << ".getResultAsFuture()"; } - else // Async methods implemented through callbacks + else if (*asyncImplGet == AsyncImpl::Awaitable) + { + propertySS << ".getResultAsAwaitable()"; + } + else // Callback { propertySS << ".uponReplyInvoke([this](std::optional error, const sdbus::Variant& value)" "{ this->on" << nameBigFirst << "PropertyGetReply(value.get<" << propertyType << ">(), std::move(error)); })"; @@ -381,7 +426,21 @@ std::tuple ProxyGenerator::processProperties(const Nod if (propertySignature == "v") propertyArg = "{" + propertyArg + ", sdbus::embed_variant}"; - const std::string realRetType = (asyncImplSet.has_value() ? (*asyncImplSet == AsyncImpl::Future ? "std::future" : "sdbus::PendingAsyncCall") : "void"); + // Determine return type based on async implementation + std::string realRetType; + if (asyncImplSet.has_value()) + { + if (*asyncImplSet == AsyncImpl::Future) + realRetType = "std::future"; + else if (*asyncImplSet == AsyncImpl::Awaitable) + realRetType = "sdbus::Awaitable"; + else // Callback + realRetType = "sdbus::PendingAsyncCall"; + } + else + { + realRetType = "void"; + } propertySS << tab << realRetType << " " << propertyNameSafe << "(" << propertyTypeArg << ")" << endl << tab << "{" << endl; @@ -395,11 +454,15 @@ std::tuple ProxyGenerator::processProperties(const Nod auto nameBigFirst = propertyName; nameBigFirst[0] = islower(nameBigFirst[0]) ? nameBigFirst[0] + 'A' - 'a' : nameBigFirst[0]; - if (*asyncImplSet == AsyncImpl::Future) // Async methods implemented through future + if (*asyncImplSet == AsyncImpl::Future) { propertySS << ".getResultAsFuture()"; } - else // Async methods implemented through callbacks + else if (*asyncImplSet == AsyncImpl::Awaitable) + { + propertySS << ".getResultAsAwaitable()"; + } + else // Callback { propertySS << ".uponReplyInvoke([this](std::optional error)" "{ this->on" << nameBigFirst << "PropertySetReply(std::move(error)); })"; From 09ead455edc1e1f3eb71e2ad1ad007fc52a317a5 Mon Sep 17 00:00:00 2001 From: Alex Cani Date: Sun, 1 Mar 2026 19:20:10 +0100 Subject: [PATCH 08/15] feat(tests): add integration tests for Awaitable Introduce new awaitable methods in TestProxy for asynchronous operations and add corresponding integration tests in DBusAwaitableMethodsTests.cpp to validate functionality. The test uses a simple coroutine task type which uses a promise/future pair to pass data between the callback and the coroutine, leveraging the fact that the tests are all multithreaded. This approach would not work in a single-threaded scenario, as the future's .get() method would block the thread. --- tests/CMakeLists.txt | 1 + .../DBusAwaitableMethodsTests.cpp | 247 ++++++++++++++++++ tests/integrationtests/TestProxy.cpp | 32 +++ tests/integrationtests/TestProxy.h | 4 + 4 files changed, 284 insertions(+) create mode 100644 tests/integrationtests/DBusAwaitableMethodsTests.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index acaabcd0..a8a379d5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -60,6 +60,7 @@ set(INTEGRATIONTESTS_SRCS ${INTEGRATIONTESTS_SOURCE_DIR}/DBusGeneralTests.cpp ${INTEGRATIONTESTS_SOURCE_DIR}/DBusMethodsTests.cpp ${INTEGRATIONTESTS_SOURCE_DIR}/DBusAsyncMethodsTests.cpp + ${INTEGRATIONTESTS_SOURCE_DIR}/DBusAwaitableMethodsTests.cpp ${INTEGRATIONTESTS_SOURCE_DIR}/DBusSignalsTests.cpp ${INTEGRATIONTESTS_SOURCE_DIR}/DBusPropertiesTests.cpp ${INTEGRATIONTESTS_SOURCE_DIR}/DBusStandardInterfacesTests.cpp diff --git a/tests/integrationtests/DBusAwaitableMethodsTests.cpp b/tests/integrationtests/DBusAwaitableMethodsTests.cpp new file mode 100644 index 00000000..ef47cecb --- /dev/null +++ b/tests/integrationtests/DBusAwaitableMethodsTests.cpp @@ -0,0 +1,247 @@ +/** + * (C) 2016 - 2021 KISTLER INSTRUMENTE AG, Winterthur, Switzerland + * (C) 2016 - 2026 Stanislav Angelovic + * (C) 2026 - Alex Cani + * + * @file DBusAwaitableMethodsTests.cpp + * + * Created on: Mar 1, 2026 + * Project: sdbus-c++ + * Description: High-level D-Bus IPC C++ library based on sd-bus + * + * This file is part of sdbus-c++. + * + * sdbus-c++ is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2.1 of the License, or + * (at your option) any later version. + * + * sdbus-c++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with sdbus-c++. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "TestFixture.h" +#include "TestProxy.h" +#include "Defs.h" + +using ::testing::Eq; +using namespace std::chrono_literals; +using namespace sdbus::test; + +// Simple coroutine task type for testing purposes +// Uses a promise/future pair to communicate results and exceptions +// between the coroutine and the test code. Do not mistake this for +// the std::future-based async API of sdbus-c++. +template +struct Task { + struct promise_type { + T value; + std::exception_ptr exception; + std::promise completion; + std::future future; + + promise_type() : future(completion.get_future()) {} + + Task get_return_object() { + return Task{std::coroutine_handle::from_promise(*this)}; + } + + // Lazy coroutine + std::suspend_always initial_suspend() noexcept { return {}; } + + // final_suspend suspends so that the test code can retrieve the result + // or exception from the promise before the coroutine is destroyed + std::suspend_always final_suspend() noexcept + { + if (exception) + { + completion.set_exception(exception); + } + else + { + completion.set_value(std::move(value)); + } + return {}; + } + + void return_value(T v) { value = std::move(v); } + void unhandled_exception() { exception = std::current_exception(); } + }; + + std::coroutine_handle handle; + + // Ctor and rule of 5 for proper handle management + explicit Task(std::coroutine_handle h) : handle(h) {} + Task(Task&& other) noexcept : handle(std::exchange(other.handle, {})) {} + Task& operator=(Task&& other) noexcept { + if (this != &other) { + if (handle) handle.destroy(); + handle = std::exchange(other.handle, {}); + } + return *this; + } + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; + ~Task() { if (handle) handle.destroy(); } + + // "User API" for the test code, allows starting the task and retrieving the result or exception + void resume() { if (handle && !handle.done()) handle.resume(); } + T get() { return handle.promise().future.get(); } +}; + +// Specialization for void +template<> +struct Task { + struct promise_type { + std::exception_ptr exception; + std::promise completion; + std::future future; + + promise_type() : future(completion.get_future()) {} + + Task get_return_object() { + return Task{std::coroutine_handle::from_promise(*this)}; + } + + std::suspend_always initial_suspend() noexcept { return {}; } + + std::suspend_always final_suspend() noexcept + { + if (exception) + { + completion.set_exception(exception); + } + else + { + completion.set_value(); + } + return {}; + } + + void return_void() {} + void unhandled_exception() { exception = std::current_exception(); } + }; + + std::coroutine_handle handle; + + explicit Task(std::coroutine_handle h) : handle(h) {} + Task(Task&& other) noexcept : handle(std::exchange(other.handle, {})) {} + Task& operator=(Task&& other) noexcept { + if (this != &other) { + if (handle) handle.destroy(); + handle = std::exchange(other.handle, {}); + } + return *this; + } + Task(const Task&) = delete; + Task& operator=(const Task&) = delete; + + ~Task() { if (handle) handle.destroy(); } + + void resume() { if (handle && !handle.done()) handle.resume(); } + void get() { handle.promise().future.get(); } +}; + +/*-------------------------------------*/ +/* -- TEST CASES -- */ +/*-------------------------------------*/ + +TYPED_TEST(AsyncSdbusTestObject, InvokesMethodAsynchronouslyOnClientSideWithAwaitable) +{ + auto task = [](TestProxy* proxy) -> Task { + co_return co_await proxy->doOperationClientSideAsync(100, sdbus::with_awaitable); + }(this->m_proxy.get()); + + task.resume(); + + ASSERT_THAT(task.get(), Eq(100)); +} + +TYPED_TEST(AsyncSdbusTestObject, InvokesMethodAsynchronouslyOnClientSideWithAwaitableOnBasicAPILevel) +{ + auto task = [](TestProxy* proxy) -> Task { + auto methodReply = co_await proxy->doOperationClientSideAsyncOnBasicAPILevel(100, sdbus::with_awaitable); + uint32_t returnValue{}; + methodReply >> returnValue; + co_return returnValue; + }(this->m_proxy.get()); + + task.resume(); + + ASSERT_THAT(task.get(), Eq(100)); +} + +TYPED_TEST(AsyncSdbusTestObject, InvokesMethodWithLargeDataAsynchronouslyOnClientSideWithAwaitable) +{ + std::map largeMap; + for (int32_t i = 0; i < 40'000; ++i) + largeMap.emplace(i, "This is string nr. " + std::to_string(i+1)); + + auto task = [&largeMap, this]() -> Task> { + co_return co_await this->m_proxy->doOperationWithLargeDataClientSideAsync(largeMap, sdbus::with_awaitable); + }(); + + task.resume(); + + ASSERT_THAT(task.get(), Eq(largeMap)); +} + +TYPED_TEST(AsyncSdbusTestObject, ThrowsErrorWhenClientSideAsynchronousMethodCallWithAwaitableFails) +{ + auto task = [](TestProxy* proxy) -> Task { + co_await proxy->doErroneousOperationClientSideAsync(sdbus::with_awaitable); + }(this->m_proxy.get()); + + task.resume(); + + ASSERT_THROW(task.get(), sdbus::Error); +} + +TYPED_TEST(AsyncSdbusTestObject, AwaitableSupportsMultipleSequentialCalls) +{ + auto task = [](TestProxy* proxy) -> Task { + auto result1 = co_await proxy->doOperationClientSideAsync(10, sdbus::with_awaitable); + auto result2 = co_await proxy->doOperationClientSideAsync(20, sdbus::with_awaitable); + auto result3 = co_await proxy->doOperationClientSideAsync(30, sdbus::with_awaitable); + co_return result1 + result2 + result3; + }(this->m_proxy.get()); + + task.resume(); + + ASSERT_THAT(task.get(), Eq(60)); +} + +TYPED_TEST(AsyncSdbusTestObject, AwaitablePropagatesExceptionsCorrectly) +{ + auto task = [](TestProxy* proxy) -> Task { + try { + co_await proxy->doErroneousOperationClientSideAsync(sdbus::with_awaitable); + co_return "FAILED"; + } catch (const sdbus::Error& e) { + // Verify we can inspect the exception + co_return std::string(e.getName()); + } + }(this->m_proxy.get()); + + task.resume(); + + ASSERT_THAT(task.get(), ::testing::HasSubstr("Error")); +} diff --git a/tests/integrationtests/TestProxy.cpp b/tests/integrationtests/TestProxy.cpp index 9aeca83f..8ca07e10 100644 --- a/tests/integrationtests/TestProxy.cpp +++ b/tests/integrationtests/TestProxy.cpp @@ -196,6 +196,38 @@ std::future TestProxy::doErroneousOperationClientSideAsync(with_future_t) .getResultAsFuture<>(); } +sdbus::Awaitable TestProxy::doOperationClientSideAsync(uint32_t param, sdbus::with_awaitable_t) +{ + return getProxy().callMethodAsync("doOperation") + .onInterface(sdbus::test::INTERFACE_NAME) + .withArguments(param) + .getResultAsAwaitable(); +} + +sdbus::Awaitable> TestProxy::doOperationWithLargeDataClientSideAsync(const std::map& largeParam, sdbus::with_awaitable_t) +{ + return getProxy().callMethodAsync("doOperationWithLargeData") + .onInterface(sdbus::test::INTERFACE_NAME) + .withArguments(largeParam) + .getResultAsAwaitable>(); +} + +sdbus::Awaitable TestProxy::doOperationClientSideAsyncOnBasicAPILevel(uint32_t param, sdbus::with_awaitable_t) +{ + auto methodCall = getProxy().createMethodCall(sdbus::test::INTERFACE_NAME, sdbus::MethodName{"doOperation"}); + methodCall << param; + + return getProxy().callMethodAsync(methodCall, sdbus::with_awaitable); +} + + +sdbus::Awaitable TestProxy::doErroneousOperationClientSideAsync(sdbus::with_awaitable_t) +{ + return getProxy().callMethodAsync("throwError") + .onInterface(sdbus::test::INTERFACE_NAME) + .getResultAsAwaitable<>(); +} + 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 750936c3..ea058f58 100644 --- a/tests/integrationtests/TestProxy.h +++ b/tests/integrationtests/TestProxy.h @@ -111,6 +111,10 @@ class TestProxy final : public sdbus::ProxyInterfaces< org::sdbuscpp::integratio std::future> doOperationWithLargeDataClientSideAsync(const std::map& largeParam, with_future_t); std::future doOperationClientSideAsyncOnBasicAPILevel(uint32_t param); std::future doErroneousOperationClientSideAsync(with_future_t); + sdbus::Awaitable doOperationClientSideAsync(uint32_t param, sdbus::with_awaitable_t); + sdbus::Awaitable> doOperationWithLargeDataClientSideAsync(const std::map& largeParam, sdbus::with_awaitable_t); + sdbus::Awaitable doOperationClientSideAsyncOnBasicAPILevel(uint32_t param, sdbus::with_awaitable_t); + sdbus::Awaitable doErroneousOperationClientSideAsync(sdbus::with_awaitable_t); void doErroneousOperationClientSideAsync(); void doOperationClientSideAsyncWithTimeout(const std::chrono::microseconds &timeout, uint32_t param); int32_t callNonexistentMethod(); From 3988cd7828dfc071cc84447d48a9d3c5cb1c23c2 Mon Sep 17 00:00:00 2001 From: Alex Cani Date: Sun, 1 Mar 2026 19:21:26 +0100 Subject: [PATCH 09/15] docs: update documentation Update the main tutorial with the new awaitable-based async method. --- docs/using-sdbus-c++.md | 73 +++++++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/docs/using-sdbus-c++.md b/docs/using-sdbus-c++.md index 66c70a33..0a1c59d1 100644 --- a/docs/using-sdbus-c++.md +++ b/docs/using-sdbus-c++.md @@ -759,7 +759,7 @@ private: Analogously to the adaptor classes described above, there is one proxy class generated for one interface in the XML IDL file. The class is de facto a proxy to the concrete single interface of a remote object. For each D-Bus signal there is a pure virtual member function whose body must be provided in a child class. For each method, there is a public function member that calls the method remotely. -Generated proxy classes are not copyable and not moveable by design. One can create them on the heap and manage them in e.g. a `std::unique_ptr` if move semantics is needed (for example, when they are stored in a container). +Generated proxy classes are not copyable and not moveable by design. One can create them on the heap and manage them in e.g. a `std::unique_ptr` if move semantics is needed (for example, when they are stored in a container). ```cpp /* @@ -1130,11 +1130,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, 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 +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, a future object returned by the async call will be set the returned value, or (with C++20) an awaitable can be `co_await`ed in a coroutine. ### Lower-level API -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: +Considering the Concatenator example based on lower-level API, if we wanted to call `concatenate` in an async way, we have several options. We can 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[]) @@ -1204,6 +1204,18 @@ Another option is to use `std::future`-based overload of the `IProxy::callMethod } ``` +A third option, available with C++20, is to use `sdbus::with_awaitable` to get an `Awaitable` that can be `co_await`ed in a coroutine: + +```c++ + // In a coroutine context: + auto method = concatenatorProxy->createMethodCall(interfaceName, concatenate); + method << numbers << separator; + auto reply = co_await concatenatorProxy->callMethod(method, sdbus::with_awaitable); + std::string result; + reply >> result; + // If an error occurs, sdbus::Error is thrown when co_await completes +``` + ### Convenience API 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: `std::optional`. All subsequent arguments shall exactly reflect the D-Bus method output arguments. A concatenator example: @@ -1264,6 +1276,18 @@ The future object will contain void for a void-returning D-Bus method, a single ... ``` +A third option, available with C++20, is to finish the async call statement with `getResultAsAwaitable()`, which returns an `Awaitable` that can be `co_await`ed in a coroutine. The template arguments are the D-Bus method return types (empty for void-returning methods). The awaitable returns `void`, a single value, or a `std::tuple` for multiple return values: + +```c++ + // In a coroutine context: + auto result = co_await concatenatorProxy->callMethodAsync("concatenate") + .onInterface(interfaceName) + .withArguments(numbers, separator) + .getResultAsAwaitable(); + std::cout << "Got concatenate result: " << result << std::endl; + // If an error occurs, sdbus::Error is thrown when co_await completes +``` + ### Marking client-side async methods in the IDL sdbus-c++-xml2cpp can generate C++ 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 `client-server` (async method on both client- and server-side): @@ -1286,7 +1310,7 @@ sdbus-c++-xml2cpp can generate C++ code for client-side async methods. We just n ``` -An asynchronous method can be generated as a callback-based method or `std::future`-based method. This can optionally be customized through an additional `org.freedesktop.DBus.Method.Async.ClientImpl` annotation. Its supported values are `callback` and `std::future`. The default behavior is callback-based method. +An asynchronous method can be generated as a callback-based method, `std::future`-based method, or C++20 awaitable-based method. This can optionally be customized through an additional `org.freedesktop.DBus.Method.Async.ClientImpl` annotation. Its supported values are `callback`, `future` and `awaitable`. The default behavior is callback-based method. #### Generating callback-based async methods @@ -1294,11 +1318,31 @@ For each client-side async method, a corresponding `onReply` pure vi So in the specific example above, the tool will generate a `Concatenator_proxy` class similar to one shown in a [dedicated section above](#concatenator-client-glueh), with the difference that it will also generate an additional `virtual void onConcatenateReply(std::optional error, const std::string& concatenatedString);` method, which we shall override in the derived `ConcatenatorProxy`. -#### Generating std:future-based async methods +#### Generating std::future-based async methods In this case, a `std::future` is returned by the method, which will later, when the reply arrives, get set to contain the return value. Or if the call returns an error, `sdbus::Error` will be thrown by `std::future::get()`. -For a real example of a client-side asynchronous D-Bus methods, please look at sdbus-c++ [stress tests](/tests/stresstests). +#### Generating awaitable-based async methods + +> **_Note_:** This requires C++20 support. The generated code uses `sdbus::Awaitable` which requires compiling with C++20 or newer. + +When using `awaitable` as the `ClientImpl` annotation value, the generated method returns an `sdbus::Awaitable` that can be used with C++20 coroutines. The return type `T` is `void` for void-returning D-Bus methods, a single type for single-value methods, or `std::tuple` for multi-value methods. + +Example annotation: + +```xml + + + + + + + +``` + +This generates a method that can be `co_await`ed: `std::string result = co_await proxy.concatenate({1, 2, 3}, ":");` + +For a real example of a client-side asynchronous D-Bus methods, please look at sdbus-c++ [stress tests](/tests/stresstests) and [integration tests](/tests/integrationtests). ## Method call timeout @@ -1338,7 +1382,7 @@ We read property value easily through `IProxy::getProperty()` method: uint32_t status = proxy->getProperty("status").onInterface("org.sdbuscpp.Concatenator"); ``` -Getting a property in asynchronous manner is also possible, in both callback-based and future-based way, by calling `IProxy::getPropertyAsync()` method: +Getting a property in asynchronous manner is also possible, in callback-based, future-based, or (with C++20) awaitable way, by calling `IProxy::getPropertyAsync()` method: ```c++ // Callback-based method: @@ -1347,10 +1391,15 @@ auto callback = [](std::optional /*error*/, sdbus::Variant value) std::cout << "Got property value: " << value.get() << std::endl; }; uint32_t status = proxy->getPropertyAsync("status").onInterface("org.sdbuscpp.Concatenator").uponReplyInvoke(std::move(callback)); + // Future-based method: std::future statusFuture = object.getPropertyAsync("status").onInterface("org.sdbuscpp.Concatenator").getResultAsFuture(); ... std::cout << "Got property value: " << statusFuture.get().get() << std::endl; + +// Awaitable method (C++20): +auto value = co_await proxy->getPropertyAsync("status").onInterface("org.sdbuscpp.Concatenator").getResultAsAwaitable(); +std::cout << "Got property value: " << value.get() << std::endl; ``` More information on an `error` callback handler parameter, on behavior of `future` in erroneous situations, can be found in section [Asynchronous client-side methods](#asynchronous-client-side-methods). @@ -1364,14 +1413,18 @@ uint32_t status = ...; proxy->setProperty("status").onInterface("org.sdbuscpp.Concatenator").toValue(status); ``` -Setting a property in asynchronous manner is also possible, in both callback-based and future-based way, by calling `IProxy::setPropertyAsync()` method: +Setting a property in asynchronous manner is also possible, in callback-based, future-based, or awaitable way, by calling `IProxy::setPropertyAsync()` method: ```c++ // Callback-based method: auto callback = [](std::optional error { /*... Error handling in case error contains a value...*/ }; uint32_t status = proxy->setPropertyAsync("status").onInterface("org.sdbuscpp.Concatenator").toValue(status).uponReplyInvoke(std::move(callback)); + // Future-based method: std::future statusFuture = object.setPropertyAsync("status").onInterface("org.sdbuscpp.Concatenator").getResultAsFuture(); + +// Awaitable method (C++20): +co_await proxy->setPropertyAsync("status").onInterface("org.sdbuscpp.Concatenator").toValue(status).getResultAsAwaitable(); ``` More information on `error` callback handler parameter, on behavior of `future` in erroneous situations, can be found in section [Asynchronous client-side methods](#asynchronous-client-side-methods). @@ -1468,7 +1521,7 @@ When implementing the adaptor, we simply need to provide the body for the `statu We can mark the property so that the generator generates either asynchronous variant of getter method, or asynchronous variant of setter method, or both. Annotations names are `org.freedesktop.DBus.Property.Get.Async`, or `org.freedesktop.DBus.Property.Set.Async`, respectively. Their values must be set to `client`. -In addition, we can choose through annotations `org.freedesktop.DBus.Property.Get.Async.ClientImpl`, or `org.freedesktop.DBus.Property.Set.Async.ClientImpl`, respectively, whether a callback-based or future-based variant will be generated. The concept is analogous to the one for asynchronous D-Bus methods described above in this document. +In addition, we can choose through annotations `org.freedesktop.DBus.Property.Get.Async.ClientImpl`, or `org.freedesktop.DBus.Property.Set.Async.ClientImpl`, respectively, whether a callback-based, future-based, or awaitable variant will be generated. Supported values are `callback`, `future`, and `awaitable`. The concept is analogous to the one for asynchronous D-Bus methods described above in this document. The callback-based method will generate a pure virtual function `OnProperty[Get|Set]Reply()`, which must be overridden by the derived class. @@ -1679,7 +1732,7 @@ The macro must be placed in the global namespace. The first argument is the stru This is described in detail in the following sections. -> **_Note_:** The macro supports **max 16 struct members**. If you need more, feel free to open an issue, or implement the teaching code yourself :o) +> **_Note_:** The macro supports **max 16 struct members**. If you need more, feel free to open an issue, or implement the teaching code yourself :o) > **_Another note_:** You may have noticed one of `my::Struct` members is `std::list`. Thanks to the custom support for `std::list` implemented higher above, it's now automatically accepted by sdbus-c++ as a D-Bus array representation. From 6b3bdbb3c0e02388157c2dd840866a41c3e43a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20Angelovi=C4=8D?= Date: Fri, 10 Apr 2026 12:32:20 +0200 Subject: [PATCH 10/15] refactor: use variant type for result --- include/sdbus-c++/Awaitable.h | 21 ++++------- include/sdbus-c++/ConvenienceApiClasses.inl | 41 +++++++++------------ src/Proxy.cpp | 13 ++----- 3 files changed, 29 insertions(+), 46 deletions(-) diff --git a/include/sdbus-c++/Awaitable.h b/include/sdbus-c++/Awaitable.h index bbcca71b..7cfd4d66 100644 --- a/include/sdbus-c++/Awaitable.h +++ b/include/sdbus-c++/Awaitable.h @@ -33,7 +33,6 @@ #include #include #include -#include #include #include @@ -61,8 +60,7 @@ template struct AwaitableData { using result_type = std::conditional_t, std::monostate, T>; - std::optional result; - std::optional exception; + std::variant result; std::coroutine_handle<> handle; std::atomic status{AwaitableState::NotReady}; }; @@ -89,7 +87,10 @@ template class Awaitable { public: - explicit Awaitable(std::shared_ptr> data) : data_(std::move(data)) {}; + explicit Awaitable(std::shared_ptr> data) + : data_(std::move(data)) + { + } // Called when the coroutine is co_await'ed. Returns true if the coroutine should be suspended. [[nodiscard]] bool await_ready() const noexcept @@ -111,19 +112,13 @@ class Awaitable // Called when the coroutine is resumed. Returns the result or throws the exception. T await_resume() const { - if (data_->exception) - { - std::rethrow_exception(*data_->exception); - } + if (auto* exception = std::get_if(&data_->result); exception != nullptr) + std::rethrow_exception(*exception); if constexpr (std::is_void_v) - { return; - } else - { - return std::move(*data_->result); - } + return std::get(std::move(data_->result)); } private: diff --git a/include/sdbus-c++/ConvenienceApiClasses.inl b/include/sdbus-c++/ConvenienceApiClasses.inl index 7122f31c..f636ca2f 100644 --- a/include/sdbus-c++/ConvenienceApiClasses.inl +++ b/include/sdbus-c++/ConvenienceApiClasses.inl @@ -357,34 +357,27 @@ namespace sdbus { template Awaitable> AsyncMethodInvoker::getResultAsAwaitable() { + // awaitable_return_t will be void for no D-Bus method return value + // or T for single D-Bus method return value + // or std::tuple<...> for multiple method return values auto data = std::make_shared>>(); - uponReplyInvoke( - [data](std::optional error, Args... args) - { - if (error) - { - data->exception = std::make_exception_ptr(*std::move(error)); - } + + uponReplyInvoke([data](std::optional error, Args... args) + { + if (!error) + if constexpr (!std::is_void_v>) + data->result = {std::move(args)...}; else - { - if constexpr (!std::is_void_v>) - { - data->result.emplace(std::move(args)...); - } - else - { - data->result = std::monostate{}; - } - } + data->result = std::monostate{}; + else + data->result = std::make_exception_ptr(*std::move(error)); - auto previous = data->status.exchange(AwaitableState::Completed, std::memory_order_acq_rel); - if (previous == AwaitableState::Waiting) - { - data->handle.resume(); - } - }); + auto previous = data->status.exchange(AwaitableState::Completed, std::memory_order_acq_rel); + if (previous == AwaitableState::Waiting) + data->handle.resume(); + }); - return Awaitable>(data); + return Awaitable(data); } /*** ---------------- ***/ diff --git a/src/Proxy.cpp b/src/Proxy.cpp index aed6d9e9..c3506044 100644 --- a/src/Proxy.cpp +++ b/src/Proxy.cpp @@ -182,7 +182,8 @@ std::future Proxy::callMethodAsync(const MethodCall& message, uint6 return future; } -Awaitable Proxy::callMethodAsync(const MethodCall& message, with_awaitable_t) { +Awaitable Proxy::callMethodAsync(const MethodCall& message, with_awaitable_t) +{ return Proxy::callMethodAsync(message, /*timeout*/ 0, with_awaitable); } @@ -192,24 +193,18 @@ Awaitable Proxy::callMethodAsync(const MethodCall& message, uint64_ async_reply_handler asyncReplyCallback = [data](MethodReply reply, std::optional error) noexcept { if (!error) - { data->result = std::move(reply); - } else - { - data->exception = std::make_exception_ptr(*std::move(error)); - } + data->result = std::make_exception_ptr(*std::move(error)); auto previous = data->status.exchange(AwaitableState::Completed, std::memory_order_acq_rel); if (previous == AwaitableState::Waiting) - { data->handle.resume(); - } }; (void)Proxy::callMethodAsync(message, std::move(asyncReplyCallback), timeout); - return Awaitable{data}; + return Awaitable{data}; } void Proxy::registerSignalHandler( const InterfaceName& interfaceName From da37295fcfd9a74c40ea1e7521cabb01be20b31f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20Angelovi=C4=8D?= Date: Fri, 10 Apr 2026 16:13:05 +0200 Subject: [PATCH 11/15] feat: use conditional coroutine support in public API This is to make the public API buildable also under C++17. --- include/sdbus-c++/Awaitable.h | 18 ++++++++++++++++++ include/sdbus-c++/ConvenienceApiClasses.inl | 2 +- src/Proxy.cpp | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/include/sdbus-c++/Awaitable.h b/include/sdbus-c++/Awaitable.h index 7cfd4d66..2aa48974 100644 --- a/include/sdbus-c++/Awaitable.h +++ b/include/sdbus-c++/Awaitable.h @@ -29,7 +29,9 @@ #define SDBUS_CXX_AWAITABLE_H_ #include +#if __has_include() #include +#endif #include #include #include @@ -61,8 +63,17 @@ struct AwaitableData { using result_type = std::conditional_t, std::monostate, T>; std::variant result; +#ifdef __cpp_lib_coroutine std::coroutine_handle<> handle; +#endif // __cpp_lib_coroutine std::atomic status{AwaitableState::NotReady}; + + void resumeCoroutine() + { +#ifdef __cpp_lib_coroutine + handle.resume(); +#endif // __cpp_lib_coroutine + } }; /********************************************//** @@ -82,6 +93,9 @@ struct AwaitableData * instance, such as IProxy::callMethodAsync with with_awaitable_t tag, * or the .getResultAsAwaitable() methods of the high-level API. * + * The class represents nothing, i.e. is a simple placeholder class, if the API + * is used as C++17 or with a standard library not supporting coroutines. + * ***********************************************/ template class Awaitable @@ -92,6 +106,8 @@ class Awaitable { } +#ifdef __cpp_lib_coroutine + // Called when the coroutine is co_await'ed. Returns true if the coroutine should be suspended. [[nodiscard]] bool await_ready() const noexcept { @@ -121,6 +137,8 @@ class Awaitable return std::get(std::move(data_->result)); } +#endif // __cpp_lib_coroutine + private: std::shared_ptr> data_; }; diff --git a/include/sdbus-c++/ConvenienceApiClasses.inl b/include/sdbus-c++/ConvenienceApiClasses.inl index f636ca2f..4ce3f436 100644 --- a/include/sdbus-c++/ConvenienceApiClasses.inl +++ b/include/sdbus-c++/ConvenienceApiClasses.inl @@ -374,7 +374,7 @@ namespace sdbus { auto previous = data->status.exchange(AwaitableState::Completed, std::memory_order_acq_rel); if (previous == AwaitableState::Waiting) - data->handle.resume(); + data->resumeCoroutine(); }); return Awaitable(data); diff --git a/src/Proxy.cpp b/src/Proxy.cpp index c3506044..1c5377f3 100644 --- a/src/Proxy.cpp +++ b/src/Proxy.cpp @@ -199,7 +199,7 @@ Awaitable Proxy::callMethodAsync(const MethodCall& message, uint64_ auto previous = data->status.exchange(AwaitableState::Completed, std::memory_order_acq_rel); if (previous == AwaitableState::Waiting) - data->handle.resume(); + data->resumeCoroutine(); }; (void)Proxy::callMethodAsync(message, std::move(asyncReplyCallback), timeout); From 341bfc240ab6fdf451b16ee00fa5a13d97dda009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20Angelovi=C4=8D?= Date: Fri, 10 Apr 2026 16:26:14 +0200 Subject: [PATCH 12/15] refactor: make Awaitable c-tor private --- include/sdbus-c++/Awaitable.h | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/include/sdbus-c++/Awaitable.h b/include/sdbus-c++/Awaitable.h index 2aa48974..b680843a 100644 --- a/include/sdbus-c++/Awaitable.h +++ b/include/sdbus-c++/Awaitable.h @@ -29,6 +29,7 @@ #define SDBUS_CXX_AWAITABLE_H_ #include +#include #if __has_include() #include #endif @@ -38,8 +39,13 @@ #include #include -namespace sdbus -{ +namespace sdbus { + + // Forward declarations + class AsyncMethodInvoker; + namespace internal { + class Proxy; + } // namespace internal /********************************************//** * @enum AwaitableState @@ -100,14 +106,8 @@ struct AwaitableData template class Awaitable { -public: - explicit Awaitable(std::shared_ptr> data) - : data_(std::move(data)) - { - } - #ifdef __cpp_lib_coroutine - +public: // Called when the coroutine is co_await'ed. Returns true if the coroutine should be suspended. [[nodiscard]] bool await_ready() const noexcept { @@ -136,10 +136,18 @@ class Awaitable else return std::get(std::move(data_->result)); } - #endif // __cpp_lib_coroutine private: + friend internal::Proxy; + friend AsyncMethodInvoker; + + explicit Awaitable(std::shared_ptr> data) + : data_(std::move(data)) + { + assert(data_ != nullptr); + } + std::shared_ptr> data_; }; From 8466b899ddd36ad7b1a53c71e0ab574c997f1a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20Angelovi=C4=8D?= Date: Fri, 10 Apr 2026 16:26:39 +0200 Subject: [PATCH 13/15] style: reformat file with no semantic changes --- include/sdbus-c++/Awaitable.h | 186 +++++++++++++++++----------------- 1 file changed, 93 insertions(+), 93 deletions(-) diff --git a/include/sdbus-c++/Awaitable.h b/include/sdbus-c++/Awaitable.h index b680843a..bc687354 100644 --- a/include/sdbus-c++/Awaitable.h +++ b/include/sdbus-c++/Awaitable.h @@ -47,109 +47,109 @@ namespace sdbus { class Proxy; } // namespace internal -/********************************************//** - * @enum AwaitableState - * - * Represents the lifecycle state of an asynchronous - * operation in the coroutine awaitable protocol. - * Used for atomic coordination between the coroutine - * and the D-Bus callback thread. - * - ***********************************************/ -enum class AwaitableState : uint8_t -{ - NotReady, // Initial state: callback hasn't fired yet - Waiting, // Coroutine is suspended and waiting for callback - Completed // Callback completed, result is ready -}; - -// Shared data -template -struct AwaitableData -{ - using result_type = std::conditional_t, std::monostate, T>; - std::variant result; -#ifdef __cpp_lib_coroutine - std::coroutine_handle<> handle; -#endif // __cpp_lib_coroutine - std::atomic status{AwaitableState::NotReady}; - - void resumeCoroutine() + /********************************************//** + * @enum AwaitableState + * + * Represents the lifecycle state of an asynchronous + * operation in the coroutine awaitable protocol. + * Used for atomic coordination between the coroutine + * and the D-Bus callback thread. + * + ***********************************************/ + enum class AwaitableState : uint8_t { + NotReady, // Initial state: callback hasn't fired yet + Waiting, // Coroutine is suspended and waiting for callback + Completed // Callback completed, result is ready + }; + + // Shared data + template + struct AwaitableData + { + using result_type = std::conditional_t, std::monostate, T>; + std::variant result; #ifdef __cpp_lib_coroutine - handle.resume(); + std::coroutine_handle<> handle; #endif // __cpp_lib_coroutine - } -}; + std::atomic status{AwaitableState::NotReady}; -/********************************************//** - * @class Awaitable - * - * A C++20 coroutine awaitable that represents an asynchronous - * operation. Allows suspending a coroutine until a D-Bus method - * call completes, then resuming with the result or exception. - * - * This is not a full-fledged coroutine type, but a simple awaitable - * that can be used with `co_await` to retrieve results of async D-Bus calls. - * This is independent of any specific coroutine framework or scheduler, - * as it relies on the D-Bus callback mechanism to resume the coroutine. - * - * You most likely don't need to use this class directly. Instead, use the - * respective low-level or high-level API functions that return an Awaitable - * instance, such as IProxy::callMethodAsync with with_awaitable_t tag, - * or the .getResultAsAwaitable() methods of the high-level API. - * - * The class represents nothing, i.e. is a simple placeholder class, if the API - * is used as C++17 or with a standard library not supporting coroutines. - * - ***********************************************/ -template -class Awaitable -{ + void resumeCoroutine() + { #ifdef __cpp_lib_coroutine -public: - // Called when the coroutine is co_await'ed. Returns true if the coroutine should be suspended. - [[nodiscard]] bool await_ready() const noexcept - { - return data_->status.load(std::memory_order_acquire) == AwaitableState::Completed; - } - - // Called when the coroutine is suspended, returning false here will immediately - // resume the coroutine. - bool await_suspend(std::coroutine_handle<> handle) noexcept - { - data_->handle = handle; - - // Attempt transition from NotReady to Waiting. - AwaitableState expected = AwaitableState::NotReady; - return data_->status.compare_exchange_strong(expected, AwaitableState::Waiting, std::memory_order_acq_rel); - } - - // Called when the coroutine is resumed. Returns the result or throws the exception. - T await_resume() const + handle.resume(); +#endif // __cpp_lib_coroutine + } + }; + + /********************************************//** + * @class Awaitable + * + * A C++20 coroutine awaitable that represents an asynchronous + * operation. Allows suspending a coroutine until a D-Bus method + * call completes, then resuming with the result or exception. + * + * This is not a full-fledged coroutine type, but a simple awaitable + * that can be used with `co_await` to retrieve results of async D-Bus calls. + * This is independent of any specific coroutine framework or scheduler, + * as it relies on the D-Bus callback mechanism to resume the coroutine. + * + * You most likely don't need to use this class directly. Instead, use the + * respective low-level or high-level API functions that return an Awaitable + * instance, such as IProxy::callMethodAsync with with_awaitable_t tag, + * or the .getResultAsAwaitable() methods of the high-level API. + * + * The class represents nothing, i.e. is a simple placeholder class, if the API + * is used as C++17 or with a standard library not supporting coroutines. + * + ***********************************************/ + template + class Awaitable { - if (auto* exception = std::get_if(&data_->result); exception != nullptr) - std::rethrow_exception(*exception); - - if constexpr (std::is_void_v) - return; - else - return std::get(std::move(data_->result)); - } +#ifdef __cpp_lib_coroutine + public: + // Called when the coroutine is co_await'ed. Returns true if the coroutine should be suspended. + [[nodiscard]] bool await_ready() const noexcept + { + return data_->status.load(std::memory_order_acquire) == AwaitableState::Completed; + } + + // Called when the coroutine is suspended, returning false here will immediately + // resume the coroutine. + bool await_suspend(std::coroutine_handle<> handle) noexcept + { + data_->handle = handle; + + // Attempt transition from NotReady to Waiting. + AwaitableState expected = AwaitableState::NotReady; + return data_->status.compare_exchange_strong(expected, AwaitableState::Waiting, std::memory_order_acq_rel); + } + + // Called when the coroutine is resumed. Returns the result or throws the exception. + T await_resume() const + { + if (auto* exception = std::get_if(&data_->result); exception != nullptr) + std::rethrow_exception(*exception); + + if constexpr (std::is_void_v) + return; + else + return std::get(std::move(data_->result)); + } #endif // __cpp_lib_coroutine -private: - friend internal::Proxy; - friend AsyncMethodInvoker; + private: + friend internal::Proxy; + friend AsyncMethodInvoker; - explicit Awaitable(std::shared_ptr> data) - : data_(std::move(data)) - { - assert(data_ != nullptr); - } + explicit Awaitable(std::shared_ptr> data) + : data_(std::move(data)) + { + assert(data_ != nullptr); + } - std::shared_ptr> data_; -}; + std::shared_ptr> data_; + }; } // namespace sdbus From eef7d0fc0c73049a40daa409a4b64a5eec14a3a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20Angelovi=C4=8D?= Date: Fri, 10 Apr 2026 16:51:29 +0200 Subject: [PATCH 14/15] fix: fix clang-tidy issues --- include/sdbus-c++/Awaitable.h | 2 +- .../DBusAwaitableMethodsTests.cpp | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/include/sdbus-c++/Awaitable.h b/include/sdbus-c++/Awaitable.h index bc687354..2a9e638b 100644 --- a/include/sdbus-c++/Awaitable.h +++ b/include/sdbus-c++/Awaitable.h @@ -126,7 +126,7 @@ namespace sdbus { } // Called when the coroutine is resumed. Returns the result or throws the exception. - T await_resume() const + [[nodiscard]] T await_resume() const { if (auto* exception = std::get_if(&data_->result); exception != nullptr) std::rethrow_exception(*exception); diff --git a/tests/integrationtests/DBusAwaitableMethodsTests.cpp b/tests/integrationtests/DBusAwaitableMethodsTests.cpp index ef47cecb..092dfc00 100644 --- a/tests/integrationtests/DBusAwaitableMethodsTests.cpp +++ b/tests/integrationtests/DBusAwaitableMethodsTests.cpp @@ -31,7 +31,6 @@ #include #include #include -#include #include #include @@ -40,7 +39,6 @@ #include "TestFixture.h" #include "TestProxy.h" -#include "Defs.h" using ::testing::Eq; using namespace std::chrono_literals; @@ -82,14 +80,14 @@ struct Task { return {}; } - void return_value(T v) { value = std::move(v); } + void return_value(T val) { value = std::move(val); } void unhandled_exception() { exception = std::current_exception(); } }; std::coroutine_handle handle; // Ctor and rule of 5 for proper handle management - explicit Task(std::coroutine_handle h) : handle(h) {} + explicit Task(std::coroutine_handle hnd) : handle(hnd) {} Task(Task&& other) noexcept : handle(std::exchange(other.handle, {})) {} Task& operator=(Task&& other) noexcept { if (this != &other) { @@ -121,7 +119,7 @@ struct Task { return Task{std::coroutine_handle::from_promise(*this)}; } - std::suspend_always initial_suspend() noexcept { return {}; } + std::suspend_always initial_suspend() noexcept { return {}; } // NOLINT(readability-convert-member-functions-to-static) std::suspend_always final_suspend() noexcept { @@ -142,7 +140,7 @@ struct Task { std::coroutine_handle handle; - explicit Task(std::coroutine_handle h) : handle(h) {} + explicit Task(std::coroutine_handle hnd) : handle(hnd) {} Task(Task&& other) noexcept : handle(std::exchange(other.handle, {})) {} Task& operator=(Task&& other) noexcept { if (this != &other) { @@ -156,8 +154,8 @@ struct Task { ~Task() { if (handle) handle.destroy(); } - void resume() { if (handle && !handle.done()) handle.resume(); } - void get() { handle.promise().future.get(); } + void resume() { if (handle && !handle.done()) handle.resume(); } // NOLINT(readability-make-member-function-const) + void get() { handle.promise().future.get(); } // NOLINT(readability-make-member-function-const) }; /*-------------------------------------*/ @@ -195,9 +193,11 @@ TYPED_TEST(AsyncSdbusTestObject, InvokesMethodWithLargeDataAsynchronouslyOnClien for (int32_t i = 0; i < 40'000; ++i) largeMap.emplace(i, "This is string nr. " + std::to_string(i+1)); - auto task = [&largeMap, this]() -> Task> { + // NOLINTNEXTLINE(cppcoreguidelines-avoid-capturing-lambda-coroutines) -- lambda closure has guaranteed lifetime + auto lambda = [&largeMap, this]() -> Task> { co_return co_await this->m_proxy->doOperationWithLargeDataClientSideAsync(largeMap, sdbus::with_awaitable); - }(); + }; + auto task = lambda(); task.resume(); From 8d7bee896e9689e7caaca3d110db10d2001b8064 Mon Sep 17 00:00:00 2001 From: Alex Cani Date: Sat, 18 Apr 2026 21:19:15 +0200 Subject: [PATCH 15/15] refactor: rearrange members of AwaitableData Have the coroutine handle be the last member in AwaitableData to keep the class layout compatible between C++ 17 and C++20 or later clients. --- include/sdbus-c++/Awaitable.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/sdbus-c++/Awaitable.h b/include/sdbus-c++/Awaitable.h index 2a9e638b..e3c5f897 100644 --- a/include/sdbus-c++/Awaitable.h +++ b/include/sdbus-c++/Awaitable.h @@ -69,10 +69,12 @@ namespace sdbus { { using result_type = std::conditional_t, std::monostate, T>; std::variant result; + std::atomic status{AwaitableState::NotReady}; #ifdef __cpp_lib_coroutine + // Keep the handle as the last member to mainting ABI compatibility + // with clients without coroutine support. std::coroutine_handle<> handle; #endif // __cpp_lib_coroutine - std::atomic status{AwaitableState::NotReady}; void resumeCoroutine() {