Add coroutine support for sdbusplus::asio method calls

Using a coroutine to asynchronously execute method calls gives the best
of both worlds:
1) better readability because the code reads like synchronous code
2) better throughput because it is actually asynchronous

When passed in a boost::asio::yield_context, the sdbusplus::asio dbus
connection members async_send and async_method_call will execute
asynchronously using coroutines.

This also adds an example of how this works in the
example/asio-example.cpp file.

Change-Id: Ifb71b2c757ecbfd16b3be95bdefc45a701ca0d51
Signed-off-by: Vernon Mauery <vernon.mauery@linux.intel.com>
diff --git a/example/Makefile.am b/example/Makefile.am
index 86ab50a..c62fcf9 100644
--- a/example/Makefile.am
+++ b/example/Makefile.am
@@ -10,11 +10,13 @@
     -DBOOST_ALL_NO_LIB \
     -DBOOST_SYSTEM_NO_DEPRECATED \
     -DBOOST_ERROR_CODE_HEADER_ONLY \
+    -DBOOST_COROUTINES_NO_DEPRECATION_WARNING \
     -I$(top_srcdir)
 
 asio_example_LDADD = \
     $(SYSTEMD_LIBS) \
     $(PTHREAD_LIBS) \
+    -lboost_coroutine \
     ../libsdbusplus.la
 
 asio_example_LDFLAGS = \
diff --git a/example/asio-example.cpp b/example/asio-example.cpp
index cced85d..9819eba 100644
--- a/example/asio-example.cpp
+++ b/example/asio-example.cpp
@@ -1,4 +1,5 @@
 #include <boost/asio.hpp>
+#include <boost/asio/spawn.hpp>
 #include <chrono>
 #include <ctime>
 #include <iostream>
@@ -9,6 +10,7 @@
 #include <sdbusplus/server.hpp>
 #include <sdbusplus/timer.hpp>
 
+using variant = sdbusplus::message::variant<int, std::string>;
 int foo(int test)
 {
     return ++test;
@@ -24,6 +26,68 @@
     return 42;
 }
 
+void do_start_async_method_call_one(
+    std::shared_ptr<sdbusplus::asio::connection> conn,
+    boost::asio::yield_context yield)
+{
+    boost::system::error_code ec;
+    variant testValue;
+    conn->yield_method_call<>(yield[ec], "xyz.openbmc_project.asio-test",
+                              "/xyz/openbmc_project/test",
+                              "org.freedesktop.DBus.Properties", "Set",
+                              "xyz.openbmc_project.test", "int", variant(24));
+    testValue = conn->yield_method_call<variant>(
+        yield[ec], "xyz.openbmc_project.asio-test", "/xyz/openbmc_project/test",
+        "org.freedesktop.DBus.Properties", "Get", "xyz.openbmc_project.test",
+        "int");
+    if (!ec && testValue.get<int>() == 24)
+    {
+        std::cout << "async call to Properties.Get serialized via yield OK!\n";
+    }
+    else
+    {
+        std::cout << "ec = " << ec << ": " << testValue.get<int>() << "\n";
+    }
+    conn->yield_method_call<void>(
+        yield[ec], "xyz.openbmc_project.asio-test", "/xyz/openbmc_project/test",
+        "org.freedesktop.DBus.Properties", "Set", "xyz.openbmc_project.test",
+        "int", variant(42));
+    testValue = conn->yield_method_call<variant>(
+        yield[ec], "xyz.openbmc_project.asio-test", "/xyz/openbmc_project/test",
+        "org.freedesktop.DBus.Properties", "Get", "xyz.openbmc_project.test",
+        "int");
+    if (!ec && testValue.get<int>() == 42)
+    {
+        std::cout << "async call to Properties.Get serialized via yield OK!\n";
+    }
+    else
+    {
+        std::cout << "ec = " << ec << ": " << testValue.get<int>() << "\n";
+    }
+}
+
+void do_start_async_method_call_two(
+    std::shared_ptr<sdbusplus::asio::connection> conn,
+    boost::asio::yield_context yield)
+{
+    boost::system::error_code ec;
+    int32_t testCount;
+    std::string testValue;
+    std::tie(testCount, testValue) =
+        conn->yield_method_call<int32_t, std::string>(
+            yield[ec], "xyz.openbmc_project.asio-test",
+            "/xyz/openbmc_project/test", "xyz.openbmc_project.test",
+            "TestMethod", int32_t(42));
+    if (!ec && testCount == 42 && testValue == "success: 42")
+    {
+        std::cout << "async call to TestMethod serialized via yield OK!\n";
+    }
+    else
+    {
+        std::cout << "ec = " << ec << ": " << testValue << "\n";
+    }
+}
+
 int main()
 {
     using GetSubTreeType = std::vector<std::pair<
@@ -121,7 +185,8 @@
 
     // test method creation
     iface->register_method("TestMethod", [](const int32_t& callCount) {
-        return "success: " + std::to_string(callCount);
+        return std::make_tuple(callCount,
+                               "success: " + std::to_string(callCount));
     });
 
     iface->register_method("TestFunction", foo);
@@ -141,6 +206,14 @@
     // add the sd_event wrapper to the io object
     sdbusplus::asio::sd_event_wrapper sdEvents(io);
 
+    // set up a client to make an async call to the server
+    // using coroutines (userspace cooperative multitasking)
+    boost::asio::spawn(io, [conn](boost::asio::yield_context yield) {
+        do_start_async_method_call_one(conn, yield);
+    });
+    boost::asio::spawn(io, [conn](boost::asio::yield_context yield) {
+        do_start_async_method_call_two(conn, yield);
+    });
     io.run();
 
     return 0;
diff --git a/sdbusplus/asio/connection.hpp b/sdbusplus/asio/connection.hpp
index 35bd853..0c7f80e 100644
--- a/sdbusplus/asio/connection.hpp
+++ b/sdbusplus/asio/connection.hpp
@@ -16,6 +16,7 @@
 #pragma once
 
 #include <boost/asio.hpp>
+#include <boost/asio/spawn.hpp>
 #include <boost/callable_traits.hpp>
 #include <chrono>
 #include <experimental/tuple>
@@ -59,10 +60,23 @@
         socket.release();
     }
 
+    /** @brief Perform an asynchronous send of a message, executing the handler
+     *         upon return and return
+     *
+     *  @param[in] m - A message ready to send
+     *  @param[in] handler - handler to execute upon completion; this may be an
+     *                       asio::yield_context to execute asynchronously as a
+     *                       coroutine
+     *
+     *  @return If a yield context is passed as the handler, the return type is
+     *          a message. If a function object is passed in as the handler,
+     *          the return type is the result of the handler registration,
+     *          while the resulting message will get passed into the handler.
+     */
     template <typename MessageHandler>
     inline BOOST_ASIO_INITFN_RESULT_TYPE(MessageHandler,
                                          void(boost::system::error_code,
-                                              message::message))
+                                              message::message&))
         async_send(message::message& m, MessageHandler&& handler)
     {
         boost::asio::async_completion<
@@ -75,8 +89,27 @@
         return init.result.get();
     }
 
+    /** @brief Perform an asynchronous method call, with input parameter packing
+     *         and return value unpacking
+     *
+     *  @param[in] handler - A function object that is to be called as a
+     *                       continuation for the async dbus method call. The
+     *                       arguments to parse on the return are deduced from
+     *                       the handler's signature and then passed in along
+     *                       with an error code.
+     *  @param[in] service - The service to call.
+     *  @param[in] objpath - The object's path for the call.
+     *  @param[in] interf - The object's interface to call.
+     *  @param[in] method - The object's method to call.
+     *  @param[in] a... - Optional parameters for the method call.
+     *
+     *  @return immediate return of the internal handler registration. The
+     *          result of the actual asynchronous call will get unpacked from
+     *          the message and passed into the handler when the call is
+     *          complete.
+     */
     template <typename MessageHandler, typename... InputArgs>
-    auto async_method_call(MessageHandler handler, const std::string& service,
+    void async_method_call(MessageHandler handler, const std::string& service,
                            const std::string& objpath,
                            const std::string& interf, const std::string& method,
                            const InputArgs&... a)
@@ -84,8 +117,8 @@
         message::message m = new_method_call(service.c_str(), objpath.c_str(),
                                              interf.c_str(), method.c_str());
         m.append(a...);
-        return async_send(m, [handler](boost::system::error_code ec,
-                                       message::message& r) {
+        async_send(m, [handler](boost::system::error_code ec,
+                                message::message& r) {
             using FunctionTuple =
                 boost::callable_traits::args_t<MessageHandler>;
             using UnpackType = typename utility::strip_first_arg<
@@ -107,6 +140,62 @@
         });
     }
 
+    /** @brief Perform a yielding asynchronous method call, with input
+     *         parameter packing and return value unpacking
+     *
+     *  @param[in] yield - A yield context to async block upon. To catch errors
+     *                     for the call, call this function with 'yield[ec]',
+     *                     thus attaching an error code to the yield context
+     *  @param[in] service - The service to call.
+     *  @param[in] objpath - The object's path for the call.
+     *  @param[in] interf - The object's interface to call.
+     *  @param[in] method - The object's method to call.
+     *  @param[in] a... - Optional parameters for the method call.
+     *
+     *  @return Unpacked value of RetType
+     */
+    template <typename... RetTypes, typename... InputArgs>
+    auto yield_method_call(boost::asio::yield_context yield,
+                           const std::string& service,
+                           const std::string& objpath,
+                           const std::string& interf, const std::string& method,
+                           const InputArgs&... a)
+    {
+        message::message m = new_method_call(service.c_str(), objpath.c_str(),
+                                             interf.c_str(), method.c_str());
+        m.append(a...);
+        message::message r = async_send(m, yield);
+        if constexpr (sizeof...(RetTypes) == 0)
+        {
+            // void return
+            return;
+        }
+        else if constexpr (sizeof...(RetTypes) == 1)
+        {
+            if constexpr (std::is_same<utility::first_type<RetTypes...>,
+                                       void>::value)
+            {
+                return;
+            }
+            else
+            {
+                // single item return
+                utility::first_type<RetTypes...> responseData;
+                // this will throw if the signature of r != RetType
+                r.read(responseData);
+                return responseData;
+            }
+        }
+        else
+        {
+            // tuple of things to return
+            std::tuple<RetTypes...> responseData;
+            // this will throw if the signature of r != RetType
+            r.read(responseData);
+            return responseData;
+        }
+    }
+
   private:
     boost::asio::io_service& io_;
     boost::asio::posix::stream_descriptor socket;
diff --git a/sdbusplus/utility/type_traits.hpp b/sdbusplus/utility/type_traits.hpp
index 2d60972..28fa31e 100644
--- a/sdbusplus/utility/type_traits.hpp
+++ b/sdbusplus/utility/type_traits.hpp
@@ -9,6 +9,13 @@
 namespace utility
 {
 
+/** @brief Retrieve the first type from a parameter pack
+ *
+ * @tparam Types - the parameter pack
+ */
+template <typename... Types>
+using first_type = std::tuple_element_t<0, std::tuple<Types...>>;
+
 /** @brief Convert T[N] to T* if is_same<Tbase,T>
  *
  *  @tparam Tbase - The base type expected.