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()}