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