sdbus++: add start of an async client

Add generator mako templates to generate a simple async client, a
corresponding implementation of the 'calculator' interface as a
client, and a client aggregation class (client_t) similar to the
server aggregation class we have (sdbusplus::server::object_t).

Signed-off-by: Patrick Williams <patrick@stwcx.xyz>
Change-Id: I6795b2d051bf4d759cb319a9388b23e29d140244
diff --git a/example/calculator-client.cpp b/example/calculator-client.cpp
new file mode 100644
index 0000000..7c9eab2
--- /dev/null
+++ b/example/calculator-client.cpp
@@ -0,0 +1,57 @@
+#include <net/poettering/Calculator/client.hpp>
+#include <sdbusplus/async.hpp>
+
+#include <iostream>
+
+auto startup(sdbusplus::async::context& ctx) -> sdbusplus::async::task<>
+{
+    constexpr auto service = "net.poettering.Calculator";
+    constexpr auto path = "/net/poettering/calculator";
+
+    auto c =
+        sdbusplus::client::net::poettering::Calculator().service(service).path(
+            path);
+
+    // Alternatively, sdbusplus::async::client_t<Calculator, ...>() could have
+    // been used to combine multiple interfaces into a single client-proxy.
+
+    {
+        auto _ = co_await c.call<int64_t>(ctx, "Multiply", int64_t(7),
+                                          int64_t(6));
+        std::cout << "Should be 42: " << _ << std::endl;
+    }
+
+    {
+        auto _ = co_await c.get_property<int64_t>(ctx, "LastResult");
+        std::cout << "Should be 42: " << _ << std::endl;
+    }
+
+    {
+        co_await c.call<>(ctx, "Clear");
+    }
+
+    {
+        auto _ = co_await c.get_property<int64_t>(ctx, "LastResult");
+        std::cout << "Should be 0: " << _ << std::endl;
+    }
+
+    {
+        co_await c.set_property<int64_t>(ctx, "LastResult", 1234);
+        auto _ = co_await c.get_property<int64_t>(ctx, "LastResult");
+        std::cout << "Should be 1234: " << _ << std::endl;
+    }
+
+    co_return;
+}
+
+int main()
+{
+    sdbusplus::async::context ctx;
+    ctx.spawn(startup(ctx));
+    ctx.spawn(
+        sdbusplus::async::execution::just() |
+        sdbusplus::async::execution::then([&ctx]() { ctx.request_stop(); }));
+    ctx.run();
+
+    return 0;
+}
diff --git a/example/meson.build b/example/meson.build
index 1850596..75b265c 100644
--- a/example/meson.build
+++ b/example/meson.build
@@ -63,3 +63,12 @@
     include_directories: include_directories('gen'),
     dependencies: sdbusplus_dep,
 )
+
+executable(
+    'calculator-client',
+    'calculator-client.cpp',
+    generated_sources,
+    implicit_include_directories: false,
+    include_directories: include_directories('gen'),
+    dependencies: sdbusplus_dep,
+)
diff --git a/include/sdbusplus/async/client.hpp b/include/sdbusplus/async/client.hpp
new file mode 100644
index 0000000..ae78b80
--- /dev/null
+++ b/include/sdbusplus/async/client.hpp
@@ -0,0 +1,76 @@
+#pragma once
+
+#include <sdbusplus/async/proxy.hpp>
+
+namespace sdbusplus::async
+{
+
+namespace client
+{
+
+/** An aggregation class of sdbusplus::async::proxy-based client types.
+ *
+ *  The resulting class acts as a union of all Types from the template
+ *  arguments.
+ *
+ *  Like a `proxy`, the class only becomes functional once the service and
+ *  path are populated.
+ */
+template <bool S, bool P, bool Preserved, template <typename> typename... Types>
+class client :
+    public Types<sdbusplus::async::proxy_ns::proxy<S, P, false, Preserved>>...
+{
+  private:
+    sdbusplus::async::proxy_ns::proxy<S, P, false, Preserved> proxy{};
+
+  public:
+    /* Delete default constructor if Service or Path have been provided. */
+    constexpr client()
+        requires(S || P)
+    = delete;
+    /* Default (empty) constructor only when Service and Path are missing. */
+    constexpr client()
+        requires(!S && !P)
+        : Types<decltype(proxy)>(proxy)...
+    {}
+
+    /* Conversion constructor for a non-empty (Service and/or Path) proxy. */
+    constexpr explicit client(
+        sdbusplus::async::proxy_ns::proxy<S, P, false, Preserved> p)
+        requires(S || P)
+        : Types<decltype(proxy)>(p)..., proxy(p)
+    {}
+
+    /* Convert a non-Service instance to a Service instance. */
+    constexpr auto service(auto& s) const noexcept
+        requires(!S)
+    {
+        return client<true, P, Preserved, Types...>(proxy.service(s));
+    }
+
+    /* Convert a non-Path instance to a Path instance. */
+    constexpr auto path(auto& p) const noexcept
+        requires(!P)
+    {
+        return client<S, true, Preserved, Types...>(proxy.path(p));
+    }
+};
+
+} // namespace client
+
+/** A non-Preserved client alias.
+ *
+ *  This holds Service/Path in string-views, which must exist longer than
+ *  the lifetime of this client_t.
+ */
+template <template <typename> typename... Types>
+using client_t = client::client<false, false, false, Types...>;
+/** A Preserved client alias.
+ *
+ *  This holds Service/Path in strings, which thus have lifetimes that are
+ *  the same as the client itself.
+ */
+template <template <typename> typename... Types>
+using client_preserved_t = client::client<false, false, false, Types...>;
+
+} // namespace sdbusplus::async
diff --git a/tools/sdbusplus/templates/interface.client.hpp.mako b/tools/sdbusplus/templates/interface.client.hpp.mako
index 7c3c6e8..8400a88 100644
--- a/tools/sdbusplus/templates/interface.client.hpp.mako
+++ b/tools/sdbusplus/templates/interface.client.hpp.mako
@@ -1,5 +1,10 @@
 #pragma once
+#include <sdbusplus/async/client.hpp>
+#include <type_traits>
 
+% for h in interface.cpp_includes():
+#include <${h}>
+% endfor
 #include <${interface.headerFile("common")}>
 
 #ifndef SDBUSPP_REMOVE_DEPRECATED_NAMESPACE
@@ -12,7 +17,76 @@
 } // namespace sdbusplus::${interface.old_cppNamespacedClass("client")}
 #endif
 
-namespace sdbusplus::client::${interface.cppNamespacedClass()}
+namespace sdbusplus::client::${interface.cppNamespace()}
 {
 
+namespace details
+{
+
+template <typename Proxy>
+class ${interface.classname} :
+    public sdbusplus::common::${interface.cppNamespacedClass()}
+{
+  public:
+    template <bool S, bool P, bool Preserved,
+              template <typename> typename... Types>
+    friend class sdbusplus::async::client::client;
+    // Delete default constructor as these should only be constructed
+    // indirectly through sdbusplus::async::client_t.
+    ${interface.classname}() = delete;
+
+    // To be replaced by generators...
+    template <typename... Rs, typename... Ss>
+    auto call(sdbusplus::async::context& ctx, auto& method, Ss&&... ss) const
+    {
+        return proxy.template call<Rs...>(ctx, method, std::forward<Ss>(ss)...);
+    }
+
+    // To be replaced by generators...
+    template <typename T>
+    auto get_property(sdbusplus::async::context& ctx, auto& property) const
+    {
+        return proxy.template get_property<T>(ctx, property);
+    }
+
+    // To be replaced by generators...
+    template <typename T>
+    auto set_property(sdbusplus::async::context& ctx, auto& property,
+                      T&& value) const
+    {
+        return proxy.template set_property<T>(ctx, property,
+                                              std::forward<T>(value));
+    }
+
+  private:
+    // Conversion constructor from proxy used by client_t.
+    constexpr explicit ${interface.classname}(Proxy p) :
+        proxy(p.interface(interface)) {}
+
+    decltype(std::declval<Proxy>().interface(interface)) proxy = {};
+};
+
+} // namespace details
+
+/** Alias class so we can use the client in both a client_t aggregation
+ *  and individually.
+ *
+ *  sdbusplus::async::client_t<${interface.classname}>() or
+ *  ${interface.classname}() both construct an equivalent instance.
+ */
+template <typename Proxy = void>
+struct ${interface.classname} : public
+    std::conditional_t<
+        std::is_void_v<Proxy>,
+        sdbusplus::async::client_t<details::${interface.classname}>,
+        details::${interface.classname}<Proxy>>
+{
+    template <typename... Args>
+    ${interface.classname}(Args&&... args) :
+        std::conditional_t<std::is_void_v<Proxy>,
+                           sdbusplus::async::client_t<details::${interface.classname}>,
+                           details::${interface.classname}<Proxy>>(
+            std::forward<Args>(args)...) {}
+};
+
 } // namespace sdbusplus::client::${interface.cppNamespacedClass()}