From 290078d6af2cc631c34a48b79d9a6eb69a9d265d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stanislav=20Angelovi=C4=8D?= Date: Thu, 14 Sep 2023 10:54:57 +0200 Subject: [PATCH] feat: add support for async property get/set on client-side (#354) * feat: add async property get/set convenience support classes * feat: add no-reply and async overloads to Properties_proxy * feat: add convenience functions for GetAll functionality * test: add tests for new functionality * add codegen IDL support and documentation --- docs/using-sdbus-c++.md | 119 +++++++++- include/sdbus-c++/ConvenienceApiClasses.h | 68 +++++- include/sdbus-c++/ConvenienceApiClasses.inl | 204 ++++++++++++++++-- include/sdbus-c++/IProxy.h | 122 +++++++++-- include/sdbus-c++/StandardInterfaces.h | 42 +++- include/sdbus-c++/TypeTraits.h | 3 + .../DBusStandardInterfacesTests.cpp | 83 +++++++ tools/xml2cpp-codegen/ProxyGenerator.cpp | 99 +++++++-- tools/xml2cpp-codegen/ProxyGenerator.h | 2 +- 9 files changed, 685 insertions(+), 57 deletions(-) diff --git a/docs/using-sdbus-c++.md b/docs/using-sdbus-c++.md index 7382e1a..3eb1bc1 100644 --- a/docs/using-sdbus-c++.md +++ b/docs/using-sdbus-c++.md @@ -1256,9 +1256,67 @@ Annotate the element with `org.freedesktop.DBus.Method.Timeout` in order to spec Using D-Bus properties ---------------------- +sdbus-c++ provides functionality for convenient working with D-Bus properties, on both convenience and generated code API level. + +### Convenience API + +Let's say a remote D-Bus object provides property `status` of type `u` under interface `org.sdbuscpp.Concatenator`. + +#### Reading a property + +We read property value easily through `IProxy::getProperty()` method: + +```c++ +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: + +```c++ +// Callback-based method: +auto callback = [](const sdbus::Error* err, 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; +``` + +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). + +#### Writing a property + +Writing a property is equally simple, through `IProxy::setProperty()`: + +```c++ +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: + +```c++ +// Callback-based method: +auto callback = [](const sdbus::Error* err) { /*... Error handling in case err is non-null...*/ }; +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(); +``` + +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). + +#### Getting all properties + +In a very analogous way, with both synchronous and asynchronous options, it's possible to read all properties of an object under given interface at once. `IProxy::getAllProperties()` is what you're looking for. + +### Generated bindings API + Defining and working with D-Bus properties using XML description is quite easy. -### Defining a property in the IDL +#### Defining a property in the IDL A property element has no arg child element. It just has the attributes name, type and access, which are all mandatory. The access attribute allows the values ‘readwrite’, ‘read’, and ‘write’. @@ -1276,7 +1334,9 @@ An example of a read-write property `status`: ``` -### Generated C++ bindings +The property may also have annotations. In addition to standard annotations defined in D-Bus specification, there are sdbus-c++-specific ones, discussed further below. + +#### Generated C++ bindings This is how generated adaptor and proxy classes would look like with the read-write `status` property. The adaptor: @@ -1329,7 +1389,60 @@ public: }; ``` -When implementing the adaptor, we simply need to provide the body for `status` getter and setter method by overriding them. Then in the proxy, we just call them. +When implementing the adaptor, we simply need to provide the body for the `status` getter and setter methods by overriding them. Then in the proxy, we just call them. + +#### Client-side asynchronous properties + +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. + +The callback-based method will generate a pure virtual function `OnProperty[Get|Set]Reply()`, which must be overridden by the derived class. + +For example, this description: + +```xml + + + + + + + + + + + + +``` + +will get generated into this C++ code on client side: + +```cpp +class PropertyProvider_proxy +{ + /*...*/ + + virtual void onStatusPropertyGetReply(const uint32_t& value, const sdbus::Error* error) = 0; + +public: + // getting the property value + sdbus::PendingAsyncCall status() + { + return object_->getPropertyAsync("status").onInterface(INTERFACE_NAME).uponReplyInvoke([this](const sdbus::Error* error, const sdbus::Variant& value){ this->onActionPropertyGetReply(value.get(), error); }); + } + + // setting the property value + void status(const uint32_t& value) + { + object_->setProperty("status").onInterface(INTERFACE_NAME).toValue(value); + } + + /*...*/ +}; +``` + +In addition to custom generated code for getting/setting properties, `org.freedesktop.DBus.Properties` standard D-Bus interface, implemented through pre-defined `sdbus::Properties_proxy` in `sdbus-c++/StandardInterfaces.h`, can also be used for reading/writing properties. See next section. Standard D-Bus interfaces ------------------------- diff --git a/include/sdbus-c++/ConvenienceApiClasses.h b/include/sdbus-c++/ConvenienceApiClasses.h index abfd6c3..261589c 100644 --- a/include/sdbus-c++/ConvenienceApiClasses.h +++ b/include/sdbus-c++/ConvenienceApiClasses.h @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -41,7 +42,6 @@ namespace sdbus { class IObject; class IProxy; - class Variant; class Error; class PendingAsyncCall; } @@ -225,7 +225,7 @@ namespace sdbus { { public: SignalUnsubscriber(IProxy& proxy, const std::string& signalName); - void onInterface(std::string interfaceName); + void onInterface(const std::string& interfaceName); private: IProxy& proxy_; @@ -236,25 +236,81 @@ namespace sdbus { { public: PropertyGetter(IProxy& proxy, const std::string& propertyName); - sdbus::Variant onInterface(const std::string& interfaceName); + Variant onInterface(const std::string& interfaceName); private: IProxy& proxy_; const std::string& propertyName_; }; + class AsyncPropertyGetter + { + public: + AsyncPropertyGetter(IProxy& proxy, const std::string& propertyName); + AsyncPropertyGetter& onInterface(const std::string& interfaceName); + template PendingAsyncCall uponReplyInvoke(_Function&& callback); + std::future getResultAsFuture(); + + private: + IProxy& proxy_; + const std::string& propertyName_; + const std::string* interfaceName_{}; + }; + class PropertySetter { public: PropertySetter(IProxy& proxy, const std::string& propertyName); - PropertySetter& onInterface(std::string interfaceName); + PropertySetter& onInterface(const std::string& interfaceName); template void toValue(const _Value& value); - void toValue(const sdbus::Variant& value); + template void toValue(const _Value& value, dont_expect_reply_t); + void toValue(const Variant& value); + void toValue(const Variant& value, dont_expect_reply_t); private: IProxy& proxy_; const std::string& propertyName_; - std::string interfaceName_; + const std::string* interfaceName_{}; + }; + + class AsyncPropertySetter + { + public: + AsyncPropertySetter(IProxy& proxy, const std::string& propertyName); + AsyncPropertySetter& onInterface(const std::string& interfaceName); + template AsyncPropertySetter& toValue(_Value&& value); + AsyncPropertySetter& toValue(Variant value); + template PendingAsyncCall uponReplyInvoke(_Function&& callback); + std::future getResultAsFuture(); + + private: + IProxy& proxy_; + const std::string& propertyName_; + const std::string* interfaceName_{}; + Variant value_; + }; + + class AllPropertiesGetter + { + public: + AllPropertiesGetter(IProxy& proxy); + std::map onInterface(const std::string& interfaceName); + + private: + IProxy& proxy_; + }; + + class AsyncAllPropertiesGetter + { + public: + AsyncAllPropertiesGetter(IProxy& proxy); + AsyncAllPropertiesGetter& onInterface(const std::string& interfaceName); + template PendingAsyncCall uponReplyInvoke(_Function&& callback); + std::future> getResultAsFuture(); + + private: + IProxy& proxy_; + const std::string* interfaceName_{}; }; } diff --git a/include/sdbus-c++/ConvenienceApiClasses.inl b/include/sdbus-c++/ConvenienceApiClasses.inl index 5c292a2..1ffc37a 100644 --- a/include/sdbus-c++/ConvenienceApiClasses.inl +++ b/include/sdbus-c++/ConvenienceApiClasses.inl @@ -293,7 +293,7 @@ namespace sdbus { template inline PropertyRegistrator& PropertyRegistrator::withGetter(_Function&& callback) { - static_assert(function_traits<_Function>::arity == 0, "Property getter function must not take any arguments"); + static_assert(function_argument_count_v<_Function> == 0, "Property getter function must not take any arguments"); static_assert(!std::is_void>::value, "Property getter function must return property value"); if (propertySignature_.empty()) @@ -311,7 +311,7 @@ namespace sdbus { template inline PropertyRegistrator& PropertyRegistrator::withSetter(_Function&& callback) { - static_assert(function_traits<_Function>::arity == 1, "Property setter function must take one parameter - the property value"); + static_assert(function_argument_count_v<_Function> == 1, "Property setter function must take one parameter - the property value"); static_assert(std::is_void>::value, "Property setter function must not return any value"); if (propertySignature_.empty()) @@ -704,7 +704,7 @@ namespace sdbus { { } - inline void SignalUnsubscriber::onInterface(std::string interfaceName) + inline void SignalUnsubscriber::onInterface(const std::string& interfaceName) { proxy_.unregisterSignalHandler(interfaceName, signalName_); } @@ -719,17 +719,56 @@ namespace sdbus { { } - inline sdbus::Variant PropertyGetter::onInterface(const std::string& interfaceName) + inline Variant PropertyGetter::onInterface(const std::string& interfaceName) { - sdbus::Variant var; - proxy_ - .callMethod("Get") - .onInterface("org.freedesktop.DBus.Properties") - .withArguments(interfaceName, propertyName_) - .storeResultsTo(var); + Variant var; + proxy_.callMethod("Get") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments(interfaceName, propertyName_) + .storeResultsTo(var); return var; } + /*** ------------------- ***/ + /*** AsyncPropertyGetter ***/ + /*** ------------------- ***/ + + inline AsyncPropertyGetter::AsyncPropertyGetter(IProxy& proxy, const std::string& propertyName) + : proxy_(proxy) + , propertyName_(propertyName) + { + } + + inline AsyncPropertyGetter& AsyncPropertyGetter::onInterface(const std::string& interfaceName) + { + interfaceName_ = &interfaceName; + + return *this; + } + + template + PendingAsyncCall AsyncPropertyGetter::uponReplyInvoke(_Function&& callback) + { + static_assert(std::is_invocable_r_v, "Property get callback function must accept Error* and property value as Variant"); + + assert(interfaceName_ != nullptr); // onInterface() must be placed/called prior to this function + + return proxy_.callMethodAsync("Get") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments(*interfaceName_, propertyName_) + .uponReplyInvoke(std::forward<_Function>(callback)); + } + + inline std::future AsyncPropertyGetter::getResultAsFuture() + { + assert(interfaceName_ != nullptr); // onInterface() must be placed/called prior to this function + + return proxy_.callMethodAsync("Get") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments(*interfaceName_, propertyName_) + .getResultAsFuture(); + } + /*** -------------- ***/ /*** PropertySetter ***/ /*** -------------- ***/ @@ -740,9 +779,9 @@ namespace sdbus { { } - inline PropertySetter& PropertySetter::onInterface(std::string interfaceName) + inline PropertySetter& PropertySetter::onInterface(const std::string& interfaceName) { - interfaceName_ = std::move(interfaceName); + interfaceName_ = &interfaceName; return *this; } @@ -750,17 +789,144 @@ namespace sdbus { template inline void PropertySetter::toValue(const _Value& value) { - PropertySetter::toValue(sdbus::Variant{value}); + PropertySetter::toValue(Variant{value}); } - inline void PropertySetter::toValue(const sdbus::Variant& value) + template + inline void PropertySetter::toValue(const _Value& value, dont_expect_reply_t) { - assert(!interfaceName_.empty()); // onInterface() must be placed/called prior to this function + PropertySetter::toValue(Variant{value}, dont_expect_reply); + } - proxy_ - .callMethod("Set") - .onInterface("org.freedesktop.DBus.Properties") - .withArguments(interfaceName_, propertyName_, value); + inline void PropertySetter::toValue(const Variant& value) + { + assert(interfaceName_ != nullptr); // onInterface() must be placed/called prior to this function + + proxy_.callMethod("Set") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments(*interfaceName_, propertyName_, value); + } + + inline void PropertySetter::toValue(const Variant& value, dont_expect_reply_t) + { + assert(interfaceName_ != nullptr); // onInterface() must be placed/called prior to this function + + proxy_.callMethod("Set") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments(*interfaceName_, propertyName_, value) + .dontExpectReply(); + } + + /*** ------------------- ***/ + /*** AsyncPropertySetter ***/ + /*** ------------------- ***/ + + inline AsyncPropertySetter::AsyncPropertySetter(IProxy& proxy, const std::string& propertyName) + : proxy_(proxy) + , propertyName_(propertyName) + { + } + + inline AsyncPropertySetter& AsyncPropertySetter::onInterface(const std::string& interfaceName) + { + interfaceName_ = &interfaceName; + + return *this; + } + + template + inline AsyncPropertySetter& AsyncPropertySetter::toValue(_Value&& value) + { + return AsyncPropertySetter::toValue(Variant{std::forward<_Value>(value)}); + } + + inline AsyncPropertySetter& AsyncPropertySetter::toValue(Variant value) + { + value_ = std::move(value); + + return *this; + } + + template + PendingAsyncCall AsyncPropertySetter::uponReplyInvoke(_Function&& callback) + { + static_assert(std::is_invocable_r_v, "Property set callback function must accept Error* only"); + + assert(interfaceName_ != nullptr); // onInterface() must be placed/called prior to this function + + return proxy_.callMethodAsync("Set") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments(*interfaceName_, propertyName_, std::move(value_)) + .uponReplyInvoke(std::forward<_Function>(callback)); + } + + inline std::future AsyncPropertySetter::getResultAsFuture() + { + assert(interfaceName_ != nullptr); // onInterface() must be placed/called prior to this function + + return proxy_.callMethodAsync("Set") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments(*interfaceName_, propertyName_, std::move(value_)) + .getResultAsFuture<>(); + } + + /*** ------------------- ***/ + /*** AllPropertiesGetter ***/ + /*** ------------------- ***/ + + inline AllPropertiesGetter::AllPropertiesGetter(IProxy& proxy) + : proxy_(proxy) + { + } + + inline std::map AllPropertiesGetter::onInterface(const std::string& interfaceName) + { + std::map props; + proxy_.callMethod("GetAll") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments(interfaceName) + .storeResultsTo(props); + return props; + } + + /*** ------------------------ ***/ + /*** AsyncAllPropertiesGetter ***/ + /*** ------------------------ ***/ + + inline AsyncAllPropertiesGetter::AsyncAllPropertiesGetter(IProxy& proxy) + : proxy_(proxy) + { + } + + inline AsyncAllPropertiesGetter& AsyncAllPropertiesGetter::onInterface(const std::string& interfaceName) + { + interfaceName_ = &interfaceName; + + return *this; + } + + template + PendingAsyncCall AsyncAllPropertiesGetter::uponReplyInvoke(_Function&& callback) + { + static_assert( std::is_invocable_r_v> + , "All properties get callback function must accept Error* and a map of property names to their values" ); + + assert(interfaceName_ != nullptr); // onInterface() must be placed/called prior to this function + + return proxy_.callMethodAsync("GetAll") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments(*interfaceName_) + .uponReplyInvoke(std::forward<_Function>(callback)); + } + + inline std::future> AsyncAllPropertiesGetter::getResultAsFuture() + { + assert(interfaceName_ != nullptr); // onInterface() must be placed/called prior to this function + + return proxy_.callMethodAsync("GetAll") + .onInterface("org.freedesktop.DBus.Properties") + .withArguments(*interfaceName_) + .getResultAsFuture>(); } } diff --git a/include/sdbus-c++/IProxy.h b/include/sdbus-c++/IProxy.h index a055872..2d44f71 100644 --- a/include/sdbus-c++/IProxy.h +++ b/include/sdbus-c++/IProxy.h @@ -83,7 +83,7 @@ namespace sdbus { virtual MethodCall createMethodCall(const std::string& interfaceName, const std::string& methodName) = 0; /*! - * @brief Calls method on the proxied D-Bus object + * @brief Calls method on the D-Bus object * * @param[in] message Message representing a method call * @param[in] timeout Timeout for dbus call in microseconds @@ -109,7 +109,7 @@ namespace sdbus { MethodReply callMethod(const MethodCall& message, const std::chrono::duration<_Rep, _Period>& timeout); /*! - * @brief Calls method on the proxied D-Bus object asynchronously + * @brief Calls method on the D-Bus object asynchronously * * @param[in] message Message representing an async method call * @param[in] asyncReplyCallback Handler for the async reply @@ -133,7 +133,7 @@ namespace sdbus { PendingAsyncCall callMethod(const MethodCall& message, async_reply_handler asyncReplyCallback, const std::chrono::duration<_Rep, _Period>& timeout); /*! - * @brief Registers a handler for the desired signal emitted by the proxied D-Bus object + * @brief Registers a handler for the desired signal emitted by the D-Bus object * * @param[in] interfaceName Name of an interface that the signal belongs to * @param[in] signalName Name of the signal @@ -178,7 +178,7 @@ namespace sdbus { virtual void unregister() = 0; /*! - * @brief Calls method on the proxied D-Bus object + * @brief Calls method on the D-Bus object * * @param[in] methodName Name of the method * @return A helper object for convenient invocation of the method @@ -199,7 +199,7 @@ namespace sdbus { [[nodiscard]] MethodInvoker callMethod(const std::string& methodName); /*! - * @brief Calls method on the proxied D-Bus object asynchronously + * @brief Calls method on the D-Bus object asynchronously * * @param[in] methodName Name of the method * @return A helper object for convenient asynchronous invocation of the method @@ -223,7 +223,7 @@ namespace sdbus { [[nodiscard]] AsyncMethodInvoker callMethodAsync(const std::string& methodName); /*! - * @brief Registers signal handler for a given signal of the proxied D-Bus object + * @brief Registers signal handler for a given signal of the D-Bus object * * @param[in] signalName Name of the signal * @return A helper object for convenient registration of the signal handler @@ -243,7 +243,7 @@ namespace sdbus { [[nodiscard]] SignalSubscriber uponSignal(const std::string& signalName); /*! - * @brief Unregisters signal handler of a given signal of the proxied D-Bus object + * @brief Unregisters signal handler of a given signal of the D-Bus object * * @param[in] signalName Name of the signal * @return A helper object for convenient unregistration of the signal handler @@ -260,7 +260,7 @@ namespace sdbus { [[nodiscard]] SignalUnsubscriber muteSignal(const std::string& signalName); /*! - * @brief Gets value of a property of the proxied D-Bus object + * @brief Gets value of a property of the D-Bus object * * @param[in] propertyName Name of the property * @return A helper object for convenient getting of property value @@ -279,23 +279,101 @@ namespace sdbus { [[nodiscard]] PropertyGetter getProperty(const std::string& propertyName); /*! - * @brief Sets value of a property of the proxied D-Bus object + * @brief Gets value of a property of the D-Bus object asynchronously + * + * @param[in] propertyName Name of the property + * @return A helper object for convenient asynchronous getting of property value + * + * This is a high-level, convenience way of reading D-Bus property values that abstracts + * from the D-Bus message concept. + * + * Example of use: + * @code + * std::future state = object.getPropertyAsync("state").onInterface("com.kistler.foo").getResultAsFuture(); + * auto callback = [](const sdbus::Error* err, const sdbus::Variant& value){ ... }; + * object.getPropertyAsync("state").onInterface("com.kistler.foo").uponReplyInvoke(std::move(callback)); + * @endcode + * + * @throws sdbus::Error in case of failure + */ + [[nodiscard]] AsyncPropertyGetter getPropertyAsync(const std::string& propertyName); + + /*! + * @brief Sets value of a property of the D-Bus object * * @param[in] propertyName Name of the property * @return A helper object for convenient setting of property value * * This is a high-level, convenience way of writing D-Bus property values that abstracts * from the D-Bus message concept. + * Setting property value with NoReply flag is also supported. + * + * Example of use: + * @code + * int state = ...; + * object_.setProperty("state").onInterface("com.kistler.foo").toValue(state); + * // Or we can just send the set message call without waiting for the reply + * object_.setProperty("state").onInterface("com.kistler.foo").toValue(state, dont_expect_reply); + * @endcode + * + * @throws sdbus::Error in case of failure + */ + [[nodiscard]] PropertySetter setProperty(const std::string& propertyName); + + /*! + * @brief Sets value of a property of the D-Bus object asynchronously + * + * @param[in] propertyName Name of the property + * @return A helper object for convenient asynchronous setting of property value + * + * This is a high-level, convenience way of writing D-Bus property values that abstracts + * from the D-Bus message concept. * * Example of use: * @code * int state = ...; - * object_.setProperty("state").onInterface("com.kistler.foo").toValue(state); + * // We can wait until the set operation finishes by waiting on the future + * std::future res = object_.setPropertyAsync("state").onInterface("com.kistler.foo").toValue(state).getResultAsFuture(); * @endcode * * @throws sdbus::Error in case of failure */ - [[nodiscard]] PropertySetter setProperty(const std::string& propertyName); + [[nodiscard]] AsyncPropertySetter setPropertyAsync(const std::string& propertyName); + + /*! + * @brief Gets values of all properties of the D-Bus object + * + * @return A helper object for convenient getting of properties' values + * + * This is a high-level, convenience way of reading D-Bus properties' values that abstracts + * from the D-Bus message concept. + * + * Example of use: + * @code + * auto props = object.getAllProperties().onInterface("com.kistler.foo"); + * @endcode + * + * @throws sdbus::Error in case of failure + */ + [[nodiscard]] AllPropertiesGetter getAllProperties(); + + /*! + * @brief Gets values of all properties of the D-Bus object asynchronously + * + * @return A helper object for convenient asynchronous getting of properties' values + * + * This is a high-level, convenience way of reading D-Bus properties' values that abstracts + * from the D-Bus message concept. + * + * Example of use: + * @code + * auto callback = [](const sdbus::Error* err, const std::map>& properties){ ... }; + * auto props = object.getAllPropertiesAsync().onInterface("com.kistler.foo").uponReplyInvoke(std::move(callback)); + * @endcode + * + * @throws sdbus::Error in case of failure + */ + [[nodiscard]] AsyncAllPropertiesGetter getAllPropertiesAsync(); /*! * @brief Provides D-Bus connection used by the proxy @@ -326,7 +404,7 @@ namespace sdbus { virtual const Message* getCurrentlyProcessedMessage() const = 0; /*! - * @brief Calls method on the proxied D-Bus object asynchronously + * @brief Calls method on the D-Bus object asynchronously * * @param[in] message Message representing an async method call * @param[in] asyncReplyCallback Handler for the async reply @@ -445,11 +523,31 @@ namespace sdbus { return PropertyGetter(*this, propertyName); } + inline AsyncPropertyGetter IProxy::getPropertyAsync(const std::string& propertyName) + { + return AsyncPropertyGetter(*this, propertyName); + } + inline PropertySetter IProxy::setProperty(const std::string& propertyName) { return PropertySetter(*this, propertyName); } + inline AsyncPropertySetter IProxy::setPropertyAsync(const std::string& propertyName) + { + return AsyncPropertySetter(*this, propertyName); + } + + inline AllPropertiesGetter IProxy::getAllProperties() + { + return AllPropertiesGetter(*this); + } + + inline AsyncAllPropertiesGetter IProxy::getAllPropertiesAsync() + { + return AsyncAllPropertiesGetter(*this); + } + /*! * @brief Creates a proxy object for a specific remote D-Bus object * diff --git a/include/sdbus-c++/StandardInterfaces.h b/include/sdbus-c++/StandardInterfaces.h index 0c0df0d..d6c37f5 100644 --- a/include/sdbus-c++/StandardInterfaces.h +++ b/include/sdbus-c++/StandardInterfaces.h @@ -138,16 +138,52 @@ namespace sdbus { return proxy_->getProperty(propertyName).onInterface(interfaceName); } + template + PendingAsyncCall GetAsync(const std::string& interfaceName, const std::string& propertyName, _Function&& callback) + { + return proxy_->getPropertyAsync(propertyName).onInterface(interfaceName).uponReplyInvoke(std::forward<_Function>(callback)); + } + + std::future GetAsync(const std::string& interfaceName, const std::string& propertyName, with_future_t) + { + return proxy_->getPropertyAsync(propertyName).onInterface(interfaceName).getResultAsFuture(); + } + void Set(const std::string& interfaceName, const std::string& propertyName, const sdbus::Variant& value) { proxy_->setProperty(propertyName).onInterface(interfaceName).toValue(value); } + void Set(const std::string& interfaceName, const std::string& propertyName, const sdbus::Variant& value, dont_expect_reply_t) + { + proxy_->setProperty(propertyName).onInterface(interfaceName).toValue(value, dont_expect_reply); + } + + template + PendingAsyncCall SetAsync(const std::string& interfaceName, const std::string& propertyName, const sdbus::Variant& value, _Function&& callback) + { + return proxy_->setPropertyAsync(propertyName).onInterface(interfaceName).toValue(value).uponReplyInvoke(std::forward<_Function>(callback)); + } + + std::future SetAsync(const std::string& interfaceName, const std::string& propertyName, const sdbus::Variant& value, with_future_t) + { + return proxy_->setPropertyAsync(propertyName).onInterface(interfaceName).toValue(value).getResultAsFuture(); + } + std::map GetAll(const std::string& interfaceName) { - std::map props; - proxy_->callMethod("GetAll").onInterface(INTERFACE_NAME).withArguments(interfaceName).storeResultsTo(props); - return props; + return proxy_->getAllProperties().onInterface(interfaceName); + } + + template + PendingAsyncCall GetAllAsync(const std::string& interfaceName, _Function&& callback) + { + return proxy_->getAllPropertiesAsync().onInterface(interfaceName).uponReplyInvoke(std::forward<_Function>(callback)); + } + + std::future> GetAllAsync(const std::string& interfaceName, with_future_t) + { + return proxy_->getAllPropertiesAsync().onInterface(interfaceName).getResultAsFuture(); } private: diff --git a/include/sdbus-c++/TypeTraits.h b/include/sdbus-c++/TypeTraits.h index 77376f9..1de2ef4 100644 --- a/include/sdbus-c++/TypeTraits.h +++ b/include/sdbus-c++/TypeTraits.h @@ -93,6 +93,9 @@ namespace sdbus { // Tag denoting an asynchronous call that returns std::future as a handle struct with_future_t { explicit with_future_t() = default; }; inline constexpr with_future_t with_future{}; + // Tag denoting a call where the reply shouldn't be waited for + struct dont_expect_reply_t { explicit dont_expect_reply_t() = default; }; + inline constexpr dont_expect_reply_t dont_expect_reply{}; // Template specializations for getting D-Bus signatures from C++ types template diff --git a/tests/integrationtests/DBusStandardInterfacesTests.cpp b/tests/integrationtests/DBusStandardInterfacesTests.cpp index 3ccfa2a..cc85cdd 100644 --- a/tests/integrationtests/DBusStandardInterfacesTests.cpp +++ b/tests/integrationtests/DBusStandardInterfacesTests.cpp @@ -82,6 +82,29 @@ TEST_F(SdbusTestObject, GetsPropertyViaPropertiesInterface) ASSERT_THAT(m_proxy->Get(INTERFACE_NAME, "state").get(), Eq(DEFAULT_STATE_VALUE)); } +TEST_F(SdbusTestObject, GetsPropertyAsynchronouslyViaPropertiesInterface) +{ + std::promise promise; + auto future = promise.get_future(); + + m_proxy->GetAsync(INTERFACE_NAME, "state", [&](const sdbus::Error* err, sdbus::Variant value) + { + if (err == nullptr) + promise.set_value(value.get()); + else + promise.set_exception(std::make_exception_ptr(*err)); + }); + + ASSERT_THAT(future.get(), Eq(DEFAULT_STATE_VALUE)); +} + +TEST_F(SdbusTestObject, GetsPropertyAsynchronouslyViaPropertiesInterfaceWithFuture) +{ + auto future = m_proxy->GetAsync(INTERFACE_NAME, "state", sdbus::with_future); + + ASSERT_THAT(future.get().get(), Eq(DEFAULT_STATE_VALUE)); +} + TEST_F(SdbusTestObject, SetsPropertyViaPropertiesInterface) { uint32_t newActionValue = 2345; @@ -91,6 +114,34 @@ TEST_F(SdbusTestObject, SetsPropertyViaPropertiesInterface) ASSERT_THAT(m_proxy->action(), Eq(newActionValue)); } +TEST_F(SdbusTestObject, SetsPropertyAsynchronouslyViaPropertiesInterface) +{ + uint32_t newActionValue = 2346; + std::promise promise; + auto future = promise.get_future(); + + m_proxy->SetAsync(INTERFACE_NAME, "action", sdbus::Variant{newActionValue}, [&](const sdbus::Error* err) + { + if (err == nullptr) + promise.set_value(); + else + promise.set_exception(std::make_exception_ptr(*err)); + }); + + ASSERT_NO_THROW(future.get()); + ASSERT_THAT(m_proxy->action(), Eq(newActionValue)); +} + +TEST_F(SdbusTestObject, SetsPropertyAsynchronouslyViaPropertiesInterfaceWithFuture) +{ + uint32_t newActionValue = 2347; + + auto future = m_proxy->SetAsync(INTERFACE_NAME, "action", sdbus::Variant{newActionValue}, sdbus::with_future); + + ASSERT_NO_THROW(future.get()); + ASSERT_THAT(m_proxy->action(), Eq(newActionValue)); +} + TEST_F(SdbusTestObject, GetsAllPropertiesViaPropertiesInterface) { const auto properties = m_proxy->GetAll(INTERFACE_NAME); @@ -101,6 +152,38 @@ TEST_F(SdbusTestObject, GetsAllPropertiesViaPropertiesInterface) EXPECT_THAT(properties.at("blocking").get(), Eq(DEFAULT_BLOCKING_VALUE)); } +TEST_F(SdbusTestObject, GetsAllPropertiesAsynchronouslyViaPropertiesInterface) +{ + std::promise> promise; + auto future = promise.get_future(); + + m_proxy->GetAllAsync(INTERFACE_NAME, [&](const sdbus::Error* err, std::map value) + { + if (err == nullptr) + promise.set_value(std::move(value)); + else + promise.set_exception(std::make_exception_ptr(*err)); + }); + const auto properties = future.get(); + + ASSERT_THAT(properties, SizeIs(3)); + EXPECT_THAT(properties.at("state").get(), Eq(DEFAULT_STATE_VALUE)); + EXPECT_THAT(properties.at("action").get(), Eq(DEFAULT_ACTION_VALUE)); + EXPECT_THAT(properties.at("blocking").get(), Eq(DEFAULT_BLOCKING_VALUE)); +} + +TEST_F(SdbusTestObject, GetsAllPropertiesAsynchronouslyViaPropertiesInterfaceWithFuture) +{ + auto future = m_proxy->GetAllAsync(INTERFACE_NAME, sdbus::with_future); + + auto properties = future.get(); + + ASSERT_THAT(properties, SizeIs(3)); + EXPECT_THAT(properties.at("state").get(), Eq(DEFAULT_STATE_VALUE)); + EXPECT_THAT(properties.at("action").get(), Eq(DEFAULT_ACTION_VALUE)); + EXPECT_THAT(properties.at("blocking").get(), Eq(DEFAULT_BLOCKING_VALUE)); +} + TEST_F(SdbusTestObject, EmitsPropertyChangedSignalForSelectedProperties) { std::atomic signalReceived{false}; diff --git a/tools/xml2cpp-codegen/ProxyGenerator.cpp b/tools/xml2cpp-codegen/ProxyGenerator.cpp index f241623..e9511f7 100644 --- a/tools/xml2cpp-codegen/ProxyGenerator.cpp +++ b/tools/xml2cpp-codegen/ProxyGenerator.cpp @@ -108,12 +108,18 @@ std::string ProxyGenerator::processInterface(Node& interface) const if (!declaration.empty()) body << declaration << endl; - std::string methodDefinitions, asyncDeclarations; - std::tie(methodDefinitions, asyncDeclarations) = processMethods(methods); + std::string methodDefinitions, asyncDeclarationsMethods; + std::tie(methodDefinitions, asyncDeclarationsMethods) = processMethods(methods); + std::string propertyDefinitions, asyncDeclarationsProperties; + std::tie(propertyDefinitions, asyncDeclarationsProperties) = processProperties(properties); - if (!asyncDeclarations.empty()) + if (!asyncDeclarationsMethods.empty()) { - body << asyncDeclarations << endl; + body << asyncDeclarationsMethods << endl; + } + if (!asyncDeclarationsProperties.empty()) + { + body << asyncDeclarationsProperties << endl; } if (!methodDefinitions.empty()) @@ -121,7 +127,6 @@ std::string ProxyGenerator::processInterface(Node& interface) const body << "public:" << endl << methodDefinitions; } - std::string propertyDefinitions = processProperties(properties); if (!propertyDefinitions.empty()) { body << "public:" << endl << propertyDefinitions; @@ -293,9 +298,9 @@ std::tuple ProxyGenerator::processSignals(const Nodes& return std::make_tuple(registrationSS.str(), declarationSS.str()); } -std::string ProxyGenerator::processProperties(const Nodes& properties) const +std::tuple ProxyGenerator::processProperties(const Nodes& properties) const { - std::ostringstream propertySS; + std::ostringstream propertySS, asyncDeclarationSS; for (const auto& property : properties) { auto propertyName = property->get("name"); @@ -307,25 +312,93 @@ std::string ProxyGenerator::processProperties(const Nodes& properties) const 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 + + Nodes annotations = (*property)["annotation"]; + for (const auto& annotation : annotations) + { + const auto annotationName = annotation->get("name"); + 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; + else if (annotationName == "org.freedesktop.DBus.Property.Get.Async.ClientImpl" && annotationValue == "callback") + futureGet = false; + else if (annotationName == "org.freedesktop.DBus.Property.Get.Async.ClientImpl" && (annotationValue == "future" || annotationValue == "std::future")) + futureGet = true; + else if (annotationName == "org.freedesktop.DBus.Property.Set.Async" && annotationValue == "client") // Server-side not supported (may be in the future) + asyncSet = true; + else if (annotationName == "org.freedesktop.DBus.Property.Set.Async.ClientImpl" && annotationValue == "callback") + futureSet = false; + else if (annotationName == "org.freedesktop.DBus.Property.Set.Async.ClientImpl" && (annotationValue == "future" || annotationValue == "std::future")) + futureSet = true; + } + if (propertyAccess == "read" || propertyAccess == "readwrite") { - propertySS << tab << propertyType << " " << propertyNameSafe << "()" << endl + const std::string realRetType = (asyncGet ? (futureGet ? "std::future" : "sdbus::PendingAsyncCall") : propertyType); + + propertySS << tab << realRetType << " " << propertyNameSafe << "()" << endl << tab << "{" << endl; - propertySS << tab << tab << "return proxy_->getProperty(\"" << propertyName << "\")" + propertySS << tab << tab << "return proxy_->getProperty" << (asyncGet ? "Async" : "") << "(\"" << propertyName << "\")" ".onInterface(INTERFACE_NAME)"; + if (asyncGet) + { + auto nameBigFirst = propertyName; + nameBigFirst[0] = islower(nameBigFirst[0]) ? nameBigFirst[0] + 'A' - 'a' : nameBigFirst[0]; + + if (futureGet) // Async methods implemented through future + { + propertySS << ".getResultAsFuture()"; + } + else // Async methods implemented through callbacks + { + propertySS << ".uponReplyInvoke([this](const sdbus::Error* error, const sdbus::Variant& value)" + "{ this->on" << nameBigFirst << "PropertyGetReply(value.get<" << propertyType << ">(), error); })"; + + asyncDeclarationSS << tab << "virtual void on" << nameBigFirst << "PropertyGetReply(" + << "const " << propertyType << "& value, const sdbus::Error* error) = 0;" << endl; + } + } propertySS << ";" << endl << tab << "}" << endl << endl; } if (propertyAccess == "readwrite" || propertyAccess == "write") { - propertySS << tab << "void " << propertyNameSafe << "(" << propertyTypeArg << ")" << endl - << tab << "{" << endl; - propertySS << tab << tab << "proxy_->setProperty(\"" << propertyName << "\")" + const std::string realRetType = (asyncSet ? (futureSet ? "std::future" : "sdbus::PendingAsyncCall") : "void"); + + propertySS << tab << realRetType << " " << propertyNameSafe << "(" << propertyTypeArg << ")" << endl + << tab << "{" << endl; + propertySS << tab << tab << (asyncSet ? "return " : "") << "proxy_->setProperty" << (asyncSet ? "Async" : "") + << "(\"" << propertyName << "\")" ".onInterface(INTERFACE_NAME)" ".toValue(" << propertyArg << ")"; + + if (asyncSet) + { + auto nameBigFirst = propertyName; + nameBigFirst[0] = islower(nameBigFirst[0]) ? nameBigFirst[0] + 'A' - 'a' : nameBigFirst[0]; + + if (futureSet) // Async methods implemented through future + { + propertySS << ".getResultAsFuture()"; + } + else // Async methods implemented through callbacks + { + propertySS << ".uponReplyInvoke([this](const sdbus::Error* error)" + "{ this->on" << nameBigFirst << "PropertySetReply(error); })"; + + asyncDeclarationSS << tab << "virtual void on" << nameBigFirst << "PropertySetReply(" + << "const sdbus::Error* error) = 0;" << endl; + } + } + propertySS << ";" << endl << tab << "}" << endl << endl; } } - return propertySS.str(); + return std::make_tuple(propertySS.str(), asyncDeclarationSS.str()); } diff --git a/tools/xml2cpp-codegen/ProxyGenerator.h b/tools/xml2cpp-codegen/ProxyGenerator.h index 07a05c6..50c1ae2 100644 --- a/tools/xml2cpp-codegen/ProxyGenerator.h +++ b/tools/xml2cpp-codegen/ProxyGenerator.h @@ -73,7 +73,7 @@ private: * @param properties * @return source code */ - std::string processProperties(const sdbuscpp::xml::Nodes& properties) const; + std::tuple processProperties(const sdbuscpp::xml::Nodes& properties) const; };