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 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/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. diff --git a/include/sdbus-c++/Awaitable.h b/include/sdbus-c++/Awaitable.h new file mode 100644 index 00000000..e3c5f897 --- /dev/null +++ b/include/sdbus-c++/Awaitable.h @@ -0,0 +1,158 @@ +/** + * (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 +#if __has_include() +#include +#endif +#include +#include +#include +#include +#include + +namespace sdbus { + + // Forward declarations + class AsyncMethodInvoker; + namespace internal { + 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; + 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 + + void resumeCoroutine() + { +#ifdef __cpp_lib_coroutine + 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 + { +#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. + [[nodiscard]] 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; + + explicit Awaitable(std::shared_ptr> data) + : data_(std::move(data)) + { + assert(data_ != nullptr); + } + + std::shared_ptr> data_; + }; + +} // namespace sdbus + +#endif // SDBUS_CXX_AWAITABLE_H_ 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..4ce3f436 100644 --- a/include/sdbus-c++/ConvenienceApiClasses.inl +++ b/include/sdbus-c++/ConvenienceApiClasses.inl @@ -354,6 +354,32 @@ namespace sdbus { return future; } + 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) + if constexpr (!std::is_void_v>) + data->result = {std::move(args)...}; + else + 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->resumeCoroutine(); + }); + + return Awaitable(data); + } + /*** ---------------- ***/ /*** SignalSubscriber ***/ /*** ---------------- ***/ @@ -524,6 +550,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 +676,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 +759,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++/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++/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; }; diff --git a/include/sdbus-c++/TypeTraits.h b/include/sdbus-c++/TypeTraits.h index 39ed6351..5e6e9af0 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; @@ -586,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; diff --git a/src/Proxy.cpp b/src/Proxy.cpp index 1d567194..1c5377f3 100644 --- a/src/Proxy.cpp +++ b/src/Proxy.cpp @@ -38,6 +38,7 @@ #include "Utils.h" #include +#include #include #include #include @@ -181,6 +182,31 @@ 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->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->resumeCoroutine(); + }; + + (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 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..092dfc00 --- /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 "TestFixture.h" +#include "TestProxy.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 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 hnd) : handle(hnd) {} + 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 {}; } // NOLINT(readability-convert-member-functions-to-static) + + 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 hnd) : handle(hnd) {} + 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(); } // NOLINT(readability-make-member-function-const) + void get() { handle.promise().future.get(); } // NOLINT(readability-make-member-function-const) +}; + +/*-------------------------------------*/ +/* -- 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)); + + // 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(); + + 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(); 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..2541c9a7 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, Awaitable }; + /** * 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,13 @@ 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; + 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; @@ -211,7 +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 = (async && !dontExpectReply ? (future ? "std::future<" + retType + ">" : "sdbus::PendingAsyncCall") : async ? "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; @@ -220,13 +244,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,16 +264,20 @@ 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) { 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)); })"; @@ -315,10 +343,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 +353,46 @@ 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.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) - 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; + 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 = (asyncGet ? (futureGet ? "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; - 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,11 +401,15 @@ 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) { 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)); })"; @@ -378,25 +426,43 @@ 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"); + // 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; - 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) { 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)); })";