async: add client-object proxy
Add a class which acts as a client-side proxy to a object. The
proxy holds the object address (service, path, interface) and
creates Senders for standard dbus operations: method-call,
get-property, set-property, get-all-properties. These Senders
can also be used in co-routine contexts (via co_await).
Signed-off-by: Patrick Williams <patrick@stwcx.xyz>
Change-Id: I4b2e50a8b55d10975984ac4244bb075db8746fbf
diff --git a/example/coroutine-example.cpp b/example/coroutine-example.cpp
new file mode 100644
index 0000000..8cfa0f4
--- /dev/null
+++ b/example/coroutine-example.cpp
@@ -0,0 +1,81 @@
+#include <sdbusplus/async.hpp>
+
+#include <iostream>
+#include <string>
+#include <variant>
+#include <vector>
+
+auto runner(sdbusplus::async::context& ctx) -> sdbusplus::async::task<>
+{
+ // Create a proxy to the systemd manager object.
+ constexpr auto systemd = sdbusplus::async::proxy()
+ .service("org.freedesktop.systemd1")
+ .path("/org/freedesktop/systemd1")
+ .interface("org.freedesktop.systemd1.Manager");
+
+ // Call ListUnitFiles method.
+ using ret_type = std::vector<std::tuple<std::string, std::string>>;
+ for (auto& [file, status] :
+ co_await systemd.call<ret_type>(ctx, "ListUnitFiles"))
+ {
+ std::cout << file << " " << status << std::endl;
+ }
+
+ // Get the Architecture property.
+ std::cout << co_await systemd.get_property<std::string>(ctx, "Architecture")
+ << std::endl;
+
+ // Get all the properties.
+ using variant_type =
+ std::variant<bool, std::string, std::vector<std::string>, uint64_t,
+ int32_t, uint32_t, double>;
+ for (auto& [property, value] :
+ co_await systemd.get_all_properties<variant_type>(ctx))
+ {
+ std::cout
+ << property << " is "
+ << std::visit(
+ // Convert the variant member to a string for printing.
+ [](auto v) {
+ if constexpr (std::is_same_v<
+ std::remove_cvref_t<decltype(v)>,
+ std::vector<std::string>>)
+ {
+ return std::string{"Array"};
+ }
+ else if constexpr (std::is_same_v<
+ std::remove_cvref_t<decltype(v)>,
+ std::string>)
+ {
+ return v;
+ }
+ else
+ {
+ return std::to_string(v);
+ }
+ },
+ value)
+ << std::endl;
+ }
+
+ // Try to set the Architecture property (which won't work).
+ try
+ {
+ co_await systemd.set_property(ctx, "Architecture", "arm64");
+ }
+ catch (const std::exception& e)
+ {
+ std::cout << "Caught exception because you cannot set Architecture: "
+ << e.what() << std::endl;
+ }
+
+ co_return;
+}
+
+int main()
+{
+ sdbusplus::async::context ctx;
+ ctx.run(runner(ctx));
+
+ return 0;
+}
diff --git a/example/meson.build b/example/meson.build
index 17fd569..1850596 100644
--- a/example/meson.build
+++ b/example/meson.build
@@ -33,6 +33,12 @@
)
executable(
+ 'coroutine-example',
+ 'coroutine-example.cpp',
+ dependencies: [ sdbusplus_dep ],
+)
+
+executable(
'register-property',
'register-property.cpp',
dependencies: asio_dep,
diff --git a/include/sdbusplus/async.hpp b/include/sdbusplus/async.hpp
index 706c3ad..2ef1df6 100644
--- a/include/sdbusplus/async.hpp
+++ b/include/sdbusplus/async.hpp
@@ -2,4 +2,5 @@
#include <sdbusplus/async/context.hpp>
#include <sdbusplus/async/execution.hpp>
+#include <sdbusplus/async/proxy.hpp>
#include <sdbusplus/async/task.hpp>
diff --git a/include/sdbusplus/async/callback.hpp b/include/sdbusplus/async/callback.hpp
new file mode 100644
index 0000000..2d73179
--- /dev/null
+++ b/include/sdbusplus/async/callback.hpp
@@ -0,0 +1,155 @@
+#pragma once
+
+#include <systemd/sd-bus.h>
+
+#include <sdbusplus/async/execution.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <type_traits>
+
+namespace sdbusplus::async
+{
+
+namespace callback_ns
+{
+
+template <typename Fn>
+concept takes_msg_handler =
+ std::is_invocable_r_v<int, Fn, sd_bus_message_handler_t, void*>;
+
+template <takes_msg_handler Init>
+struct callback_sender;
+
+} // namespace callback_ns
+
+/** Create a sd_bus-callback Sender.
+ *
+ * In the sd_bus library there are many functions named `*_async` that take
+ * a `sd_bus_message_handler_t` as the callback. This function turns them
+ * into a Sender.
+ *
+ * The `Init` function is a simple indirect so that the library sd_bus
+ * function can be called, but with the callback handler (and data) placed
+ * in the right call positions.
+ *
+ * For example, `sd_bus_call_async` could be turned into a Sender with a
+ * call to this and a small lambda such as:
+ * ```
+ * callback([bus = get_busp(ctx.get_bus()),
+ * msg = std::move(msg)](auto cb, auto data) {
+ * return sd_bus_call_async(bus, nullptr, msg.get(), cb, data, 0);
+ * })
+ * ```
+ *
+ * @param[in] i - A function which calls the underlying sd_bus library
+ * function.
+ *
+ * @returns A Sender which completes when sd-bus calls the callback and yields
+ * a `sdbusplus::message_t`.
+ */
+template <callback_ns::takes_msg_handler Init>
+auto callback(Init i)
+{
+ return callback_ns::callback_sender<Init>(std::move(i));
+}
+
+namespace callback_ns
+{
+
+/** The operation which handles the Sender completion. */
+template <takes_msg_handler Init, execution::receiver R>
+struct callback_operation
+{
+ callback_operation() = delete;
+ callback_operation(callback_operation&&) = delete;
+
+ callback_operation(Init&& init, R&& r) :
+ init(std::move(init)), receiver(std::move(r))
+ {}
+
+ // Handle the call from sd-bus by ensuring there were no errors
+ // and setting the completion value to the resulting message.
+ static int handler(sd_bus_message* m, void* cb, sd_bus_error* e) noexcept
+ {
+ callback_operation& self = *static_cast<callback_operation*>(cb);
+ try
+ {
+ // Check 'e' for error.
+ if ((nullptr != e) && (sd_bus_error_is_set(e)))
+ {
+ throw exception::SdBusError(e, "callback");
+ }
+
+ message_t msg{m};
+
+ // Check the message response for error.
+ if (msg.is_method_error())
+ {
+ auto err = *msg.get_error();
+ throw exception::SdBusError(&err, "method");
+ }
+
+ execution::set_value(std::move(self.receiver), std::move(msg));
+ }
+ catch (...)
+ {
+ execution::set_error(std::move(self.receiver),
+ std::current_exception());
+ }
+ return 0;
+ }
+
+ // Call the init function upon Sender start.
+ friend void tag_invoke(execution::start_t,
+ callback_operation& self) noexcept
+ {
+ try
+ {
+ auto rc = self.init(handler, &self);
+ if (rc < 0)
+ {
+ throw exception::SdBusError(-rc, __PRETTY_FUNCTION__);
+ }
+ }
+ catch (...)
+ {
+ execution::set_error(std::move(self.receiver),
+ std::current_exception());
+ }
+ }
+
+ private:
+ Init init;
+ R receiver;
+};
+
+/** The Sender for a callback.
+ *
+ * The basically just holds the Init function until the Sender is connected
+ * to (co_awaited on for co-routines), when it is turned into a pending
+ * operation.
+ */
+template <takes_msg_handler Init>
+struct callback_sender
+{
+ explicit callback_sender(Init init) : init(std::move(init)){};
+
+ // This Sender yields a message_t.
+ friend auto tag_invoke(execution::get_completion_signatures_t,
+ const callback_sender&, auto)
+ -> execution::completion_signatures<execution::set_value_t(message_t)>;
+
+ template <execution::receiver R>
+ friend auto tag_invoke(execution::connect_t, callback_sender&& self, R r)
+ -> callback_operation<Init, R>
+ {
+ return {std::move(self.init), std::move(r)};
+ }
+
+ private:
+ Init init;
+};
+
+} // namespace callback_ns
+
+} // namespace sdbusplus::async
diff --git a/include/sdbusplus/async/proxy.hpp b/include/sdbusplus/async/proxy.hpp
new file mode 100644
index 0000000..f4b6eee
--- /dev/null
+++ b/include/sdbusplus/async/proxy.hpp
@@ -0,0 +1,236 @@
+#pragma once
+
+#include <sdbusplus/async/callback.hpp>
+#include <sdbusplus/async/context.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <string>
+#include <string_view>
+#include <type_traits>
+#include <unordered_map>
+#include <variant>
+
+namespace sdbusplus::async
+{
+namespace proxy_ns
+{
+/** A (client-side) proxy to a dbus object.
+ *
+ * A dbus object is referenced by 3 address pieces:
+ * - The service hosting the object.
+ * - The path the object resides at.
+ * - The interface the object implements.
+ * The proxy is a holder of these 3 addresses.
+ *
+ * One all 3 pieces of the address are known by the proxy, the proxy
+ * can be used to perform normal dbus operations: method-call, get-property
+ * set-property, get-all-properties.
+ *
+ * Addresses are supplied to the object by calling the appropriate method:
+ * service, path, interface. These methods return a _new_ object with the
+ * new information filled in.
+ *
+ * If all pieces are known at compile-time it is suggested to be constructed
+ * similar to the following:
+ *
+ * ```
+ * constexpr auto systemd = sdbusplus::async::proxy()
+ * .service("org.freedesktop.systemd1")
+ * .path("/org/freedesktop/systemd1")
+ * .interface("org.freedesktop.systemd1.Manager");
+ * ```
+ *
+ * The proxy object can be filled as information is available and attempts
+ * to be as effecient as possible (supporting constexpr construction and
+ * using std::string_view mostly). In some cases it is necessary for the
+ * proxy to leave a scope where it would be no longer safe to use the
+ * previously-supplied string_views. The `preserve` operation can be used
+ * to transform an existing proxy into one which is safe to leave (because
+ * it uses less-efficient but safe std::string values).
+ */
+template <bool S = false, bool P = false, bool I = false,
+ bool Preserved = false>
+struct proxy : private sdbusplus::bus::details::bus_friend
+{
+ // Some typedefs to reduce template noise...
+
+ using string_t =
+ std::conditional_t<Preserved, std::string, std::string_view>;
+ using string_ref = const string_t&;
+ using sv_ref = const std::string_view&;
+
+ template <bool V>
+ using value_t = std::conditional_t<V, string_t, std::monostate>;
+ template <bool V>
+ using value_ref = const value_t<V>&;
+
+ // Default constructor should only work for the "all empty" case.
+ constexpr proxy()
+ requires(!S && !P && !I)
+ = default;
+ constexpr proxy()
+ requires(S || P || I)
+ = delete;
+
+ // Construtor allowing all 3 to be passed in.
+ constexpr proxy(value_ref<S> s, value_ref<P> p, value_ref<I> i) :
+ s(s), p(p), i(i){};
+
+ // Functions to assign address fields.
+ constexpr auto service(string_ref s) const noexcept
+ requires(!S)
+ {
+ return proxy<true, P, I, Preserved>{s, this->p, this->i};
+ }
+ constexpr auto path(string_ref p) const noexcept
+ requires(!P)
+ {
+ return proxy<S, true, I, Preserved>{this->s, p, this->i};
+ }
+ constexpr auto interface(string_ref i) const noexcept
+ requires(!I)
+ {
+ return proxy<S, P, true, Preserved>{this->s, this->p, i};
+ }
+
+ /** Make a copyable / returnable proxy.
+ *
+ * Since proxy deals with string_view by default, for efficiency,
+ * there are cases where it would be dangerous for a proxy object to
+ * leave a scope either by a return or a pass into a lambda. This
+ * function will convert an existing proxy into one backed by
+ * `std::string` so that it can safely leave a scope.
+ */
+ auto preserve() const noexcept
+ requires(!Preserved)
+ {
+ return proxy<S, P, I, true>{std::string{this->s}, std::string{this->p},
+ std::string{this->i}};
+ }
+
+ /** Perform a method call.
+ *
+ * @tparam Rs - The return type(s) of the method call.
+ * @tparam Ss - The parameter type(s) of the method call.
+ *
+ * @param[in] ctx - The context to use.
+ * @param[in] method - The method name.
+ * @param[in] ss - The calling parameters.
+ *
+ * @return A Sender which completes with either { void, Rs, tuple<Rs...> }.
+ */
+ template <typename... Rs, typename... Ss>
+ auto call(context& ctx, sv_ref method, Ss&&... ss) const
+ requires((S) && (P) && (I))
+ {
+ // Create the method_call message.
+ auto msg = ctx.get_bus().new_method_call(c_str(s), c_str(p), c_str(i),
+ method.data());
+ if constexpr (sizeof...(Ss) > 0)
+ {
+ msg.append(std::forward<Ss>(ss)...);
+ }
+
+ // Use 'callback' to perform the operation and "then" "unpack" the
+ // contents.
+ return callback([bus = get_busp(ctx.get_bus()),
+ msg = std::move(msg)](auto cb, auto data) mutable {
+ return sd_bus_call_async(bus, nullptr, msg.get(), cb, data,
+ 0);
+ }) |
+ execution::then([](message_t&& m) { return m.unpack<Rs...>(); });
+ }
+
+ /** Get a property.
+ *
+ * @tparam T - The type of the property.
+ *
+ * @param[in] ctx - The context to use.
+ * @param[in] property - The property name.
+ *
+ * @return A Sender which completes with T as the property value.
+ */
+ template <typename T>
+ auto get_property(context& ctx, sv_ref property) const
+ requires((S) && (P) && (I))
+ {
+ using result_t = std::variant<T>;
+ auto prop_intf = proxy(s, p, dbus_prop_intf);
+
+ return prop_intf.template call<result_t>(ctx, "Get", c_str(i),
+ property.data()) |
+ execution::then([](result_t&& v) { return std::get<T>(v); });
+ }
+
+ /** Get all properties.
+ *
+ * @tparam V - The variant type of all possible properties.
+ *
+ * @param[in] ctx - The context to use.
+ *
+ * @return A Sender which completes with unordered_map<string, V>.
+ */
+ template <typename V>
+ auto get_all_properties(context& ctx) const
+ requires((S) && (P) && (I))
+ {
+ using result_t = std::unordered_map<std::string, V>;
+ auto prop_intf = proxy(s, p, dbus_prop_intf);
+
+ return prop_intf.template call<result_t>(ctx, "GetAll", c_str(i));
+ }
+
+ /** Set a property.
+ *
+ * @tparam T - The type of the property (usually deduced by the compiler).
+ *
+ * @param[in] ctx - The context to use.
+ * @param[in] property - The property name.
+ * @param[in] value - The value to set.
+ *
+ * @return A Sender which completes void when the property is set.
+ */
+ template <typename T>
+ auto set_property(context& ctx, sv_ref property, T&& value) const
+ requires((S) && (P) && (I))
+ {
+ auto prop_intf = proxy(s, p, dbus_prop_intf);
+
+ return prop_intf.template call<>(ctx, "Set", c_str(i), property.data(),
+ std::forward<T>(value));
+ }
+
+ private:
+ static constexpr std::string_view dbus_prop_intf =
+ "org.freedesktop.DBus.Properties";
+
+ // Helper to get the underlying c-string of a string_view or string.
+ static auto c_str(string_ref v)
+ {
+ if constexpr (Preserved)
+ {
+ return v.c_str();
+ }
+ else
+ {
+ return v.data();
+ }
+ }
+
+ value_t<S> s = {};
+ value_t<P> p = {};
+ value_t<I> i = {};
+};
+
+} // namespace proxy_ns
+
+// clang currently has problems with the intersect of default template
+// parameters and concepts. I've opened llvm/llvm-project#57646 and added
+// this indirect.
+using proxy = proxy_ns::proxy<>;
+
+// Sometimes it is useful to hold onto a proxy, such as in a class member, so
+// define a type alias for one which can be safely held.
+using finalized_proxy = proxy_ns::proxy<true, true, true, true>;
+
+} // namespace sdbusplus::async